深入webpack-插件

webpack目前是前端打包工具使用最多的工具, 这不仅仅是因为它有非常好的打包性能, 还应为它具备非常强的扩展性,可以为不同的场景进行功能的扩展.

如果我们去浏览一下webpack的源码, 我们会发现webpack源码里有了非常非常多的插件,在看过一部分webpack源码后你会发现, 实际上webpack的所有功能都是用一个一个插件堆砌出来的.下面我们先讲一讲webpack插件的核心逻辑到底是什么.

打包的主要流程

我们把webpack的构建过程想象成是一个有大量生命周期的一个MVVM前端组件, 从组件初始化到成功的渲染的页面上会触发多个生命周期, 在不同的生命周期来会触发不同的回调函数,而回调函数可以访问到组件的内部状态.

webpack在每一次构建的时候会生成一个compiler对象, 这个对象就类似于一个组件实例, 所有的生命周期都是对这个compiler对象的状态描述.compiler对象会依次触发它上面的每一个生命周期, 只不过在webpack里我们不叫它生命周期, 叫做钩子

webpack里所有的对象几乎都是由类来生成的, 而钩子就是在类的constructor方法中进行初始化. 看过上一篇对tapable介绍的同学应该知道, 这里在大概说一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Compiler {
constructor(context) {
this.hook = {
/** @type {SyncBailHook<[string, Entry], boolean>} 处理webpack config option */
entryOption: new SyncBailHook(["context", "entry"])
/** @type {AsyncSeriesHook<[Compiler]>} 正式开始 */
run: new AsyncSeriesHook(["compiler"]),
/** @type {SyncHook<[CompilationParams]>} 编译 */
compile: new SyncHook(["params"]),
/** @type {SyncHook<[Compilation, CompilationParams]>} 生成 compilation */
compilation: new SyncHook(["compilation", "params"]),
/** @type {AsyncParallelHook<[Compilation], Module>} 生成依赖树 */
make: new AsyncParallelHook(["compilation"]),
/** @type {AsyncSeriesHook<[Compilation]>} 写入文件系统 */
emit: new AsyncSeriesHook(["compilation"]),
/** @type {AsyncSeriesHook<[Stats]>} 完成 */
done: new AsyncSeriesHook(["stats"]),
/** @type {SyncHook<[Error]>} 失败 */
failed: new SyncHook(["error"]),
}
}
}

上面列出的钩子并不是所有的,做了很大的精简, 书写顺序合调用顺序一致,compiler钩子完整列表

如何使用钩子

那么在实例化Compiler后生成了对象compiler,当我们拿到compiler对象后, 我们就可以为compiler对象上面的各种钩子添加对应的回调函数.

1
2
3
4
5
const compiler = new Compiler();
compiler.hooks.compilation.tap('pluginName', (compilation, params) => {
// 通过对给对应的钩子添加回调函数, 在回调函数执行时,就可以拿到对应的对象
// 泽辉这个钩子里, 我们可以拿到compilation对象
})

看上去载一个钩子上添加回调函数是是非容易的. 但是问题来了, 我们怎样才能获取到compiler对象呢? 这里webpack插件就需要登场了

如何写一个插件

官方的plugin示例是这样的:

1
2
3
4
5
6
7
8
class Plugin {
apply(compiler) {
// 获得compiler了!
compiler.hooks.compilation.tap((compilation, params) => {
...
})
}
}

在定义好plugin后, 在webpack的config文件中引入这个plugin, 并在对应位置调用:

1
2
3
4
5
6
const Plugin = require('./Plugin.js');
module.export = {
...
plugins: [new Plugin()],
...
}

那么我们自定义的plugin是如何调用,在什么时候注入到compiler里的呢?我们来看一下初始化compiler的代码, 我们发现在生成compiler的时候:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const createCompiler = options => {
options = new WebpackOptionsDefaulter().process(options);
// 生成一个compiler对象
const compiler = new Compiler(options.context);
// 依次调用插件
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
...
};

从上面的源码我们可以看出来, 插件仅仅可以是一个类, 也可以是一个函数, 使用方法大概是:

1
2
3
4
5
6
7
8
9
10
11
12
// 使用function的方式挂载插件
function plugin(compiler) {
// 获得compiler了!
compiler.hooks.compilation.tap((compilation, params) => {
...
})
}
module.export = {
...
plugins: [plugin],
...
}

还有一种挂载插件的方式是在使用node api来调用webpack()打包的时候, 在拿到返回的compiler对象后,手动调用插件:

1
2
3
4
5
6
const Plugin = require('./Plugin.js');
const compiler = webpack(() => {
...
});
// 手动调用插件
(new Plugin()).apply(compiler);

不过这种方法并不适用与所有的场景, 因为在调用webpack的过程中,会有一些钩子已经运行完毕, 并不推荐.

上面的几个插件声明的示例都使用的是compilation钩子, 回调函数种可以拿到一个compilation对象, 那么它到底是什么呢?

complication

每一个compiler对象会在构建的时候会生成一个compilation对象, 用来保存在整个构建过程中产生的不同数据, 比如解析模块的结果缓存, 依赖模块图, 依赖文件图, chunk图等.compilation对象会在compile之后的大部分钩子回调中获得.

webpack的整个构建过程产生的数据信息都保存在compiler对象合compilation对象上,那么既然插件能够拿到这两个对象说明插件能够对webpack的全部构建流程进行干预.事实上也是如此. webpack的整个核心功能几乎都是由内部上百个私有插件呢组织而成的.所以说webpack插件的扩展能力是非常强大的.

compiler和compilation之间的关系:

compilation对象里有大量的钩子, 具体列表

一个简单的例子

现在我们希望在webpack构建完毕后,获取构建时依赖的所有项目文件.

目前的思路是获取到compilation对象, 上面应该有所有的依赖文件列表.

为了保证compilation对象上的依赖文件列表是已经收集完整的了,所以我们再选择钩子的时候要确保构建过程已经结束,依赖收集已经完毕的钩子

1
2
3
4
5
6
7
8
9
10
11
module.export = class exportDepPlugin {
apply(compiler) {
// emit钩子 说明整个构建已经完成, 准备输出文件,这是依赖已经收集完毕
compiler.hooks.emit.tagAsync('exportDepPlugin', (compilation) => {
const _fileDependencies= Array.from(compilation.fileDependencies);// fileDependencies是一个Set
})
// 排除node_modules里的文件
const dep = _fileDependencies.filter((d) => !d.includes('node_modules'));
console.log(dep);
}
}

小结

webpack的插件功能非常强大, 因为它能够访问到webpack构建过程中的两个核心对象,并做出干预.而且你也可以自己为这几个对象添加新的钩子,好让其他第三方插件来对你的插件进行二次扩展.

webpack的这种插件机制因为使用了类似发布订阅的模式,在我看来实实在在是一个双刃剑, 在提供了强大的扩展性的同时,也把相互之间的代码逻辑隐藏在了深处. 在查看源码的时候阻碍重重, 无法让编辑器进行任何静态分析.

想要了解更多进阶的插件使用方法, 这边建议还是看一些webpack源码, 里面有很多骚气的写法

扩展阅读:

webpack API文档

webpack 项目