手写一个webpack插件(转)

原文地址

webpack本质上是一种事件流的机制,核心是Tapable

webpack中最核心的负责编译的Compiler和负责创建bundlesCompilation都是Tapable的实例。

Tapable是什么?

Tapable暴露了很多Hook(钩子)类,为插件提供挂载。

1
2
3
4
5
6
7
8
9
10
11
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require('tapable');

Tapable用法

  1. 新建钩子new Hook();
  2. 使用tap/tapAsync/tapPromise绑定钩子
  3. call/callAsync执行绑定事件
1
2
3
4
5
6
// 1
const hook1 = new SyncHook(['arg1', 'arg2', 'arg3']);
// 2
hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3));
// 3
hoo1.call(1, 2, 3); // 1, 2, 3

举个例子

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
const { SyncHook, AsyncParallelHook } = require('tapable');

class Car {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newSpeed']),
break: new SyncHook(),
caculateRoutes: new AsyncParallelHook(['source', 'target', 'routesList'])
};
}
}

const myCar = new Car();

myCar.hooks.break.tap('WarningLampPlugin', () => console.log('WarningLampPlugin'));

myCar.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log(`Accelerate to ${newSpeed}`));

myCar.hooks.calculateRoutes.tapPromise('calculateRoutes tapPromise', (source, target, routesList, callback) => {
return new Promise((resolve, rejct) => {
setTimeout(() => {
console.log(`tapPromise to ${source} ${target} ${routesList}`);
resolve();
}, 1000)
})
});

myCar.hooks.break.call();
myCar.hooks.accelerate.call('hello');

console.time('cost');

myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => {
console.timeEnd('cost');
}).catch(err => {
console.error(err);
console.timeEnd('cost');
})

进阶一下

上面学习了Tapable的用法,但它和webpack/plugin有什么关系呢?

我们将刚才但代码稍作改动,拆成两个文件:Compiler.jsMyPlugin.js.

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
const {
SyncHook,
AsyncParallelHook
} = require('tapable');

class Compiler {
constructor(options) {
this.hooks = {
accelerate: new SyncHook(['newSpeed']),
break: new SyncHook(),
calculateRoutes: new AsyncParallelHook(['source', 'target', 'routesList'])
};

let plugins = options.plugins;
if (plugins && plugins.length > 0) {
plugins.forEach(plugin => plugin.apply(this));
}
}

run() {
console.time('cost');
this.accelerate('hello');
this.break();
this.caculateRoutes('i', 'love', 'tapable');
}

accelerate(param) {
this.hooks.accelerate.call(param);
}

break() {
this.hooks.break.call();
}

calculateRoutes() {
const args = Array.from(arguments);
this.hooks.calculateRoutes.callAsync(...args, err=> {
console.time('cost');
if (err) console.error(err);
});
}
}

module.exports = Compiler;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Compiler = require('./Compiler');

class MyPlugin {
constructor() {}

apply(compiler) {
compiler.hooks.break.tap('WarningLampPlugin', () => console.log('WarningLampPlugin'));
compiler.hooks.accelerate.tap('LoggerPlugin', newSpeed => console.log(`Accelerating to ${newSpeed}`));
compiler.hooks.calculateRoutes.tapAsync('calculateRoutes tapAsync', (source, target, routesList, callback) => {
setTimeout(() => {
console.log(`tapAsync to ${source} ${target} ${routesList}`);
callback();
}, 2000);
});
}
}

const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
let compiler = new Compiler(options);
compiler.run();

Plugin基础

webpack通过plugin机制让其更加灵活,以适应各种应用场景。在webpack运行的生命周期中会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的API改变输出结果。

一个最基础的plugin的代码是这样的:

1
2
3
4
5
6
7
8
9
class PluginDemo {
constructor() {}

apply(compiler) {
compiler.hooks.compilation.tap('PluginDemo', compilation => {});
}
}

module.exports = PluginDemo;

在使用这个plugin时,相关代码如下:

1
2
3
4
5
6
7
const PluginDemo = require('./PluginDemo');

module.exports = {
plugins: [
new PluginDemo(options);
]
}

CompilerCompilation

在开发plugin时最常用的两个对象就是CompilerCompilation,它们是Plugin和webpack之间的桥梁。

Compiler

Compiler对象包含了webpack环境所有的配置信息,包含optionsloadersplugins等,这个对象在webpack启动的时候被实例化,它在全局是唯一的。

Compilation

Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当webpack以开发模式运行时,每当检测到一个文件变化,一次新的Compilation将被创建。Compilation对象也提供了很多事件回调供插件做扩展。通过Compilation对象也可以读取到Compiler对象。

它们的区别在于:Compiler代表了整个webpack从启动到关闭的生命周期,而Compilation只是代表了一次新的编译。

常用API

Writing a Plugin