Home >Web Front-end >JS Tutorial >Detailed introduction to building webpack

Detailed introduction to building webpack

亚连
亚连Original
2018-06-13 10:48:151774browse

Currently, almost all business development and construction will use webpack. Therefore, the following article mainly introduces relevant information to you about the detailed process of webpack construction. The article introduces it in great detail through sample code. It has certain reference and learning value for everyone's study or work. Friends who need it can read it together. Take a look.

As a module loading and packaging artifact, you only need to configure a few files and load various loaders to enjoy painless process development. But for a highly complex plug-in collection like webpack, its overall process and ideas are still very transparent to us.

The purpose of this article is to understand the process of typing the webpack command from the command line, or configuring npm script and then executing the command in package.json, until the packaged bundle file appears in the project directory. What work have you done for us?

The webpack version used for testing is webpack@3.4.1

Define the relevant configurations in webpack.config.js, including entry, output, module, plugins, etc., execute the webpack command on the command line, webpack The file will be packaged and processed according to the configuration in the configuration file, and the final packaged file will be generated.

Step 1: What happens when executing the webpack command? (bin/webpack.js)

When executing webpack from the command line, if the webpack command is not found in the global command line, the local node-modules/bin/webpack.js file will be executed.

Use the yargs library in bin/webpack.js to parse the command line parameters, process the webpack configuration object options, and call the processOptions() function.

// 处理编译相关,核心函数
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);
}

Step 2: Call webpack and return the compiler object (lib/webpack.js)

As shown in the figure below, the key function in lib/webpack.js is webpack, where Defines some operations related to compilation.

"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 = {}, {...});

Let’s look at the main operations defined in the webpack function

// 核心方法,调用该方法,返回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;
}

The webpack function mainly performs the following two operations,

  • Instantiate the Compiler class. This class inherits from the Tapable class, which is a plug-in architecture based on publish and subscribe. webpack is the entire process implemented based on Tapable's publish-subscribe model. In Tapable, plugins are used to register the plug-in name and the corresponding callback function. Callbacks registered under a certain plug-in are called in different ways through functions such as apply, applyPlugins, applyPluginsWater, and applyPluginsAsync.

  • Process the webpack compiler object through WebpackOptionsApply, and call some necessary plug-ins through compiler.apply. In these plug-ins, some plugins are registered. During the compilation process, some processes are processed by calling some plug-ins.

Step 3: Call the compiler's run process (Compiler.js)

run() calls

run The function mainly triggers the before-run event, and the run event is triggered in the callback function of the before-run event. In the run event, the readRecord function is called to read the file, and the compile() function is called for compilation.

compile() calls the

compile function to define the relevant processes of compilation, which mainly include the following processes:

  • Create Compilation parameters

  • trigger the before-compile event,

  • trigger the compile event, start compilation

  • Create a compilation object, the object responsible for the specific details of the entire compilation process

  • Trigger the make event to start creating the module and analyze its dependencies

  • According to the entry The type of configuration determines which plugin's make event callback is called. For example, a single-entry entry calls the callback function registered for the make event under SingleEntryPlugin.js. The same applies to other multi-entry entries.

  • Call the addEntry function of the compilation object to create modules and dependencies.

  • In the callback function of the make event, the result of the build is encapsulated by seal

  • The onCompiled callback function defined in the run method is called to complete emit process, writing the results to the target file

Definition of compile function

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);
 });
 });
 });
 });
}

[Question] After the make event is triggered, which plug-ins have registered make event and got a chance to run?

Taking the single-entry entry configuration as an example, the EntryOptionPlugin plug-in defines which plug-in should be called for parsing of entries with different configurations. Corresponding make event callback functions are registered in entry plug-ins with different configurations, and are called after the make event is triggered.

As shown below:

The apply method of a plug-in is the core method of a plug-in. When a plug-in is called, its apply method is mainly called.

EntryOptionPlugin plug-in is called in webpackOptionsApply, which internally defines which plug-in is used to parse the entry file.

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;
 });
 }
};

When the entry-option event is triggered, the EntryOptionPlugin plug-in does the following things:

Determine the type of entry through the entry field, which corresponds to the entry field being a string object function. Three situations

Each different type calls a different plug-in to handle the configuration of the entrance. The general processing logic is as follows:

  • 数组类型的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,使得开发者可以通过编写相应的插件来自定义打包。

上面是我整理给大家的,希望今后会对大家有帮助。

相关文章:

使用Vue.js 2.0如何实现背景视频登录页面

使用Vue开发时间转换指令该怎么做?

angularjs中如何实现页面自适应?

The above is the detailed content of Detailed introduction to building webpack. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn