欢迎你们前往腾讯云社区,获取更多腾讯海量技术实践干货哦~javascript
做者介绍:陈柏信,腾讯前端开发,目前主要负责手Q游戏中心业务开发,以及项目相关的技术升级、架构优化等工做。html
webpack 是一个强大的模块打包工具,之因此强大的一个缘由在于它拥有灵活、丰富的插件机制。可是 webpack 的文档不太友好,就我的的学习经从来说,官方的文档并不详细,网上的学习资料又少有完整的概述和例子。因此,在研究了一段时间的 webpack 源码以后,本身但愿写个系列文章,结合本身的实践一块儿来谈谈 webpack 插件这个主题,也但愿可以帮助其余人更全面地了解 webpack。前端
这篇文章是系列文章的第一篇,将会讲述 webpack 的基本架构以及构建流程。vue
P.S. 如下的分析都基于 webpack 3.6.0java
webpack 的基本架构,是基于一种相似事件的方式。下面的代码中,对象可使用 plugin
函数来注册一个事件,暂时能够理解为咱们熟悉的 addEventListener
。但为了区分概念,后续的讨论中会将事件名称为 任务点,好比下面有四个任务点 compilation
, optimize
, compile
, before-resolve
:node
compiler.plugin("compilation", (compilation, callback) => { // 当Compilation实例生成时 compilation.plugin("optimize", () => { // 当全部modules和chunks已生成,开始优化时 }) }) compiler.plugin("compile", (params) => { // 当编译器开始编译模块时 let nmf = params.normalModuleFactory nmf.plugin("before-resolve", (data) => { // 在factory开始解析模块前 }) })
webpack 内部的大部分功能,都是经过这种注册任务点的形式来实现的,这在后面中咱们很容易发现这一点。因此这里直接抛出结论:webpack 的核心功能,是抽离成不少个内部插件来实现的。
那这些内部插件是如何对 webpack 产生做用的呢?在咱们开始运行 webpack 的时候,它会先建立一个 Compiler
实例,而后调用 WebpackOptionsApply
这个模块给 Compiler
实例添加内部插件:webpack
// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L37 compiler = new Compiler(); // 其余代码.. compiler.options = new WebpackOptionsApply().process(options, compiler);
在 WebpackOptionsApply
这个插件内部会根据咱们传入的 webpack 配置来初始化须要的内部插件:git
// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js JsonpTemplatePlugin = require("./JsonpTemplatePlugin"); NodeSourcePlugin = require("./node/NodeSourcePlugin"); compiler.apply( new JsonpTemplatePlugin(options.output), new FunctionModulePlugin(options.output), new NodeSourcePlugin(options.node), new LoaderTargetPlugin(options.target) ); // 其余代码.. compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry); compiler.apply( new CompatibilityPlugin(), new HarmonyModulesPlugin(options.module), new AMDPlugin(options.module, options.amd || {}), new CommonJsPlugin(options.module), new LoaderPlugin(), new NodeStuffPlugin(options.node), new RequireJsStuffPlugin(), new APIPlugin(), new ConstPlugin(), new UseStrictPlugin(), new RequireIncludePlugin(), new RequireEnsurePlugin(), new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles), new ImportPlugin(options.module), new SystemPlugin(options.module) );
每个内部插件,都是经过监放任务点的方式,来实现自定义的逻辑。好比 JsonpTemplatePlugin
这个插件,是经过监听 mainTemplate
对象的 require-ensure
任务点,来生成 jsonp
风格的代码:github
// https://github.com/webpack/webpack/blob/master/lib/JsonpTemplatePlugin.js mainTemplate.plugin("require-ensure", function(_, chunk, hash) { return this.asString([ "var installedChunkData = installedChunks[chunkId];", "if(installedChunkData === 0) {", this.indent([ "return new Promise(function(resolve) { resolve(); });" ]), "}", "", "// a Promise means \"currently loading\".", "if(installedChunkData) {", this.indent([ "return installedChunkData[2];" ]), "}", "", "// setup Promise in chunk cache", "var promise = new Promise(function(resolve, reject) {", this.indent([ "installedChunkData = installedChunks[chunkId] = [resolve, reject];" ]), "});", "installedChunkData[2] = promise;", "", "// start chunk loading", "var head = document.getElementsByTagName('head')[0];", this.applyPluginsWaterfall("jsonp-script", "", chunk, hash), "head.appendChild(script);", "", "return promise;" ]); });
如今咱们理解了 webpack 的基本架构以后,可能会产生疑问,每一个插件应该监听哪一个对象的哪一个任务点,又如何对实现特定功能呢?web
要彻底解答这个问题很难,缘由在于 webpack 中构建过程当中,会涉及到很是多的对象和任务点,要对每一个对象和任务点都进行讨论是很困难的。可是,咱们仍然能够挑选完整构建流程中涉及到的几个核心对象和任务点,把 webpack 的构建流程讲清楚,当咱们须要实现某个特定内容的时候,再去找对应的模块源码查阅任务点。
那么下面咱们就来聊一聊 webpack 的构建流程。
为了更清楚和方便地讨论构建流程,这里按照我的理解整理了 webpack 构建流程中比较重要的几个对象以及对应的任务点,而且按照构建顺序画出了流程图:
图中每一列顶部名称表示该列中任务点所属的对象
图中每一行表示一个阶段
图中每一个节点表示任务点名称
图中每一个节点括号表示任务点的参数,参数带有callback是异步任务点
图中的箭头表示任务点的执行顺序
图中虚线表示存在循环流程
上面展现的只是 webpack 构建的一部分,好比与 Module
相关的对象只画出了 NormalModuleFactory
,与 Template
相关的对象也只画出了 MainTemplate
等。缘由在于上面的流程图已经足以说明主要的构建步骤,另外有没画出来的对象和任务点跟上述的相似,好比 ContextModuleFactory
跟 NormalModuleFactory
是十分类似的对象,也有类似的任务点。有兴趣的同窗能够自行拓展探索流程图。*
流程图中已经展现了一些核心任务点对应的对象以及触发顺序,可是咱们仍然不明白这些任务点有什么含义。因此剩下的内容会详细讲解 webpack 一些任务点详细的动做,按照我的理解将流程图分红了水平的三行,表示三个阶段,分别是:
webpack的准备阶段
modules和chunks的生成阶段
文件生成阶段
这个阶段的主要工做,是建立 Compiler
和 Compilation
实例。
首先咱们从 webpack 的运行开始讲起,在前面咱们大概地讲过,当咱们开始运行 webpack 的时候,就会建立 Compiler
实例而且加载内部插件。这里跟构建流程相关性比较大的内部插件是 EntryOptionPlugin
,咱们来看看它到底作了什么:
// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js compiler.apply(new EntryOptionPlugin()); compiler.applyPluginsBailResult("entry-option", options.context, options.entry); // 立刻触发任务点运行 EntryOptionPlugin 内部逻辑 // https://github.com/webpack/webpack/blob/master/lib/EntryOptionPlugin.js module.exports = class EntryOptionPlugin { apply(compiler) { compiler.plugin("entry-option", (context, entry) => { if(typeof entry === "string" || Array.isArray(entry)) { compiler.apply(itemToPlugin(context, entry, "main")); } else if(typeof entry === "object") { Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(context, entry[name], name))); } else if(typeof entry === "function") { compiler.apply(new DynamicEntryPlugin(context, entry)); } return true; }); } };
EntryOptionPlugin
的代码只有寥寥数行可是很是重要,它会解析传给 webpack 的配置中的 entry 属性,而后生成不一样的插件应用到 Compiler
实例上。这些插件多是 SingleEntryPlugin
, MultiEntryPlugin
或者 DynamicEntryPlugin
。但不论是哪一个插件,内部都会监听 Compiler
实例对象的 make
任务点,以 SingleEntryPlugin
为例:
// https://github.com/webpack/webpack/blob/master/lib/SingleEntryPlugin.js class SingleEntryPlugin { // 其余代码.. apply(compiler) { // 其余代码.. compiler.plugin("make", (compilation, callback) => { const dep = SingleEntryPlugin.createDependency(this.entry, this.name); compilation.addEntry(this.context, dep, this.name, callback); }); } }
这里的 make
任务点将成为后面解析 modules 和 chunks 的起点。
除了 EntryOptionPlugin
,其余的内部插件也会监听特定的任务点来完成特定的逻辑,但咱们这里再也不仔细讨论。当 Compiler
实例加载完内部插件以后,下一步就会直接调用 compiler.run
方法来启动构建,任务点 run
也是在此时触发,值得注意的是此时基本只有 options
属性是解析完成的:
// 监放任务点 run compiler.plugin("run", (compiler, callback) => { console.log(compiler.options) // 能够看到解析后的配置 callback() })
另外要注意的一点是,任务点 run
只有在 webpack 以正常模式运行的状况下会触发,若是咱们以监听(watch)的模式运行 webpack,那么任务点 run
是不会触发的,可是会触发任务点 watch-run
。
接下来, Compiler
对象会开始实例化两个核心的工厂对象,分别是 NormalModuleFactory
和 ContextModuleFactory
。工厂对象顾名思义就是用来建立实例的,它们后续用来建立 NormalModule
以及 ContextModule
实例,这两个工厂对象会在任务点 compile
触发时传递过去,因此任务点 compile
是间接监听这两个对象的任务点的一个入口:
// 监放任务点 compile compiler.plugin("compile", (params) => { let nmf = params.normalModuleFactory nmf.plugin("before-resolve", (data, callback) => { // ... }) })
下一步 Compiler
实例将会开始建立 Compilation
对象,这个对象是后续构建流程中最核心最重要的对象,它包含了一次构建过程当中全部的数据。也就是说一次构建过程对应一个 Compilation
实例。在建立 Compilation
实例时会触发任务点 compilaiion
和 this-compilation
:
// https://github.com/webpack/webpack/blob/master/lib/Compiler.js class Compiler extends Tapable { // 其余代码.. newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; this.applyPlugins("this-compilation", compilation, params); this.applyPlugins("compilation", compilation, params); return compilation; } }
这里为何会有 compilation
和 this-compilation
两个任务点?实际上是跟子编译器有关,Compiler
实例经过 createChildCompiler
方法能够建立子编译器实例 childCompiler
,建立时 childCompiler
会复制 compiler
实例的任务点监听器。任务点 compilation
的监听器会被复制,而任务点 this-compilation
的监听器不会被复制。 更多关于子编译器的内容,将在下一篇文章中讨论。
compilation
和 this-compilation
是最快可以获取到 Compilation
实例的任务点,若是你的插件功能须要尽早对 Compilation
实例进行一些操做,那么这两个任务点是首选:
// 监听 this-compilation 任务点 compiler.plugin("this-compilation", (compilation, params) => { console.log(compilation.options === compiler.options) // true console.log(compilation.compiler === compiler) // true console.log(compilation) })
当 Compilation
实例建立完成以后,webpack 的准备阶段已经完成,下一步将开始 modules 和 chunks 的生成阶段。
**这个阶段的主要内容,是先解析项目依赖的全部 modules,再根据 modules 生成 chunks。
module 解析,包含了三个主要步骤:建立实例、loaders应用以及依赖收集。
chunks 生成,主要步骤是找到 chunk 所须要包含的 modules。**
当上一个阶段完成以后,下一个任务点 make
将被触发,此时内部插件 SingleEntryPlugin
, MultiEntryPlugin
, DynamicEntryPlugin
的监听器会开始执行。监听器都会调用 Compilation
实例的 addEntry
方法,该方法将会触发第一批 module 的解析,这些 module 就是 entry 中配置的模块。
咱们先讲一个 module 解析完成以后的操做,它会递归调用它所依赖的 modules 进行解析,因此当解析中止时,咱们就可以获得项目中全部依赖的 modules,它们将存储在 Compilation
实例的 modules
属性中,并触发任务点 finish-modules
:
// 监听 finish-modules 任务点 compiler.plugin("this-compilation", (compilation) => { compilation.plugin("finish-modules", (modules) => { console.log(modules === compilation.modules) // true modules.forEach(module => { console.log(module._source.source()) // 处理后的源码 }) }) })
下面将以 NormalModule
为例讲解一下 module 的解析过程,ContextModule
等其余模块实例的处理是相似的。
第一个步骤是建立 NormalModule
实例。这里须要用到上一个阶段讲到的 NormalModuleFactory
实例, NormalModuleFactory
的 create
方法是建立 NormalModule
实例的入口,内部的主要过程是解析 module 须要用到的一些属性,好比须要用到的 loaders
, 资源路径 resource
等等,最终将解析完毕的参数传给 NormalModule
构建函数直接实例化:
// https://github.com/webpack/webpack/blob/master/lib/NormalModuleFactory.js // 以 require("raw-loader!./a") 为例 // 而且对 .js 后缀配置了 babel-loader createdModule = new NormalModule( result.request, // <raw-loader>!<babel-loader>!/path/to/a.js result.userRequest, // <raw-loader>!/path/to/a.js result.rawRequest, // raw-loader!./a.js result.loaders, // [<raw-loader>, <babel-loader>] result.resource, // /path/to/a.js result.parser );
这里在解析参数的过程当中,有两个比较实用的任务点 before-resolve
和 after-resolve
,分别对应了解析参数前和解析参数后的时间点。举个例子,在任务点 before-resolve
能够作到忽略某个 module 的解析,webpack 内部插件 IgnorePlugin
就是这么作的:
// https://github.com/webpack/webpack/blob/master/lib/IgnorePlugin.js class IgnorePlugin { checkIgnore(result, callback) { // check if result is ignored if(this.checkResult(result)) { return callback(); // callback第二个参数为 undefined 时会终止module解析 } return callback(null, result); } apply(compiler) { compiler.plugin("normal-module-factory", (nmf) => { nmf.plugin("before-resolve", this.checkIgnore); }); compiler.plugin("context-module-factory", (cmf) => { cmf.plugin("before-resolve", this.checkIgnore); }); } }
在建立完 NormalModule
实例以后会调用 build
方法继续进行内部的构建。咱们熟悉的 loaders 将会在这里开始应用,NormalModule
实例中的 loaders
属性已经记录了该模块须要应用的 loaders。应用 loaders 的过程相对简单,直接调用loader-runner 这个模块便可:
// https://github.com/webpack/webpack/blob/master/lib/NormalModule.js const runLoaders = require("loader-runner").runLoaders; // 其余代码.. class NormalModule extends Module { // 其余代码.. doBuild(options, compilation, resolver, fs, callback) { this.cacheable = false; const loaderContext = this.createLoaderContext(resolver, options, compilation, fs); runLoaders({ resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { // 其余代码.. }); } }
webpack 中要求 NormalModule
最终都是 js 模块,因此 loader 的做用之一是将不一样的资源文件转化成 js 模块。好比 html-loader
是将 html 转化成一个 js 模块。在应用完 loaders 以后,NormalModule
实例的源码必然就是 js 代码,这对下一个步骤很重要。
下一步咱们须要获得这个 module 所依赖的其余模块,因此就有一个依赖收集的过程。webpack 的依赖收集过程是将 js 源码传给 js parser(webpack 使用的 parser 是 acorn):
// https://github.com/webpack/webpack/blob/master/lib/NormalModule.js class NormalModule extends Module { // 其余代码.. build(options, compilation, resolver, fs, callback) { // 其余代码.. return this.doBuild(options, compilation, resolver, fs, (err) => { // 其余代码.. try { this.parser.parse(this._source.source(), { current: this, module: this, compilation: compilation, options: options }); } catch(e) { const source = this._source.source(); const error = new ModuleParseError(this, source, e); this.markModuleAsErrored(error); return callback(); } return callback(); }); } }
parser 将 js 源码解析后获得对应的AST(抽象语法树, Abstract Syntax Tree)。而后 webpack 会遍历 AST,按照必定规则触发任务点。 好比 js 源码中有一个表达式:a.b.c
,那么 parser
对象就会触发任务点 expression a.b.c
。更多相关的规则 webpack 在官网有罗列出来,你们能够对照着使用。
有了AST对应的任务点,依赖收集就相对简单了,好比遇到任务点 call require
,说明在代码中是有调用了require
函数,那么就应该给 module 添加新的依赖。webpack 关于这部分的处理是比较复杂的,由于 webpack 要兼容多种不一样的依赖方式,好比 AMD 规范、CommonJS规范,而后还要区分动态引用的状况,好比使用了 require.ensure
, require.context
。但这些细节对于咱们讨论构建流程并非必须的,由于不展开细节讨论。
当 parser
解析完成以后,module 的解析过程就完成了。每一个 module 解析完成以后,都会触发 Compilation
实例对象的任务点 succeed-module
,咱们能够在这个任务点获取到刚解析完的 module 对象。正如前面所说,module 接下来还要继续递归解析它的依赖模块,最终咱们会获得项目所依赖的全部 modules。此时任务点 make
结束。
继续往下走,Compialtion
实例的 seal
方法会被调用并立刻触发任务点 seal
。在这个任务点,咱们能够拿到全部解析完成的 module:
// 监听 seal 任务点 compiler.plugin("this-compilation", (compilation) => { console.log(compilation.modules.length === 0) // true compilation.plugin("seal", () => { console.log(compilation.modules.length > 0) // true }) })
有了全部的 modules 以后,webpack 会开始生成 chunks。webpack 中的 chunk 概念,要不就是配置在 entry 中的模块,要不就是动态引入(好比 require.ensure
)的模块。这些 chunk 对象是 webpack 生成最终文件的一个重要依据。
每一个 chunk 的生成就是找到须要包含的 modules。这里大体描述一下 chunk 的生成算法:
webpack 先将 entry 中对应的 module 都生成一个新的 chunk
遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
若是一个依赖 module 是动态引入的模块,那么就会根据这个 module 建立一个新的 chunk,继续遍历依赖
重复上面的过程,直至获得全部的 chunks
在全部 chunks 生成以后,webpack 会对 chunks 和 modules 进行一些优化相关的操做,好比分配id、排序等,而且触发一系列相关的任务点:
// https://github.com/webpack/webpack/blob/master/lib/Compilation.js class Compilation extends Tapable { // 其余代码 .. seal(callback) { // 生成 chunks 代码.. 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); // 其余代码.. }) } }
这些任务点通常是 webpack.optimize
属性下的插件会使用到,好比 CommonsChunkPlugin
会使用到任务点 optimize-chunks
,但这里咱们不深刻讨论。
至此,modules 和 chunks 的生成阶段结束。接下来是文件生成阶段。
本文来源于 小时光茶社 微信公众号
玩转webpack(一)下篇:webpack的基本架构和构建流程
Webpack + vue 之抽离 CSS 的正确姿式
apt 与 JavaPoet 自动生成代码