作为模块加载和打包神器,只需配置几个文件,加载各种 loader 就可以享受无痛流程化开发。但对于 webpack 这样一个复杂度较高的插件集合,它的整体流程及思想对我们来说还是很透明的。
本文旨在搞清楚从命令行下敲下 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()
函数。
函数
function processOptions(options) {
// promise风格的处理,暂时还没遇到这种情况的配置
if(typeof options.then === "function") {...}
// 处理传入的options为数组的情况
var firstOptions = [].concat(options)[0];
var statsPresetToOptions = require("../lib/Stats.js").presetToOptions;
// 设置
输出的options
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);
});
...
// 引入lib下的webpack.js,入口
文件
var webpack = require("../lib/webpack.js");
// 设置最大
错误追踪堆栈
Error.stackTraceLimit = 30;
var lastHash = null;
var compiler;
try {
// 编译,这里是关键,需要进入lib/webpack.js
文件查看
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); // eslint-disable-line no-process-exit
}
throw e;
}
//
显示相关参数处理
if(argv.progress) {
var ProgressPlugin = require("../lib/ProgressPlugin");
compiler.apply(new ProgressPlugin({
profile: argv.profile
}));
}
// 编译完后的回调
函数
function compilerCallback(err,stats) {}
// watch模式下的处理
if(firstOptions.watch || options.watch) {
var watchOptions = firstOptions.watchOptions || firstOptions.watch || options.watch || {};
if(watchOptions.stdin) {
process.stdin.on("end",function() {
process.exit(0); // eslint-disable-line
});
process.stdin.resume();
}
compiler.watch(watchOptions,compilerCallback);
console.log("\nWebpack is watching the files…\n");
} else
//
调用run()
函数,正式进入编译过程
compiler.run(compilerCallback);
}
第二步: 调用 webpack,返回 compiler 对象的过程(lib/webpack.js)
如下图所示,lib/webpack.js 中的关键函数为 webpack,其中定义了编译相关的一些操作。
方法,
调用该
方法,返回Compiler的实例对象compiler
function webpack(options,callback) {...}
exports = module.exports = webpack;
// 设置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函数中主要定义了哪些操作
webpack(options)));
} else if(typeof options === "object") {
// 配置options的默认参数
new WebpackOptionsDefaulter().process(options);
// 初始化一个Compiler的实例
compiler = new Compiler();
// 设置context的默认值为进程的当前目录,
绝对路径
compiler.context = options.context;
// 定义compiler的options
属性
compiler.options = options;
// Node环境
插件,其中设置compiler的inputFileSystem,outputFileSystem,watchFileSystem,并定义了before-run的
钩子函数
new NodeEnvironmentPlugin().apply(compiler);
// 应用每个
插件
if(options.plugins && Array.isArray(options.plugins)) {
compiler.apply.apply(compiler,options.plugins);
}
//
调用environment
插件
compiler.applyPlugins("environment");
//
调用after-environment
插件
compiler.applyPlugins("after-environment");
// 处理compiler对象,
调用一些必备
插件
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()调用
run函数中主要触发了before-run事件,在before-run事件的回调函数中触发了run事件,run事件中调用了readRecord函数读取文件,并调用compile()
函数进行编译。
compile()调用
compile函数中定义了编译的相关流程,主要有以下流程:
- 创建编译参数
- 触发 before-compile 事件,
- 触发 compile 事件,开始编译
- 创建 compilation对象,负责整个编译过程中具体细节的对象
- 触发 make 事件,开始创建模块和分析其依赖
- 根据入口配置的类型,决定是调用哪个plugin中的 make 事件的回调。如单入口的 entry,调用的是SingleEntryPlugin.js下 make 事件注册的回调函数,其他多入口同理。
- 调用 compilation 对象的 addEntry 函数,创建模块以及依赖。
- make 事件的回调函数中,通过seal 封装构建的结果
- run 方法中定义的 onCompiled回调函数被调用,完成emit过程,将结果写入至目标文件
compile函数的定义
{
if(err) return callback(err);
// 触发compile事件
this.applyPlugins("compile",params);
// 构建compilation对象,compilation对象负责具体的编译细节
const compilation = this.newCompilation(params);
// 触发make事件,对应的监听make事件的回调
函数在不同的EntryPlugin中
注册,比如singleEntryPlugin
this.applyPluginsParallel("make",compilation,err => {
if(err) return callback(err);
compilation.finish();
compilation.seal(err => {
if(err) return callback(err);
this.applyPluginsAsync("after-compile",err => {
if(err) return callback(err);
return callback(null,compilation);
});
});
});
});
}
【问题】make 事件触发后,有哪些插件中注册了make事件并得到了运行的机会呢?
以单入口entry配置为例,在EntryOptionPlugin插件中定义了,不同配置的入口应该调用何种插件进行解析。不同配置的入口插件中注册了对应的 make 事件回调函数,在make事件触发后被调用。
如下所示:
EntryOptionPlugin 插件在webpackOptionsApply中被调用,其内部定义了使用何种插件来解析入口文件。
{
function itemToPlugin(item,name) {
if(Array.isArray(item)) {
return new MultiEntryPlugin(context,item,name);
} else {
return new SingleEntryPlugin(context,name);
}
}
// 判断entry字段的类型去
调用不同的入口
插件去处理
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 字段来判断,对应了 entry 字段为 string object function的三种情况
每种不同的类型调用不同的插件去处理入口的配置。大致处理逻辑如下:
- 数组类型的entry调用multiEntryPlugin插件去处理,对应了多入口的场景
- function的entry调用了DynamicEntryPlugin插件去处理,对应了异步chunk的场景
- string类型的entry或者object类型的entry,调用SingleEntryPlugin去处理,对应了单入口的场景
【问题】entry-option 事件是在什么时机被触发的呢?
如下代码所示,是在WebpackOptionsApply.js中,先调用处理入口的EntryOptionPlugin插件,然后触发 entry-option 事件,去调用不同类型的入口处理插件。
注意:
调用插件的过程也就是一个
注册事件以及回调
函数的过程。
WebpackOptionApply.js
调用处理入口entry的
插件
compiler.apply(new EntryOptionPlugin());
compiler.applyPluginsBailResult("entry-option",options.context,options.entry);
前面说到,make事件触发时,对应的回调逻辑都在不同配置入口的插件中注册的。下面以SingleEntryPlugin为例,说明从 make 事件被触发,到编译结束的整个过程。
SingleEntryPlugin.js
{
const normalModuleFactory = params.normalModuleFactory;
compilation.dependencyFactories.set(SingleEntryDependency,normalModuleFactory);
});
// make 事件在执行compile的时候被触发
compiler.plugin("make",callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry,this.name);
// 编译的关键,
调用Compilation中的addEntry,
添加入口,进入编译过程。
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
{
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);
});
}
{
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);
// 构建模块,
包括调用loader处理
文件,使用acorn
生成AST,遍历AST收集依赖
this.buildModule(module,false,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 事件的回调函数中进行了调用。
{
const module = preparedChunk.module;
// 将module保存在chunk的origins中,origins保存了module的信息
const chunk = self.addChunk(preparedChunk.name,module);
// 创建一个entrypoint
const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
// 将chunk创建的chunk保存在entrypoint中,并将该entrypoint的实例保存在chunk的entrypoints中
entrypoint.unshiftChunk(chunk);
// 将module保存在chunk的_modules数组中
chunk.addModule(module);
// module实例上记录chunk的信息
module.addChunk(chunk);
// 定义该chunk的entryModule
属性
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)) { /* empty */ }
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)) { /* empty */ }
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.modules);
while(self.applyPluginsBailResult("optimize-chunk-modules-basic",self.modules) ||
self.applyPluginsBailResult("optimize-chunk-modules",self.modules) ||
self.applyPluginsBailResult("optimize-chunk-modules-advanced",self.modules)) { /* empty */ }
self.applyPlugins2("after-optimize-chunk-modules",self.modules);
const shouldRecord = self.applyPluginsBailResult("should-record") !== false;
self.applyPlugins2("revive-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.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.records);
if(shouldRecord)
self.applyPlugins2("record-chunks",self.records);
self.applyPlugins0("before-hash");
// 创建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");
// 使用template创建最后的js
代码
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",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,使得开发者可以通过编写相应的插件来自定义打包。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对编程之家的支持。
原文链接:https://www.f2er.com/js/34281.html