webpack本质上可以将其理解为是一种基于事件流的编程范例,一系列的插件运行。
1. webpack启动过程分析
1. 运行webpack
有两种方式:
- 通过npm scripts运行webpack
// package.json
{
...
"scripts": {
"build": "webpack --config webpack.pro.config.js",
"dev": "webpack-dev-server --config webpack.dev.config.js --open"
},
...
}
- 通过webpack直接运行
webpack entry.js
2. 查找webpack入口文件
在运行以上命令后,npm 会让命令行工具进入 node_modules.bin 目录查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在就执行,不存在就抛出错误。
实际的文件入口是:node_modules/webpack/bin/webpack.js
webpack最终找到webpack-cli这个npm包,并且执行CLI
2. webpack-cli做了什么
- 引入yargs,对命令进行定制
- 分析命令行参数,对各个参数进行转换,组成编译配置项
- 引用webpack,根据配置项进行编译和构建 webpack-cli对配置文件和命令行参数进行转换,最终生成配置选项参数options,然后根据配置参数实例化webpack对象,执行构建流程。
// Compiler 模块是 webpack 的支柱引擎,它通过 CLI 或 Node API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件会首先在 Compiler 上注册。
compiler = webpack(options);
if (argv.progress) {
const ProgressPlugin = require("webpack").ProgressPlugin;
new ProgressPlugin({
profile: argv.profile
}).apply(compiler);
}
3. Tapable插件架构与Hooks设计
compiler和compilation两个核心对象,都是继承自Tapable类。Tapable是一个类似于nodejs的EventEmitter库,主要是控制钩子函数的发布与订阅,控制着webpack的插件系统。
Tapable钩子
Tapable库提供了很多Hook类,为插件提供挂载的钩子:
- SyncHook:同步钩子
- SyncBailHook:同步熔断钩子
- SyncWaterfallHook:同步流水钩子
- SyncLoopHook:同步循环钩子
- AsyncParallelHook:异步并发钩子
- AsyncParallelBailHook:异步并发熔断钩子
- AsyncSeriesHook:异步串行钩子
- AsyncSeriesBailHook:异步串行熔断钩子
- AsyncSeriesWaterfallHook:异步串行流水钩子
Tapable钩子的绑定与执行
- Async* 钩子:
- 绑定:tapAsync/tapPromise/tap
- 执行:callAsync/promise
- Sync* 钩子
- 绑定:tap
- 执行:call
Tapable Hook的使用
const hook1 = new SyncHook(['arg1', 'arg2', 'arg3']);
// 绑定事件到webpack事件流,第一个参数是事件名, 第二个参数是回调函数
hook1.tap('hook1', (arg1, arg2, arg3) => { console.log(arg1, arg2, arg3); }); // 1 2 3
// 执行绑定的事件
hook1.call(1, 2, 3);
Compiler与插件的执行机制
webpack的插件有以下特点:
- 独立的 JS 模块,暴露相应的函数
- 函数原型上的 apply 方法会注入 compiler 对象
- compiler 对象上挂载了相应的 webpack 事件钩子
- 事件钩子的回调函数里能拿到编译后的 compilation 对象,如果是异步钩子还能拿到相应的 callback
compiler和compilation的区别
Compiler 对象包含了当前运行Webpack的所有配置信息,包含entry、output、loaders、plugins等配置,这个对象在webpack启动时候被实例化,它是全局唯一的。我们可以把它理解为webpack的实列。Plugin可以通过该对象获取到Webpack的配置信息进行处理。
compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。一个编译对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。
Compiler对象 与 Compilation 对象 的区别是:Compiler代表了是整个webpack从启动到关闭的生命周期。Compilation 对象只代表了一次新的编译。
类似于以下模拟过程:
// Compiler.js
const { SyncHook, AsyncSeriesHook } = require('tapable');
class Compiler {
constructor() {
this.hooks = {
accelerate: new SyncHook(['newspeed']),
brake: new SyncHook(),
caculateRoutes: new AsyncSeriesHook(['source', 'target', 'routesList'])
}
}
// 入口
run () {
this.brake();
this.accelerate(11);
this.caculateRoutes('async', 'hook', 'demo');
}
brake() {
this.hooks.brake.call();
}
accelerate(speed) {
this.hooks.accelerate.call(speed);
}
caculateRoutes() {
this.hooks.caculateRoutes.promise(...arguments).then(() => {
console.log('1');
}).catch(err => {
console.log(err);
})
}
}
module.exports = Compiler;
// myPlugin.js
class MyPlugin {
constructor() {}
// 插件的apply方法会注入compiler对象
apply(compiler) {
// compiler对象上挂载了相应的webpack事件钩子, 事件钩子的回调函数里,能拿到编译后的compilation对象
// 绑定同步钩子
compiler.hooks.brake.tap('WarningLampPlugin', () => { console.log('warning Lamp'); });
// 绑定同步钩子并传参
compiler.hooks.accelerate.tap('LoggerLogin', (newspeed) => { console.log(newspeed) });
// 绑定一个异步promise钩子
compiler.hooks.caculateRoutes.tapPromise('CacularRoutes', (source, target, routesList, callback) => {
console.log('source', source);
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('tapPromise');
resolve();
}, 1000)
})
});
}
}
module.exports = MyPlugin;
// index.js
const Compiler = require('./Compiler');
const MyPlugin = require('./myPlugin');
const myPlugin = new MyPlugin();
const options = {
plugins: [myPlugin]
}
const compiler = new Compiler();
for (let plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
// 调用run
compiler.run();
Compiler 对象继承于 Tapable(Tapable 是 webpack 的一个底层库),发布: compiler.applyPlugins('eventName')、 订阅:compiler.plugin('eventName', callback)、 注册所有插件: new WebpackPlugin().apply(compiler) ,插件必须提供 apply 方法给 WebPack 完成注册流程,插件在 apply 方法内做一些初始化操作并监听 WebPack 构建过程中的生命周期事件,等待构建时生命周期事件的发布。
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);
}
};
4. webpack的构建流程
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 对象在 WebPack 构建过程中代表着整个 WebPack 环境,包含上下文、项目配置信息、执行、监听、统计等等一系列的信息,提供给 loader 和插件使用。
compiler.run(callback); 方法就开始了 WebPack 的编译流程,显示异步触发了 before-run,执行完对应的插件回调后再触发 run。最后执行 this.compile(onCompiled)。
// 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);
});
});
});
});
}
在 make 这个阶段根据配置文件中 entry 字段的值注册了对应的插件:SingleEntryPlugin、MultiEntryPlugin、DynamicEntryPlugin。触发 make 阶段时,调用了 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.
webpack构建流程
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
- 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
webpack钩子函数的执行顺序
webpack的编译按照下面钩子函数的调用顺序执行
- entry-option:初始化option
- run:开始编译
- make:从entry开始递归的分析依赖,对每个依赖模块进行build
- before-resolve:对模块位置进行解析
- build-module:开始构建某个模块
- normal-module-loader:将loader加载完成的模块进行编译,生成AST树
- program:遍历AST树,当遇到require等一些调用表达式时,收集依赖
- seal:所有依赖build完成,开始优化
- emit:输出到dist目录
准备阶段
WebpackOptionsApply
在WebpackOptionsApply.js中调用了entry-option这个钩子,这个方法内部主要的功能是:
- 将所有的配置options参数转换成webpack内部插件,挂载到compiler实例上
- 使用默认插件列表 比如:
- output.library -> LibraryTemplatePlugin
- externals -> ExternalsPlugin
- devtool -> EvalDevtoolModulePlugin、SourceMapDevtoolModulePlugin
- AMDPlugin
- CommonJsPlugin
- RemoveEmptyChunksPlugin
EntryOptionPlugin.js中订阅了entry-option钩子,在entry 配置项处理过之后,执行插件,初始化完成,实例化compiler时,会创造compilation对象,和contextModule和normalModule两个工厂函数,最终调用compiler.run()。
模块构建和资源生成阶段
流程相关Hooks
- beforeRun
- run
- beforeCompile:编译(compilation)参数创建之后,执行插件。
- compile:一个新的编译(compilation)创建之后,钩入(hook into) compiler。
- make
- compilation.addEntry -> addModuleChain -> buildModule
- compilation.finish
- compilation.seal
- afterCompile
- emit:生成资源到 output 目录之前。
- afterEmit:生成资源到 output 目录之后。
- done:编译(compilation)完成。
监听相关:
- watch-run:监听模式下,一个新的编译(compilation)触发之后,执行一个插件,但是是在实际编译开始之前。
- watch-close:监听模式停止。
每次文件系统访问文件都会被缓存,以便于更快触发对同一文件的多个并行或串行请求。在 watch 模式 下,只有修改过的文件会被从缓存中移出。如果关闭 watch 模式,则会在每次编译前清理缓存。
compiler调用compilation生命周期方法
- addEntry -> addModuleChain
- finish
- seal
chunk生成算法
chunk 有两种形式
- initial(初始化) 是入口起点的 main chunk。此 chunk 包含为入口起点指定的所有模块及其依赖项。
- non-initial 是可以延迟加载的块。可能会出现在使用 动态导入(dynamic imports) 或者 SplitChunksPlugin 时。 每个 chunk 都有对应的 asset(资源)。资源,是指输出文件(即打包结果)。
默认情况下,这些 non-initial chunk 没有名称,因此会使用唯一 ID 来替代名称。 在使用动态导入时,我们可以通过使用 magic comment(魔术注释) 来显式指定 chunk 名称
import(
/* webpackChunkName: "app" */
'./app.jsx'
).then(App => ReactDOM.render(<App />, root));
输出文件的名称会受配置中的两个字段的影响:
- output.filename - 用于 initial chunk 文件
- output.chunkFilename - 用于 non-initial chunk 文件
chunk生成过程
- webpack先将entry中对应的module都生成一个新的chunk
- 遍历module的依赖列表,将依赖的module也加入到chunk中
- 如果一个依赖module是动态引入的模块,那么就会根据这个module创建一个新的chunk,继续遍历依赖
- 重复上面的过程,直到得到所有的chunks
webpack loader
loader的参数获取
通过loader-utils的getOptions方法获取loader中配置的options参数
loader异常处理
- 内部throw
- 通过this.callback(err, res)返回