这篇文章主要讲解 WebPack 插件机制,也简单描述了 WebPack 编译构建的机制。
WebPack 构建机制
使用 VSCode 调试功能,运行项目打包程序,一步一步走 WebPack(version: 3.10.0)执行代码。
1. WebPack 使用方式
1 2 3 4 5 6 7 |
var webpack = require('webpack') var webpackConfig = require('./webpack.config.js') var compiler = webpack(webpackConfig) compiler.run(function (err, stats) { if (err) console.log(err) }); |
2. WebPack 编译流程
- 将配置文件导出的对象作为 WebPack 的参数,返回一个 Compiler 的实例 compiler。这里会做很多参数处理、还有注册各种插件形成一个插件的事件流。
- 然后调用实例方法 compiler.run(callback),依次触发事件流,执行插件任务。
再来看看 WebPack 函数源码:
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 |
function webpack(options, callback) { // 设置options的默认值,如output.path的默认值为process.cwd(),target的默认值为web new WebpackOptionsDefaulter().process(options); compiler = new Compiler(); // 指定上下文context compiler.context = options.context; compiler.options = options; // 注册nodeEveironmentPlugin插件,触发‘before-run’时执行 new NodeEnvironmentPlugin().apply(compiler); if(options.plugins && Array.isArray(options.plugins)) { // 注册配置文件中的所有插件 compiler.apply.apply(compiler, options.plugins); } // 触发environment和after-environment事件 compiler.applyPlugins("environment"); compiler.applyPlugins("after-environment"); // 处理参数,例如为不同的target注册插件,为externals配置注册ExternalsPlugin,为不同的devtool注册对应的插件,如果cache为true就注册CachePlugin等 compiler.options = new WebpackOptionsApply().process(options, compiler); // 如果有callback就会直接调用compiler.run(callback) if(callback) { if(options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) { const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : (options.watchOptions || {}); return compiler.watch(watchOptions, callback); } compiler.run(callback); } return compiler; } exports = module.exports = webpack; // 导出各种插件,这样我们就可以直接使用插件:new webpack.DefinePlugin() exportPlugins(exports, { "DefinePlugin": () => require("./DefinePlugin"), "NormalModuleReplacementPlugin": () => require("./NormalModuleReplacementPlugin"), ... }); |
这里主要做了几件事情:
- 设置配置参数的默认值。
- new 一个 Compiler 实例。
- 调用实例方法 compiler.apply(plugins) 或 plugin.apply(compiler) 注册插件,比如将 nodeEveironmentPlugin 插件注册在 before-run 阶段。
- 调用 compiler.applyPlugins() 触发插件执行,比如 compiler.applyPlugins(‘before-run’),就会通知注册在 before-run 这个阶段的插件执行。
- 最后导出 WebPack,以及 WebPack 官方提供的一些插件。
确定 Compiler 对象
这里需要先着重介绍下 Compiler 对象,它对我们了解 WebPack 的构建机制和接下来的讲解至关重要,需要理解它到底是什么,有什么作用。
先借用官网的一些介绍:
Compiler 对象代表了完整的 WebPack 环境配置。这个对象在启动 WebPack 时被一次性建立,并在所有可操作的设置中被配置,包括原始配置,loader 和插件。当在 WebPack 环境中应用一个插件时,插件将收到一个编译器对象的引用。可以使用它来访问 WebPack 的主环境。
Compiler 对象在 WebPack 构建过程中代表着整个 WebPack 环境,包含上下文、项目配置信息、执行、监听、统计等等一系列的信息,提供给 loader 和插件使用。
它继承于 Tapable(Tapable 是 webpack 的一个底层库),发布: compiler.applyPlugins('eventName')
、 订阅:compiler.plugin('eventName', callback)
、 注册所有插件: new WebpackPlugin().apply(compiler)
,插件必须提供 apply 方法给 WebPack 完成注册流程,插件在 apply 方法内做一些初始化操作并监听 WebPack 构建过程中的生命周期事件,等待构建时生命周期事件的发布。
这里的核心在于 Compiler 类继承了 Tapable,使用 Tapable 实现了事件发布订阅处理的插件架构。
compiler.apply(plugins) 和 compiler.applyPlugins(‘before-run’) 方法都是继承自 Tapable。
我们来看下 Tapable 有什么来了解更多插件的发布订阅细节 tapable-0.2/lib/Tapable.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 |
function Tapable() { this._plugins = {}; } //发布name消息 Tapable.prototype.applyPlugins = function applyPlugins(name) { if(!this._plugins[name]) return; var args = Array.prototype.slice.call(arguments, 1); var plugins = this._plugins[name]; for(var i = 0; i < plugins.length; i++) { plugins[i].apply(this, args); }; } // fn订阅name消息 Tapable.prototype.plugin = function plugin(name, fn) { if(!this._plugins[name]) { this._plugins[name] = [fn]; } else { this._plugins[name].push(fn); } } //给定一个插件数组,对其中的每一个插件调用插件自身的apply方法注册插件 Tapable.prototype.apply = function apply() { for(var i = 0; i < arguments.length; i++) { arguments[i].apply(this); } }; |
到此 WebPack 已经根据参数配置注册了很多插件、并且注册了一些内部插件,当 WebPack 构建到某个阶段就会发布一个生命周期事件,此时所有订阅了当前发布的生命周期事件的插件会按照注册顺序一个一个执行订阅时提供的回调函数,回调函数的参数是与发布的生命周期事件相对应的参数,比如常用的 Compilation 生命周期事件回调函数参数就包含 Compilation 对象(此对象也是 webpack 构建机制的重要成员),entry-option 生命周期事件回调函数参数是 context(项目上下文路径)和 entry(项目配置的入口对象)。
另外,插件如果需要异步执行编译,则还会提供一个回调函数作为监听回调函数的参数,异步编译完成必须调用回调函数。
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 |
//Compiler.js run(callback) { this.applyPluginsAsync("before-run", this, err => { if(err) return callback(err); this.applyPluginsAsync("run", this, err => { if(err) return callback(err); this.readRecords(err => { if(err) return callback(err); this.compile(onCompiled); }); }); }); }, compile(callback) { const params = this.newCompilationParams(); this.applyPluginsAsync("before-compile", params, err => { if(err) return callback(err); this.applyPlugins("compile", params); const compilation = this.newCompilation(params); this.applyPluginsParallel("make", compilation, err => { if(err) return callback(err); compilation.finish(); compilation.seal(err => { if(err) return callback(err); this.applyPluginsAsync("after-compile", compilation, err => { if(err) return callback(err); return callback(null, compilation); }); }); }); }); } |
compiler.run(callback); 方法就开始了 WebPack 的编译流程,显示异步触发了 before-run,执行完对应的插件回调后再触发 run。最后执行 this.compile(onCompiled)。这是下一个重要步骤
其实核心编译流程就在这里,更细节的流程在 Compilation.js 中
下面的代码可以看到在 make 阶段传入了 Compilation 的实例 compilation:
那我们要知道在 make 这个阶段做了什么,查看初始化时注册插件的那段源码可以知道,在 make 这个阶段根据配置文件中 entry 字段的值注册了对应的插件:SingleEntryPlugin、MultiEntryPlugin、DynamicEntryPlugin。
entry 配置与插件怎么对应的可以查看源码:EntryOptionPlugin.js。
那我们 WebPack 编译的入口从这几个入口插件开始,这里只看下 SingleEntryPlugin。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; } apply(compiler) { ... compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); compilation.addEntry(this.context, dep, this.name, callback); }); } ... } |
前面看过注册插件时会调用插件的 apply 方法,注册某个阶段的执行函数。在这里可以看到注册在 make 阶段的 SingleEntryPlugin 插件,在触发 make 阶段时会做什么。当触发 make 阶段时会执行这个函数:
1 2 3 4 5 |
(compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); compilation.addEntry(this.context, dep, this.name, callback); } |
在这里调用了 compilation.addEntry(this.context, dep, this.name, callback);
,就这样,WebPack 编译从入口模块开始了。
至于调用 compilation.addEntry 后干了什么,大概就是 解析模块、分析模块依赖、对每个模块用相应的 loader 处理。
整个 make
阶段处理完毕后进入 seal
阶段,封装构建结果,最后进入 emit 阶段输出结果。
比较核心的几个文件和方法: webpack.js > Compiler.js(run > compile) > Compilation.js(addEntry)。 其中 Compiler 和 Compilation都继承了 Tapable.
编译过程大致经历的阶段以及在各阶段注册的插件:
阶段 | 插件 |
---|---|
before-run | NodeEnvironmentPlugin |
run | CachePlugin |
before-compile | DllReferencePlugin |
compile | DllReferencePlugin ExternalsPlugin DelegatedPlugin |
this-compilation | … |
compilation | … |
make | SingleEntryPlugin MultiEntryPlugin DynamicEntryPlugin |
build-module | SourceMapDevToolModuleOptionsPlugin |
finish-modules | … |
seal | … |
… | … |
optimize-chunks | CommonsChunkPlugin |
optimize-chunk-assets | UglifyJsPlugin |
… | … |
after-seal | … |
after-compile | … |
emit | LibManifestPlugin |
after-emit | SizeLimitsPlugin |
done | … |
有些插件会在不同的阶段都有注册不同的处理方法,比如 :CachePlugin、ProgressPlugin。
WebPack 编译过程分的很细致,提供很多个阶段,涉及很多的插件,就没有一一列出来了。
插件开发
- 实现一个 apply 方法,以 Compiler 对象 compiler 作为参数,Compiler 类继承 Tapable。
- 在 apply 方法中调用 compiler.plugin(name,fn) 注册插件,其中 fn 是订阅 name 的函数。(Compilation 中的插件同理)
EntryOptionPlugin
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const SingleEntryPlugin = require("./SingleEntryPlugin"); const MultiEntryPlugin = require("./MultiEntryPlugin"); const DynamicEntryPlugin = require("./DynamicEntryPlugin"); function itemToPlugin(context, item, name) { return Array.isArray(item) ? new MultiEntryPlugin(context, item, name) : new SingleEntryPlugin(context, item, name); } module.exports = class EntryOptionPlugin { apply(compiler) { compiler.plugin("entry-option", (context, entry) => { if(typeof entry === "string" || Array.isArray(entry)) { compiler.apply(itemToPlugin(context, entry, "main")); } else if(typeof entry === "object") { Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(context, entry[name], name))); } else if(typeof entry === "function") { compiler.apply(new DynamicEntryPlugin(context, entry)); } return true; }); } }; |
序列图
- 图中每一列顶部名称表示该列中任务点所属的对象
- 图中每一行表示一个阶段
- 图中每个节点表示任务点名称
- 图中每个节点括号表示任务点的参数,参数带有 callback 是异步任务点
- 图中的箭头表示任务点的执行顺序
- 图中虚线表示存在循环流程
本文作者:王洪莹