解读Webpack的plugin机制
loader和plugin可以说是Webpack的两大支柱功能了。相较于loader专注于模块类型的转化,plugin提供了更为广阔的功能,原因就在于其能直接通过编译生命周期钩子影响webpack编译流程,实现强大的自定义功能拓展。这篇文章就来分析下plugin的运作原理。
webpack工作流程简述
在进入plugin之前,我们必须先了解webpack的工作流程,因为插件的本质就是在工作流程的生命周期钩子中进行合适的处理。
配置项和cli参数解析
启动webpack的第一步就是在命令行输入webpack命令并传入参数,操作系统会调用bin文件目录中的webpack.js
,在该文件中会解析用户配置的webpack.config.js
和通过命令行传递过来的参数并合成为options对象,这个过程是使用yargs
这个模块处理的。该options对象就是决定webpack如何编译和构建的重要配置了。接下来通过如下核心代码进入编译流程。
// ...
var compiler = webpack(options);
function compilerCallback(err, stats) {...}
if (options.watch) {
...
} else {
compiler.run(compilerCallback);
}
编译器初始化
通过上一步获得的compiler对象就是webpack的编译器对象了,可以看到该对象在启动时一次性的初始化并包含了自定义配置选项。接下来进入webpack函数先看看编译器对象是如何构造的。
function webpack(options, callback) {
// ...
compiler = new Compiler();
compiler.context = options.context;
compiler.options = options;
new NodeEnvironmentPlugin().apply(compiler);
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler, options.plugins);
}
compiler.applyPlugins("environment");
compiler.applyPlugins("after-environment");
compiler.options = new WebpackOptionsApply().process(options, compiler);
// ...
return compiler;
}
构造编译器对象的过程中主要做了两件事:
- 实例化Compiler对象,该对象继承自Tapable插件框架,该框架抽象了一套插件机制并提供了一系列事件钩子。Webpack的插件机制就是基于该框架。可以看到在构造编译器对象的过程了已经调用了插件相关的环境钩子方法。
- 通过WebpackoptionsApply实例再编译options,主要是注册配置选项对应需要的内部插件。比如针对entry选项会注册一个处理entry的插件
EntryOptionPlugin
。
获取了compiler对象后,就通过run方法正式进入编译流程了。
构建模块并收集依赖
编译的第一阶段是创建compilation对象。该对象可以理解为编译资源生成对象,同样继承自Tapable。每当一次新的编译开始时都会更新该对象并生成新的编译资源。其包含了当前模块资源,编译生成资源,文件变化,模块依赖等等打包相关的状态信息以及各种关键状态钩子供我们做自定义处理。
然后进入make阶段,该阶段会创建entry相关的内部插件实例,比如单入口相对应的SingleEntryPlugin
,然后调用compliation对象的addEntry方法。该方法主要处理入口文件的模块依赖并构建相关模块。分为如下几步:
- 注册不同类型模块的对应模块工厂并保存到compliation对象上。
- 构建入口模块,调用各个loader处理模块依赖,将我们各种各样的模块转化为标准的JS模块
- 通过
acorn
这个库解析JS模块为抽象语法树AST - 遍历AST收集依赖模块到依赖数组中。
- 递归构建依赖的模块,重复以上步骤。
封装
在模块构建完成后会触发seal事件调用compliation对象上的seal方法对构建结果进行封装,封装过程中会遍历chunks并传给Template类的内部渲染插件,该插件会根据chunk的依赖关系渲染出最终代码,最后调用Compiler的emitAssets
方法按照options中的output配置参数将最终文件输出到对应path中,至此整个流程结束。
Tapable
从以上Webpack的工作流程中可以看出,Webpack的整体就是一个以插件为基础的架构,比如入口解析,最终文件渲染等等都是基于内部实现的插件实现的。
可以将Webpack的工作流程比作一条生产线,在生产线上有相当数量的事件节点,在对应的节点钩入插件对生产线上的资源处理就是Webpack的功能实现了。我们也应该注意到,与这条生产线最密切的两个对象就是Complier和Compilation了,关键事件的触发都离不开这两者,而这两者都继承自Tapable——抽象插件事件机制的核心。
Tapable的主要成员方法如下:
- plugin(name:string, handler: function): 该方法给Tapable实例注册一个自定义插件,等同于观察者模式中的事件监听,也是我们编写Webpack插件需要频繁用到的方法。
- apply(…pluginInstances:
[]):该方法用于传入已有插件对象并调用插件对象的apply方法注册插件。 - applyPlugins(name: string, args: any…):调用对应事件下的所有插件,类似于观察者模式中的事件触发。
Webpack关键事件
Webpack中插件工作的核心流程如下:
- 初始化Complier对象时会根据options传入插件调用相关apply方法进行注册。
- Webpack工作流程期间会调用tapable实例的applyPlugins方法在合适的生命周期触发相关关键事件
- 插件内部通过plugin方法定义相关逻辑并触发
complier钩子
- entry-option: 初始化option
- run: 启动webpack编译流程
- before-compile: 真正开始编译前
- complie: 开始编译,即将创建compilation对象
- compilation: 完成compilation对象,可以在插件对应回调中获取该参数
- make: 从入口模块开始递归构建
- build-module: 开始构建这个module并使用对应loader加载
- normal-module-loader: 解析module生成AST
- program: 遍历AST收集依赖
- after-compile: 内容编译完成,准备封装
- seal: 封装构建结果,优化chunk,比如合并,加hash等
- emit: 将打包文件输出到磁盘之前
- after-emit: 打包文件输出到磁盘后
- done: 完成所有编译流程
- failed: 编译失败
compilation钩子
- normal-module-loader: 普通的模块加载
- seal: 编译的封装阶段
- optimize-modules: 优化编译模块
- optimize-chunks: 优化编译chunks
- optimize-chunk-assets: 优化chunk所生成的相关资源
- before-hash: 编译生成哈希前
- after-hash: 编译生成哈希后
- build-module: 模块构建前
- succeed-module: 模块构建后
-- EOF --