plugin
loader 说完,我们说说 plugin。其实 loader 只是 webpack 的基本工具,在 webpack 中真正强大的是 plugin。
简介
和 loader 相比 plugin 的功能更加强大并且贯穿 webpack。所有 loader 做不了的事情几乎都可以用 plugin 来做。可以说在 webpack 中 Everything is a plugin,webpack 中 80% 的代码都是 plugin。
Plugins are a key piece of the webpack ecosystem and provide the community with a powerful way to tap into webpack's compilation process. A plugin is able to hook into key events that are fired throughout each compilation. Every step of the way, the plugin will have full access to the
compilerand, when applicable, the currentcompilation.
While loaders are used to transform certain types of modules, plugins can be leveraged to perform a wider range of tasks like bundle optimization, asset management and injection of environment variables.
Plugins are the backbone of webpack. webpack itself is built on the same plugin system that you use in your webpack configuration!
They also serve the purpose of doing anything else that a loader cannot do.
Plugins grant unlimited opportunity to perform customizations within the webpack build system. This allows you to create custom asset types, perform unique build modifications, or even enhance the webpack runtime while using middleware. The following are some features of webpack that become useful while writing plugins.
It's interesting to know that 80% of the webpack is made up of its own plugin system. Webpack itself is an event-driven architecture. Plugins are a key piece of the webpack ecosystem and provide the community with a powerful way to tap into webpack’s compilation process. A plugin is able to
hookinto key events that are fired throughout each compilation.
-- webpack-behind-the-scenes
总之拥有了 plugin 你就拥有了整个 webpack。
plugin 的写法
我们来尝试写一个 webpack plugin。
A webpack plugin is a JavaScript object that has an
applymethod. Thisapplymethod is called by the webpack compiler, giving access to the entire compilation lifecycle.
上面这段话的内容很好的概括了 plugin 的写法和功能。
plugin 就是一个拥有 apply 方法的对象,apply 的参数是 compiler,通过 compiler 的各种 hooks 挂载各种方法,这个方法会拥有 compilation 对象,然后 compilation 对象上也有各种 hooks,可以挂载各种方法。进而获取整个 webpack 和打包的所有相关信息。
看一个最简单的 plugin:
const pluginName = 'ConsoleLogOnBuildWebpackPlugin';
class ConsoleLogOnBuildWebpackPlugin {
apply(compiler) {
compiler.hooks.run.tap(pluginName, compilation => {
console.log('The webpack build process is starting!!!');
});
}
}
module.exports = ConsoleLogOnBuildWebpackPlugin;
这里面有几个概念:compiler, hooks, run, tap, compilation,我们一个一个来说,先看 compiler 和 compilation。
compiler 相当于 webpack 的实例(instance),也是整个系统最顶层的或者说最核心的内容,它来控制着整个 webpack 的编译过程。
It is the top level, it's central dispatch.
-- compiler-compilation
Compiler 负责文件监听和启动编译。Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。
--深入浅出 webpack
If you don’t pass the
webpackrunner function a callback, it will return a webpackCompilerinstance. This instance can be used to manually trigger the webpack runner or have it build and watch for changes, much like the CLI. TheCompilerinstance provides the following methods:
.run(callback)
.watch(watchOptions, handler)Typically, only one master
Compilerinstance is created, although child compilers can be created in order to delegate specific tasks. TheCompileris ultimately just a function which performs bare minimum functionality to keep a lifecycle running. It delegates all the loading, bundling, and writing work to registered plugins.
Thehooksproperty on aCompilerinstance is used to register a plugin to any hook event in theCompiler's lifecycle. TheWebpackOptionsDefaulterandWebpackOptionsApplyutilities are used by webpack to configure itsCompilerinstance with all the built-in plugins.
Therunmethod is then used to kickstart all compilation work. Upon completion, the givencallbackfunction is executed. The final logging of stats and errors should be done in thiscallbackfunction.
-- webpack.js.org/api/node/#c…
我们来看一下 plugin 在 webpack 中被调用的位置。
// node_modules/webpack/lib/webpack.js
46 if (options.plugins && Array.isArray(options.plugins)) {
47 for (const plugin of options.plugins) {
48 if (typeof plugin === "function") {
49 plugin.call(compiler, compiler);
50 } else {
51 plugin.apply(compiler);
52 }
53 }
54 }
基本上是在初始化 config 之后就被调用,因为 plugin 是都要注册到整个流程中的,所以在还没有开始编译的时候,就需要首先就是把这些流程注册上,这样在编译的过程中才能应用每个 plugin。
既然注册了,那就有触发的时机,这个触发的时机就是 compiler.hooks 的所有事件,hooks 就是整个编译过程中暴露出来的所有钩子。run 其实就是其中一个钩子,也就是开始编译的时候。而 emit 是最终生成文件的时候,所有的钩子都可以在官网中找到说明,包括钩子的回调函数中传递的参数。那实际上官网的文档只是对源码的翻译,我们看完官网文档再去看源码就会发现所有的内容在源码中都已经有展示了。
// node_modules/webpack/lib/Compiler.js
45 this.hooks = {
46 /** @type {SyncBailHook<Compilation>} */
47 shouldEmit: new SyncBailHook(["compilation"]),
48 /** @type {AsyncSeriesHook<Stats>} */
49 done: new AsyncSeriesHook(["stats"]),
50 /** @type {AsyncSeriesHook<>} */
51 additionalPass: new AsyncSeriesHook([]),
52 /** @type {AsyncSeriesHook<Compiler>} */
53 beforeRun: new AsyncSeriesHook(["compiler"]),
54 /** @type {AsyncSeriesHook<Compiler>} */
55 run: new AsyncSeriesHook(["compiler"]),
56 /** @type {AsyncSeriesHook<Compilation>} */
57 emit: new AsyncSeriesHook(["compilation"]),
58 /** @type {AsyncSeriesHook<string, Buffer>} */
59 assetEmitted: new AsyncSeriesHook(["file", "content"]),
60 /** @type {AsyncSeriesHook<Compilation>} */
61 afterEmit: new AsyncSeriesHook(["compilation"]),
62
63 /** @type {SyncHook<Compilation, CompilationParams>} */
64 thisCompilation: new SyncHook(["compilation", "params"]),
65 /** @type {SyncHook<Compilation, CompilationParams>} */
66 compilation: new SyncHook(["compilation", "params"]),
67 /** @type {SyncHook<NormalModuleFactory>} */
68 normalModuleFactory: new SyncHook(["normalModuleFactory"]),
69 /** @type {SyncHook<ContextModuleFactory>} */
70 contextModuleFactory: new SyncHook(["contextModulefactory"]),
71
72 /** @type {AsyncSeriesHook<CompilationParams>} */
73 beforeCompile: new AsyncSeriesHook(["params"]),
74 /** @type {SyncHook<CompilationParams>} */
75 compile: new SyncHook(["params"]),
76 /** @type {AsyncParallelHook<Compilation>} */
77 make: new AsyncParallelHook(["compilation"]),
78 /** @type {AsyncSeriesHook<Compilation>} */
79 afterCompile: new AsyncSeriesHook(["compilation"]),
80
81 /** @type {AsyncSeriesHook<Compiler>} */
82 watchRun: new AsyncSeriesHook(["compiler"]),
83 /** @type {SyncHook<Error>} */
84 failed: new SyncHook(["error"]),
85 /** @type {SyncHook<string, string>} */
86 invalid: new SyncHook(["filename", "changeTime"]),
87 /** @type {SyncHook} */
88 watchClose: new SyncHook([]),
89
90 /** @type {SyncBailHook<string, string, any[]>} */
91 infrastructureLog: new SyncBailHook(["origin", "type", "args"]),
92
93 // TODO the following hooks are weirdly located here
94 // TODO move them for webpack 5
95 /** @type {SyncHook} */
96 environment: new SyncHook([]),
97 /** @type {SyncHook} */
98 afterEnvironment: new SyncHook([]),
99 /** @type {SyncHook<Compiler>} */
100 afterPlugins: new SyncHook(["compiler"]),
101 /** @type {SyncHook<Compiler>} */
102 afterResolvers: new SyncHook(["compiler"]),
103 /** @type {SyncBailHook<string, Entry>} */
104 entryOption: new SyncBailHook(["context", "entry"])
105 };
我们来看几个常见的 hooks 以及他们的触发时机。
**run
**AsyncSeriesHook
Hook into the compiler before it begins readingrecords.
Callback Parameters:compiler
**compilation
**SyncHook
Runs a plugin after a compilation has been created.
Callback Parameters:compilation,compilationParams
**shouldEmit
**SyncBailHook
Called before emitting assets. Should return a boolean telling whether to emit.
Callback Parameters:compilation
emi****t
AsyncSeriesHook
Executed right before emitting assets to output dir.
Callback Parameters:compilation
虽然看到了这些 hooks,但是我们可以来看看这些 hooks 真正执行的时机。每个 hooks 执行的时机都是这个 hook 调用 call 的时候。例如我们来看下 run 真正执行的时机,以及 run 之前的 beforeRun 和 compile 等。
// node_modules/webpack/lib/Compiler.js
// line 264 - 310 的 onCompiled 代码也能看出很多 hooks 的执行时机和次序
312 this.hooks.beforeRun.callAsync(this, err => { // beforeRun 调用的时机
313 if (err) return finalCallback(err);
314
315 this.hooks.run.callAsync(this, err => { // beforeRun 运行结束之后的回调里才执行 run
316 if (err) return finalCallback(err);
317
318 this.readRecords(err => {
319 if (err) return finalCallback(err);
320
321 this.compile(onCompiled); //
322 });
323 });
324 });
...
660 compile(callback) {
661 const params = this.newCompilationParams();
662 this.hooks.beforeCompile.callAsync(params, err => { // beforeCompile
663 if (err) return callback(err);
664
665 this.hooks.compile.call(params); // compile
666
667 const compilation = this.newCompilation(params);
668
669 this.hooks.make.callAsync(compilation, err => { // make
670 if (err) return callback(err);
671
672 compilation.finish(err => {
673 if (err) return callback(err);
674
675 compilation.seal(err => {
676 if (err) return callback(err);
677
678 this.hooks.afterCompile.callAsync(compilation, err => { // afterCompile
679 if (err) return callback(err);
680
681 return callback(null, compilation);
682 });
683 });
684 });
685 });
686 });
687 }
hooks 越多,表示整个流程控制的精细度越细,例如包括 emit 事件,其前后就有 shouldEmit, emit, afterEmit, assetEmitted 四个事件。可见对于 emit 而言作者给第三方的开发者就提供了很多的可以介入 webpack 编译过程的机会。这个精细度是很高的,这也就是为什么说 plugin 才是真正构成 webpack 核心的东西。webpack 的 compiler 和 compilation 全部的 hooks 加起来总共有 100 多个,也就是说在整个编译的过程中你可以有 100 多次参与编译各个环节的机会。
下面这篇文章也是官方的文档
compiler 对象上的 hooks 看这个文档
这个官方文档基本展示了所有的 compiler 上的 hooks
我们接着来看 compilation。
当 Webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。
--深入浅出 webpack
关于 compilation 对象上挂载的方法和属性,官方也给出了文档。
compilation 对象上的 hooks 看这个文档,源码如下:
250 this.hooks = {
251 /** @type {SyncHook<Module>} */
252 buildModule: new SyncHook(["module"]),
253 /** @type {SyncHook<Module>} */
254 rebuildModule: new SyncHook(["module"]),
... ...
430 /** @type {SyncBailHook<Chunk[]>} */
431 optimizeExtractedChunks: new SyncBailHook(["chunks"]),
432 /** @type {SyncBailHook<Chunk[]>} */
433 optimizeExtractedChunksAdvanced: new SyncBailHook(["chunks"]),
434 /** @type {SyncHook<Chunk[]>} */
435 afterOptimizeExtractedChunks: new SyncHook(["chunks"])
436 };
说完 compiler / compilation / hooks / run,我们来看看 tap,tap 就是类似 nodejs 中 EventEmitter 的 on 方法。而触发 tap 的就是 call (async)方法,类似 EventEmitter 中的 emit 方法。
那么 tap 是什么?怎么来的呢?就不得不说 Tapable,我们先讲一下 tapbale。
在 writing-a-plugin 的文档中其实有对 tapable 的详细说明和解释。但是还是要把 tapable 抽出来单独来写。详细写一下 tapable 的用法和意义。
class MyPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => {
// Explore each chunk (build output):
compilation.chunks.forEach(chunk => {
// Explore each module within the chunk (built inputs):
chunk.getModules().forEach(module => {
// Explore each source file path that was included into the module:
module.buildInfo && module.buildInfo.fileDependencies && module.buildInfo.fileDependencies.forEach(filepath => {
// we've learned a lot about the source structure now...
});
});
// Explore each asset filename generated by the chunk:
chunk.files.forEach(filename => {
// Get the asset source for each file generated by the chunk:
var source = compilation.assets[filename].source();
});
});
callback();
});
}
}
module.exports = MyPlugin;
除了 compiler 和 compilation 之外,其实 webpack 的 parser 也是对 tapable 的扩展,也有 hooks,具体参见文档或者源码(node_modules/webpack/lib/Parser.js)。
分析几个官方的 plugin,尝试自己写一个 plugin。
官方的 plugin 之一:html-webpack-plugin
这里就是生成 html 的代码
279 // Add the evaluated html code to the webpack assets
280 compilation.assets[finalOutputName] = {
281 source: () => html,
282 size: () => html.length
283 };
webpack 默认的 ProgressPlugin,里面看到了 compiler.hooks.emit.intercept,这个 intercept 也是 tapable 的一种特殊的方法。
node_modules/webpack/lib/optimize/RemoveEmptyChunksPlugin.js 这个简单
Tapable
This small library is a core utility in webpack but can also be used elsewhere to provide a similar plugin interface. Many objects in webpack extend the
Tapableclass. The class exposestap,tapAsync, andtapPromisemethods which plugins can use to inject custom build steps that will be fired throughout a compilation.
也就是说 tapable 并不是为 webpack 而生的,而是为 plugin 而生的。tapable 更像是一种流程处理机制,而并不是和编译相关。了解这个关键其实很重要,我最初想看 tapable 一直是想从编译过程的角度来学习 tapable ,导致根本就看不下去,因为所有的代码都和编译完全无关。你的 mental modal 根本就没有和 tapable 关联起来,当然看不下去了。tapable核心就是暴露出来的 hooks,通过不同类型的 hooks 来触发流程中不同类型的事件。
tapable 的核心:hooks
const {
SyncHook,
SyncBailHook,
SyncWaterfallHook,
SyncLoopHook,
AsyncParallelHook,
AsyncParallelBailHook,
AsyncSeriesHook,
AsyncSeriesBailHook,
AsyncSeriesWaterfallHook
} = require("tapable");
以上就是 tapable 中所有的 hooks,简单做个分类,从同步异步的角度区分有 syncHook 和 asyncHook,从流处理的机制角度区分有 hook, bailHook, waterfallHook, loopHook, parallelHook, seriesHook。没中类型的 hook 的处理逻辑都不同,但是基本覆盖了所有的流程处理机制。
webpack 的文档中有关于不同 Hooks 的描述。
-
syncHook
正常的同步串行流程处理机制,按照 plugin 的接入次序依次去处理同一个内容。 -
bailHook
In these type of hooks, each of the plugin callbacks will be invoked one after the other with the specificargs. If any value is returned except undefined by any plugin, then that value is returned by hook and no further plugin callback is invoked. Many useful events likeoptimizeChunks,optimizeChunkModulesare SyncBailHooks.
同步串行熔断机制,按照 plugin 的次序依次去处理同一个内容,当任何一个 plugin 有返回值的时候,就立即返回,后面的 plugin 不会再执行了。 -
waterfallHook
Here each of the plugins are called one after the other with the arguments from the return value of the previous plugin. The plugin must take the order of its execution into account. It must accept arguments from the previous plugin that was executed. The value for the first plugin isinit. Hence at least 1 param must be supplied for waterfall hooks. This pattern is used in the Tapable instances which are related to the webpack templates likeModuleTemplate,ChunkTemplateetc.
同步流水作业机制,按照 plugin 的次序依次处理内容,上一个 plugin 的返回值是下一个 plugin 的输入值,因此要考虑 plugin 的接入次序。 -
asyncSeriesHook
The plugin handler functions are called with all arguments and a callback function with the signature(err?: Error) -> void. The handler functions are called in order of registration.callbackis called after all the handlers are called. This is also a commonly used pattern for events likeemit,run.
异步串行机制,按照 plugin 的次序依次处理,所有的 plugin 接收的是同样的参数。 -
asyncWaterfallHook
The plugin handler functions are called with the current value and a callback function with the signature(err: Error, nextValue: any) -> void.When callednextValueis the current value for the next handler. The current value for the first handler isinit. After all handlers are applied, callback is called with the last value. If any handler passes a value forerr, the callback is called with this error and no more handlers are called. This plugin pattern is expected for events likebefore-resolveandafter-resolve.
异步流水作业机制,上一个 plugin 的返回值是下一个 Plugin 的输入值,依次执行。
下面这篇文章非常好地解释了所有的 hook 类型原理和流程。
www.programmersought.com/article/145…
画几张图来表达一下所有的 hook 类型。
关于 tapable 的各个 hook 的说明:webpack.js.org/contribute/…
看完 tapable 的这些 hooks 之后我不禁发出了这样的感慨:这 tm 和编译打包有什么关系?没错,tapable 和编译没有任何关系。tapable 在 webpack 中的出现完全是为了 plugin 服务的,就是为了在打包的流程中方便添加各种 plugin,方便各种 plugin 融入到整个打包的流程中。
因此 tapable 是 webpack 的 backbone,但是不是核心。只能算的上是架构的核心,并不是打包的核心,打包的核心还是分析各个模块的依赖关系,形成 dependency graph(模块之间的关系网),进而把所有文件打包成一个或者多个 chunk / bundle,然后执行的时候各个模块之间能正常互相调用,这个才是 webpack 的核心工作。
因此,我们再从两个角度来看看 webpack。一个是从 tapable 的角度,我们回头看看我们写的 plugin 到底是如何工作的,如何融入 webpack 的。第二个是完全从打包编译的角度考虑,既然 tapable 和打包编译没有关系,我们接下来就把 webpack 和 tapable 剥离开,写一个单纯的打包功能的 webpack,来看下 webpack 的工作原理到底是怎么样的。
其实看完,tapable 是不是和数组的几个高级函数很像呢?比如 some, every, forEach, reduce, map 等等。其实说白了 tapable 就是对 plugin list 的整合,因为 plugin list 是个数组,那么自然 tapable 做的工作就是对 plugin list 的各个 plugin 都执行,过程自然就是和 some / every / forEach / reduce 等很像,只不过是会加入异步的操作。
webpack 原理
webpack.js.org/concepts/ 里面有三个关于 webpack 原理的链接
手写 webpack
1. 创建 simple-webpack 的配置文件:simplepack.config.js
2. 创建 src 目录下的内容:src/index.js, src/greeting.js, src/name.js
3. 创建 lib 目录下的内容:
lib/index.js: 引入 Compiler 执行 run 方法。
lib/Parser.js: 解析语法树获取依赖,把 es6 的语法转成 es5 的语法
lib/Compiler.js: constructor 接受 options 参数,把 entry 和 output 挂载到实例上。有三个方法:入口的 run,构建模块的 buildModule,输出文件的 emitFiles。
4. packages: @babel/core, @babel/parser, @babel/preset-env, @babel/traverse
见 github 仓库中的 minipack