目录
Webpack设计原理
要分析Webpack的设计原理,我们首先需要了解像Webpack这样子的打包构建构建工具在构建项目时分析,编译项目代码的流程。
构建流程
- 获取配置(webpack.config.js)
- 配置事件驱动
- 开始构建 (run方法)
- 编译模块(_build)
- 借助plugin,loader编译模块(less,图片,ts,es6编译,HtmlWebpackPlugin插件自动引入输出文件等)
- 构建优化(代码分割,Tree Shaking, css文件压缩,js文件压缩)
获取配置
const webpack = require('webpack')
const compiler = webpack({
// 同默认webpack.config.js通过module.exports导出的配置参数
})
// webpack源码文件路径:lib\webpack.js
webpack获取配置后会返回一个compiler。用来开启后续的构建流程
配置事件驱动
compiler中的hooks属性借助tapable包含了大量的事件驱动。用来挂载plugin等,以按照webpack正确的执行时机来触发对应的发布事件
例如:在第一步中,webpack会对config中的配置通过事件驱动的方式进行挂载
compiler.hooks.entryOption.tap("EntryOptionPlugin", () => {}) // 源码位置 lib\EntryOptionPlugin.js 21-24
compiler.hooks.entryOption.call() // 源码位置 lib\WebpackOptionsApply.js 392-396
//如对入口文件处理逻辑的事件挂载
compiler.hooks.make.tapAsync(PLUGIN_NAME, (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {
callback(err);
});
}); // lib\EntryPlugin.js 47-52 (该挂起事件的执行位于开始构建过程中,可以看下一部分)
开始构建
通过配置设置获取的返回值compiler,调用run方法开始进行构建。run方法接收一个回调函数用来返回错误信息等
compiler.run((err, stats) => {}) // webpack源码文件路径:lib\Compiler.js
构建中会调用配置事件驱动过程中挂起的事件
this.hooks.make.callAsync(compilation, err => {
//...其它逻辑
}); // lib\Compiler.js 1322--1353
构建过程函数调用的树形图
EntryPlugin.apply (lib\EntryPlugin.js)
└── addEntry
└── _addEntryItem
└── addModuleTree
└── handleModuleCreation
├── addModule
└── _handleModuleBuildAndDependencies (传入addModule中的callback)
├── buildModule
│ └── buildQueue.add (注入任务队列) (addModuleQueue实例化属性 processor: this._buildModule.bind(this))
│ └── this._buildModule (任务队列调用-构建模块)
└── processModuleDependencies (传入buildModule中的callback)
└── processDependenciesQueue.add (注入任务队列) (processDependenciesQueue实例化属性 processor: this._processModuleDependencies.bind(this))
└── this._processModuleDependencies (任务队列调用-模块依赖)
buildQueue.add与processDependenciesQueue.add的任务队列逻辑进行了抽象设计,将processor这个构建函数作为参数传入,达到对任务队列功能的封装。
// 创建依赖分析任务队列 lib\Compilation.js 1064-1068
this.processDependenciesQueue = new AsyncQueue({
name: "processDependencies",
parallelism: options.parallelism || 100,
processor: this._processModuleDependencies.bind(this) // 队列的执行逻辑
});
// 创建构建模块任务队列 lib\Compilation.js 1083-1087
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this) // 队列的执行逻辑
});
编译模块
每一个module都会根据各自的文件类型,调用不同的Module子类的构建实例,以下先简单以入口js文件作为例子
开始构建中_buildModule的后续调用逻辑
Compilation (lib\Compilation.js)
└── _buildModule
└── module.build (module是各种Module类的子类的实例)
└── _doBuild
└── runLoaders (使用loader-runner从左到右,从上到下执行loader链编译文件)
借助plugin,loader编译模块(less,图片,ts,es6编译,HtmlWebpackPlugin插件自动引入输出文件等)
上步骤提到的loader-runner库提供的runLoaders,是webpack当中用来通过config中配置的loader执行loader链式编译的方法
plugin的挂载和执行,下面简单以html-webpack-plugin进行举例
// plugin挂载的逻辑
if (Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
/** @type {WebpackPluginFunction} */
(plugin).call(compiler, compiler);
} else if (plugin) {
plugin.apply(compiler);
}
}
} // lib\webpack.js 75-84
// html-webpack-plugin 实例后的apply方法
apply () {
//...其它逻辑
compilation.hooks.processAssets.tapAsync({}, (...args) => {
this.generateHTML(...args)
})
} // html-webpack-plugin\index.js 281-307
// html-webpack-plugin 实例挂载执行函数调用
Compiler (lib\Compiler.js)
└── compile
└── compilation.seal
└── this.hooks.processAssets.callAsync
构建优化(代码分割,Tree Shaking, css文件压缩,js文件压缩)
以下简单以代码分割作为例子。在第一步获取配置生成compiler的逻辑中会调用
createCompiler方法。其中的new WebpackOptionsApply().process()方法的调用会挂载代码分割的逻辑等待时机调用
const createCompiler = () => {
// ...其它逻辑
new WebpackOptionsApply().process(options, compiler); // lib\webpack.js 94
}
// process方法中有代码分割相关调用用来发布事件
process() {
// ...其它逻辑
if (options.optimization.splitChunks) {
const SplitChunksPlugin = require("./optimize/SplitChunksPlugin");
new SplitChunksPlugin(options.optimization.splitChunks).apply(compiler); // 这里的apply内部的逻辑与上一步中提到的同理,亦是通过事件的发布订阅在正确的时机调用代码分割逻辑
}
} // lib\WebpackOptionsApply.js 501-504
设计理念
到目前为止,可以看出webpack在构建过程中利用事件的发布订阅,将整个构建过程串联起来。在不同的周期内,订阅不同的事件,借助发布的事件(可以是webpack自己的,也可以是第三方库在配置阶段发布的)对模块进行打包处理,最终达到编译代码的效果。