Webpack 3.4.1 构建流程源码深度解析

作者:知秋

时间:2026-10-22

浏览:179

本文深入剖析 webpack@3.4.1 的构建全流程,从命令行执行到最终 bundle 文件生成。详细解析了 bin/webpack.js 的参数处理、lib/webpack.js 中 Compiler 实例化、Compiler.run 触发事件流、Compilation 模块构建以及 seal 封装等核心步骤,揭示 Tapable 发布订阅模式在插件机制中的应用。

文章目录

作为模块加载与打包的核心工具,Webpack 仅需少量配置及 Loader 即可实现流畅的开发体验。然而,面对这一由众多插件构成的复杂系统,其内部运作机制与整体架构对开发者而言仍具一定黑盒性质。

本文旨在梳理从命令行输入 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,该函数定义了编译过程中的关键操作。


"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");
// 核心方法,调用该方法,返回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 函数中主要定义的操作内容。


// 核心方法,调用该方法,返回Compiler的实例对象compiler
function webpack(options, callback) {
 // 验证是否符合格式
 const webpackOptionsValidationErrors = validateSchema(webpackOptionsSchema, options);
 if(webpackOptionsValidationErrors.length) {
 throw new WebpackOptionsValidationError(webpackOptionsValidationErrors);
 }
 let compiler;
 // 传入的options为数组的情况,调用MultiCompiler进行处理,目前还没遇到过这种情况的配置
 if(Array.isArray(options)) {
 compiler = new MultiCompiler(options.map(options => 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、applyPluginsWaterfall、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 函数的定义


compile(callback) {
 // 创建编译参数,包括模块工厂和编译依赖参数数组
 const params = this.newCompilationParams();
 // 触发before-compile 事件,开始整个编译过程
 this.applyPluginsAsync("before-compile", params, err => {
 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", compilation, err => {
 if(err) return callback(err);
 return callback(null, compilation);
 });
 });
 });
 });
}

【问题】make 事件触发后,有哪些插件中注册了 make 事件并得到了运行的机会呢?

以单入口 entry 配置为例,在 EntryOptionPlugin 插件中定义了不同配置的入口应调用何种插件进行解析。不同配置的入口插件中注册了对应的 make 事件回调函数,在 make 事件触发后被调用。

如下所示:

一个插件的 apply 方法是一个插件的核心方法,当说一个插件被调用时主要是其 apply 方法被调用。

EntryOptionPlugin 插件在 WebpackOptionsApply 中被调用,其内部定义了使用何种插件来解析入口文件。


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);
 }
 }
 // 判断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


class SingleEntryPlugin {
 constructor(context, entry, name) {
 this.context = context;
 this.entry = entry;
 this.name = name;
 }
 apply(compiler) {
 // compilation 事件在初始化Compilation对象的时候被触发
 compiler.plugin("compilation", (compilation, params) => {
 const normalModuleFactory = params.normalModuleFactory;
 compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory);
 });
 // make 事件在执行compile的时候被触发
 compiler.plugin("make", (compilation, 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


addEntry(context, entry, name, callback) {
 const slot = {
 name: name,
 module: null
 };
 this.preparedChunks.push(slot);
 // 添加该chunk上的module依赖
 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);
 });
}

_addModuleChain(context, dependency, onModule, callback) {
 const start = this.profile && Date.now();
 ...
 // 根据模块的类型获取对应的模块工厂并创建模块
 const moduleFactory = this.dependencyFactories.get(dependency.constructor);
 ...
 // 创建模块,将创建好的模块module作为参数传递给回调函数
 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);
 // 构建模块,包括调用loader处理文件,使用acorn生成AST,遍历AST收集依赖
 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 事件的回调函数中进行了调用。


seal(callback) {
 const self = this;
 // 触发seal事件,提供其他插件中seal的执行时机
 self.applyPlugins0("seal");
 self.nextFreeModuleIndex = 0;
 self.nextFreeModuleIndex2 = 0;
 self.preparedChunks.forEach(preparedChunk => {
 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.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)) { /* empty */ }
 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");
 // 创建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", 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 源码解析

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章