本文旨在搞清楚从命令行下敲下 webpack
命令,或者配置 npm script
后执行 package.json
中的命令,到工程目录下出现打包的后的 bundle
文件的过程中,webpack都替我们做了哪些工作。
测试用webpack版本为 webpack@3.4.1
webpack.config.js中定义好相关配置,包括 entry
、output
、module
、plugins
等,命令行执行 webpack 命令,webpack 便会根据配置文件中的配置进行打包处理文件,并生成最后打包后的文件。
第一步:执行 webpack 命令时,发生了什么?(bin/webpack.js)
命令行执行 webpack 时,如果全局命令行中未找到webpack命令的话,执行本地的node-modules/bin/webpack.js 文件。
在bin/webpack.js中使用 yargs库 解析了命令行的参数,处理了 webpack 的配置对象 options,调用 processOptions()
函数。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
| function processOptions(options) { if(typeof options.then === "function") {...} var firstOptions = [].concat(options)[0];
var statsPresetToOptions = require("../lib/Stats.js").presetToOptions;
var outputOptions = options.stats; if(typeof outputOptions === "boolean" || typeof outputOptions === "string") { outputOptions = statsPresetToOptions(outputOptions); } else if(!outputOptions) { outputOptions = {}; } ifArg("display", function(preset) { outputOptions = statsPresetToOptions(preset); }); ...
var webpack = require("../lib/webpack.js");
Error.stackTraceLimit = 30; var lastHash = null; var compiler; try { compiler = webpack(options); } catch(e) { var WebpackOptionsValidationError = require("../lib/WebpackOptionsValidationError"); if(e instanceof WebpackOptionsValidationError) { if(argv.color) console.error("\u001b[1m\u001b[31m" + e.message + "\u001b[39m\u001b[22m"); else console.error(e.message); process.exit(1); } throw e; } if(argv.progress) { var ProgressPlugin = require("../lib/ProgressPlugin"); compiler.apply(new ProgressPlugin({ profile: argv.profile })); } function compilerCallback(err, stats) {} if(firstOptions.watch || options.watch) { var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {}; if(watchOptions.stdin) { process.stdin.on("end", function() { process.exit(0); }); process.stdin.resume(); } compiler.watch(watchOptions, compilerCallback); console.log("\nWebpack is watching the files…\n"); } else compiler.run(compilerCallback); }
|
第二步: 调用 webpack,返回 compiler 对象的过程(lib/webpack.js)
如下图所示,lib/webpack.js 中的关键函数为 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
| "use strict"; const Compiler = require("./Compiler"); const MultiCompiler = require("./MultiCompiler"); const NodeEnvironmentPlugin = require("./node/NodeEnvironmentPlugin"); const WebpackOptionsApply = require("./WebpackOptionsApply"); const WebpackOptionsDefaulter = require("./WebpackOptionsDefaulter"); const validateSchema = require("./validateSchema"); const WebpackOptionsValidationError = require("./WebpackOptionsValidationError"); const webpackOptionsSchema = require("../schemas/webpackOptionsSchema.json");
function webpack(options, callback) {...} exports = module.exports = webpack;
webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter; webpack.WebpackOptionsApply = WebpackOptionsApply; webpack.Compiler = Compiler; webpack.MultiCompiler = MultiCompiler; webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin; webpack.validate = validateSchema.bind(this, webpackOptionsSchema); webpack.validateSchema = validateSchema; webpack.WebpackOptionsValidationError = WebpackOptionsValidationError;
function exportPlugins(obj, mappings) {...}
exportPlugins(exports, {...}); exportPlugins(exports.optimize = {}, {...});
|
接下来看在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 40 41 42 43 44 45 46 47
| function webpack(options, callback) { const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
if(webpackOptionsValidationErrors.length) { throw new WebpackOptionsValidationError(webpackOptionsValidationErrors); } let compiler;
if(Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if(typeof options === "object") { new WebpackOptionsDefaulter().process(options); 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); } else { throw new Error("Invalid argument: options"); } if(callback) { if(typeof callback !== "function") throw new Error("Invalid argument: 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; }
|
webpack函数中主要做了以下两个操作,
实例化 Compiler 类。该类继承自 Tapable 类,Tapable 是一个基于发布订阅的插件架构。webpack 便是基于Tapable的发布订阅模式实现的整个流程。Tapable 中通过 plugins 注册插件名,以及对应的回调函数,通过 apply
,applyPlugins
,applyPluginsWater
,applyPluginsAsync
等函数以不同的方式调用注册在某一插件下的回调。
通过WebpackOptionsApply 处理webpack compiler对象,通过 compiler.apply的方式调用了一些必备插件,在这些插件中,注册了一些 plugins,在后面的编译过程中,通过调用一些插件的方式,去处理一些流程。
第三步:调用compiler的run的过程(Compiler.js)
run函数中主要触发了before-run事件,在before-run事件的回调函数中触发了run事件,run事件中调用了readRecord函数读取文件,并调用compile()函数进行编译。
compile函数中定义了编译的相关流程,主要有以下流程:
- 创建编译参数
- 触发
before-compile
事件,
- 触发
compile
事件,开始编译
- 创建
compilation
对象,负责整个编译过程中具体细节的对象
- 触发
make
事件,开始创建模块和分析其依赖
- 根据入口配置的类型,决定是调用哪个plugin中的
make
事件的回调。如单入口的 entry
,调用的是SingleEntryPlugin.js下 make
事件注册的回调函数,其他多入口同理。
- 调用
compilation
对象的 addEntry
函数,创建模块以及依赖。
make
事件的回调函数中,通过seal
封装构建的结果
run
方法中定义的 onCompiled
回调函数被调用,完成emit过程,将结果写入至目标文件
compile函数的定义
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
| 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
事件触发后,有哪些插件中注册了make事件并得到了运行的机会呢?
以单入口entry配置为例,在EntryOptionPlugin插件中定义了,不同配置的入口应该调用何种插件进行解析。不同配置的入口插件中注册了对应的 make
事件回调函数,在make事件触发后被调用。
如下所示:
一个插件的apply
方法是一个插件的核心方法,当说一个插件被调用时主要是其apply方法被调用。
EntryOptionPlugin 插件在webpackOptionsApply中被调用,其内部定义了使用何种插件来解析入口文件。
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
| const SingleEntryPlugin = require("./SingleEntryPlugin"); const MultiEntryPlugin = require("./MultiEntryPlugin"); const DynamicEntryPlugin = require("./DynamicEntryPlugin");
module.exports = class EntryOptionPlugin { apply(compiler) { compiler.plugin("entry-option", (context, entry) => { function itemToPlugin(item, name) { if(Array.isArray(item)) { return new MultiEntryPlugin(context, item, name); } else { return new SingleEntryPlugin(context, item, name); } } if(typeof entry === "string" || Array.isArray(entry)) { compiler.apply(itemToPlugin(entry, "main")); } else if(typeof entry === "object") { Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(entry[name], name))); } else if(typeof entry === "function") { compiler.apply(new DynamicEntryPlugin(context, entry)); } return true; }); } };
|
entry-option
事件被触发时,EntryOptionPlugin 插件做了这几个事情:
【问题】entry-option
事件是在什么时机被触发的呢?
如下代码所示,是在WebpackOptionsApply.js中,先调用处理入口的EntryOptionPlugin插件,然后触发 entry-option
事件,去调用不同类型的入口处理插件。
注意:调用插件的过程也就是一个注册事件以及回调函数的过程。
WebpackOptionApply.js
1 2 3 4
| compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
|
前面说到,make事件触发时,对应的回调逻辑都在不同配置入口的插件中注册的。下面以SingleEntryPlugin为例,说明从 make
事件被触发,到编译结束的整个过程。
SingleEntryPlugin.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 28
| class SingleEntryPlugin { constructor(context, entry, name) { this.context = context; this.entry = entry; this.name = name; }
apply(compiler) { compiler.plugin("compilation", (compilation, params) => { const normalModuleFactory = params.normalModuleFactory; compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory); }); compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); compilation.addEntry(this.context, dep, this.name, callback); }); } static createDependency(entry, name) { const dep = new SingleEntryDependency(entry); dep.loc = name; return dep; } }
module.exports = SingleEntryPlugin;
|
Compilation中负责具体编译的细节,包括如何创建模块以及模块的依赖,根据模板生成js等。如:addEntry
,buildModule
, processModuleDependencies
等。
Compilation.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
| addEntry(context, entry, name, callback) { const slot = { name: name, module: null }; this.preparedChunks.push(slot); this._addModuleChain(context, entry, (module) => { entry.module = module; this.entries.push(module); module.issuer = null; }, (err, module) => { if(err) { return callback(err); }
if(module) { slot.module = module; } else { const idx = this.preparedChunks.indexOf(slot); this.preparedChunks.splice(idx, 1); } return callback(null, module); }); }
|
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| _addModuleChain(context, dependency, onModule, callback) { const start = this.profile && Date.now(); ...
const moduleFactory = this.dependencyFactories.get(dependency.constructor); ... moduleFactory.create({ contextInfo: { issuer: "", compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { if(err) { return errorAndCallback(new EntryModuleNotFoundError(err)); }
let afterFactory;
if(this.profile) { if(!module.profile) { module.profile = {}; } afterFactory = Date.now(); module.profile.factory = afterFactory - start; } const result = this.addModule(module); if(!result) { module = this.getModule(module);
onModule(module);
if(this.profile) { const afterBuilding = Date.now(); module.profile.building = afterBuilding - afterFactory; }
return callback(null, module); } if(result instanceof Module) { if(this.profile) { result.profile = module.profile; } module = result; onModule(module); moduleReady.call(this); return; } onModule(module); this.buildModule(module, false, null, null, (err) => { if(err) { return errorAndCallback(err); } if(this.profile) { const afterBuilding = Date.now(); module.profile.building = afterBuilding - afterFactory; } moduleReady.call(this); });
function moduleReady() { this.processModuleDependencies(module, err => { if(err) { return callback(err); }
return callback(null, module); }); } }); }
|
_addModuleChain
主要做了以下几件事情:
- 调用对应的模块工厂类去创建module
buildModule
,开始构建模块,收集依赖。构建过程中最耗时的一步,主要完成了调用loader处理模块以及模块之间的依赖,使用acorn生成AST的过程,遍历AST循环收集并构建依赖模块的过程。此处可以深入了解webpack使用loader处理模块的原理。
第四步:模块build完成后,使用seal进行module和chunk的一些处理,包括合并、拆分等。
Compilation的 seal
函数在 make
事件的回调函数中进行了调用。
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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121
| seal(callback) { const self = this; self.applyPlugins0("seal"); self.nextFreeModuleIndex = 0; self.nextFreeModuleIndex2 = 0; self.preparedChunks.forEach(preparedChunk => { const module = preparedChunk.module; const chunk = self.addChunk(preparedChunk.name, module); const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name); entrypoint.unshiftChunk(chunk); chunk.addModule(module); module.addChunk(chunk); chunk.entryModule = module; self.assignIndex(module); self.assignDepth(module); self.processDependenciesBlockForChunk(module, chunk); }); self.sortModules(self.modules); self.applyPlugins0("optimize");
while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) || self.applyPluginsBailResult1("optimize-modules", self.modules) || self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { } self.applyPlugins1("after-optimize-modules", self.modules);
while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) || self.applyPluginsBailResult1("optimize-chunks", self.chunks) || self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)) { } self.applyPlugins1("after-optimize-chunks", self.chunks);
self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) { if(err) { return callback(err); }
self.applyPlugins2("after-optimize-tree", self.chunks, self.modules);
while(self.applyPluginsBailResult("optimize-chunk-modules-basic", self.chunks, self.modules) || self.applyPluginsBailResult("optimize-chunk-modules", self.chunks, self.modules) || self.applyPluginsBailResult("optimize-chunk-modules-advanced", self.chunks, self.modules)) { } self.applyPlugins2("after-optimize-chunk-modules", self.chunks, self.modules);
const shouldRecord = self.applyPluginsBailResult("should-record") !== false;
self.applyPlugins2("revive-modules", self.modules, self.records); self.applyPlugins1("optimize-module-order", self.modules); self.applyPlugins1("advanced-optimize-module-order", self.modules); self.applyPlugins1("before-module-ids", self.modules); self.applyPlugins1("module-ids", self.modules); self.applyModuleIds(); self.applyPlugins1("optimize-module-ids", self.modules); self.applyPlugins1("after-optimize-module-ids", self.modules);
self.sortItemsWithModuleIds();
self.applyPlugins2("revive-chunks", self.chunks, self.records); self.applyPlugins1("optimize-chunk-order", self.chunks); self.applyPlugins1("before-chunk-ids", self.chunks); self.applyChunkIds(); self.applyPlugins1("optimize-chunk-ids", self.chunks); self.applyPlugins1("after-optimize-chunk-ids", self.chunks);
self.sortItemsWithChunkIds();
if(shouldRecord) self.applyPlugins2("record-modules", self.modules, self.records); if(shouldRecord) self.applyPlugins2("record-chunks", self.chunks, self.records);
self.applyPlugins0("before-hash"); self.createHash(); self.applyPlugins0("after-hash");
if(shouldRecord) self.applyPlugins1("record-hash", self.records);
self.applyPlugins0("before-module-assets"); self.createModuleAssets(); if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) { self.applyPlugins0("before-chunk-assets"); self.createChunkAssets(); } self.applyPlugins1("additional-chunk-assets", self.chunks); self.summarizeDependencies(); if(shouldRecord) self.applyPlugins2("record", self, self.records);
self.applyPluginsAsync("additional-assets", err => { if(err) { return callback(err); } self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => { if(err) { return callback(err); } self.applyPlugins1("after-optimize-chunk-assets", self.chunks); self.applyPluginsAsync("optimize-assets", self.assets, err => { if(err) { return callback(err); } self.applyPlugins1("after-optimize-assets", self.assets); if(self.applyPluginsBailResult("need-additional-seal")) { self.unseal(); return self.seal(callback); } return self.applyPluginsAsync("after-seal", callback); }); }); }); }); }
|
在 seal
中可以发现,调用了很多不同的插件,主要就是操作chunk和module的一些插件,生成最后的源代码。其中 createHash
用来生成hash,createChunkAssets
用来生成chunk的源码,createModuleAssets
用来生成Module的源码。在 createChunkAssets
中判断了是否是入口chunk,入口的chunk用mainTemplate生成,否则用chunkTemplate生成。
第五步:通过 emitAssets
将生成的代码输入到output的指定位置
在compiler中的 run
方法中定义了compile的回调函数 onCompiled
, 在编译结束后,会调用该回调函数。在该回调函数中调用了 emitAsset
,触发了 emit
事件,将文件写入到文件系统中的指定位置。
总结:
webpack的源码通过采用Tapable控制其事件流,并通过plugin机制,在webpack构建过程中将一些事件钩子暴露给plugin,使得开发者可以通过编写相应的插件来自定义打包。
参考文章:
细说 webpack 之流程篇
webpack 源码解析