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,使得开发者可以通过编写相应的插件来自定义打包。
好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对的支持。
参考文章:
上一篇: 初识MySQL
下一篇: 腾讯发布外链管理公告