有时候,我们不仅仅满足于 github 上现有的 webpack plugin,还想根据架构设计或者业务场景进行个性化的 plugin 定制,来辅助我们的工程化体系的构建。

比如说,我在开始调试前端项目的时候,还想另外起一个 nodejs 服务器,来 mock 一些数据,以便在后端开发滞后或者不能灵活地提供目标数据给前端时,我们能够自己伪造一些数据,来独立于后端开发进程。

起步

https://webpack.docschina.org/contribute/writing-a-plugin/

创建一个插件的步骤构成:

  • 写一个插件构造函数,并定义一个 apply 方法,以供 webpack 加载 plugin 时进行调用;
  • 指定需要绑定到 webpack 自身的事件钩子;
  • 使用 webpack 提供的 plugin API 操作构建结果;

来一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class MySimplePlugin {
constructor (options) {
this.options = options;
}

// 当 webpack 加载 plugin 时,就会调用 apply 函数
apply (compiler) {
const plugin = { name: 'MySimplePlugin' };

// 为旧版本作了一下兼容
const _registHook = (target, plugin)=>{
return target.hooks
? ((hookName, hookFn, opt={}) => {
let tapWay;
if (Object.getPrototypeOf(opt) !== Obejct.prototype) {
throw new Error('Please confirm opt is an plain object.');
}
switch (opt.tapWay) {
case 'tapAsync':
tapWay = 'tapAsync'; break;
case 'tapPromise':
tapWay = 'tapPromise'; break;
case 'tap':
default:
tapWay = 'tap'; break;
}
target['hooks'][hookName][tapWay](plugin, hookFn);
})
: ((hookName, hookFn) => { target.plugin(hookName, hookFn) });
};

const environment = () => {
console.log([].slice.call(arguments, 0));
console.log('environment 准备好之后,执行了该插件');
};

const afterCompile = (compilation, cb) => {
console.log(compilation);
console.log('编译完成之后,执行了该插件');
cb();
};

const _registCompilerHook = _registHook(compiler, plugin);
_registCompilerHook('environment', environment, { tapWay: 'tap' });
_registCompilerHook('afterCompile', afterCompile, { tapWay: 'tapAsync' });
}
}

export default MySimplePluginl;

apply 方法会在插件安装时,被 webpack plugin 调用一次;其可以接收一个 compiler 对象的引用,从而在回调中可以访问到。

添加完成之后,还需要在 webpack 配置文件中的 plugins 数组字段中,添加我们刚才创建的插件实例。

重点关注 compilercompilation

扩展 webpack 引擎、打造 webpack 插件重要的第一步,就是理解 compilercompilation 对象的角色。

compiler 对象
该对象代表了完整的 webpack 环境配置,其在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 optionsloaderplugin 等等。挡在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。因此,可以使用 compiler 来访问 webpack 的朱环境。

compilation 对象
该对象代表了一次资源版本构建,当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

访问 compilercompilation

compiler 会暴露一组钩子,提供对每个新的编译对象(compilation)的引用。反过来,编译对象(compilation)提供了额外的事件钩子函数,用于钩入到构建流程的步骤中。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MySimplePlugin {
constructor (options) {
this.options = options;
}

apply () {
//... 承接上面内容

const compilation = (compilation) => {
console.log(compilation);
console.log('编译(compilation)创建之后,执行插件');

const _registCompilationHook = _registHook(compilation, plugin);
const optimize = () => {
console.log('优化阶段开始触发');
};

_registCompilationHook('optimize', optimize, false);
};


}
}

详细的 compiler 和 compilation 各自可用的钩子,可参阅:官方插件 API 文档

注册事件钩子

可以注意到,上面我们注册事件钩子函数的时候,有 3 种写法:taptapAsynctapPromise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const _registHook = (target, plugin)=>{
return target.hooks
? ((hookName, hookFn, opt={}) => {
let tapWay;
if (Object.getPrototypeOf(opt) !== Obejct.prototype) {
throw new Error('Please confirm opt is an plain object.');
}
switch (opt.tapWay) {
case 'tapAsync':
tapWay = 'tapAsync'; break;
case 'tapPromise':
tapWay = 'tapPromise'; break;
case 'tap':
default:
tapWay = 'tap'; break;
}
target['hooks'][hookName][tapWay](plugin, hookFn);
})
: ((hookName, hookFn) => { target.plugin(hookName, hookFn) });
};

来看一下具体用法和区别:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MySimplePlugin {
// ...
apply (compiler) {
// ...
const _registCompilerHook = _registHook(compiler, plugin);

_registCompilerHook('emit', ()=>{
// 这里没有异步任务
console.log('Done with sync work.');
}, { tapWay: 'tap' });

// tapAsync 注册钩子函数
_registCompilerHook('emit', (compilation, cb)=>{
// 这里有异步任务
setTimeout(() => {
console.log('Done with asybnc work.');
cb();
}, 1000);
}, { tapWay: 'tapAsync' });

// tapPromise 注册钩子函数
_registCompilerHook('emit', (compilation)=>{
// 这里有异步任务
return new Promise((resolve, reject) => {
// 完成一些异步任务
resolve(true);
});
}, { tapWay: 'tapPromise' });

// tap 注册钩子函数
_registCompilerHook('emit', ()=>{
// 这里没有异步任务
console.log('Done with sync work.');
}, { tapWay: 'tap' });
}
}

总结一下就是,我们需要阅读官方的钩子列表文档,然后对指定的钩子使用不同的注册方法就好了。

  • 同步方法
    • tap
  • 异步方法
    • tapAsync
    • tapPromise

结语

写一个插件其实了解清楚内部运行机制之后,真没什么难的,可以延伸以下阅读:

参考链接

看清楚真正的 Webpack 插件
webpack之plugin内部运行机制