webpack构建的详细流程探底

网友投稿 346 2023-02-28


webpack构建的详细流程探底

作为模块加载和打包神器,只需配置几个文件,加载各种 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,其中定义了编译相关的一些操作。

"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,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函数的定义

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 源码解析


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:深入理解 webpack 文件打包机制(小结)
下一篇:java中string.trim()函数的作用实例及源码
相关文章

 发表评论

暂时没有评论,来抢沙发吧~