速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

玩转 webpack(一):webpack 的基本架构和构建流程

  • 2019-08-21
  • 本文字数:12517 字

    阅读完需:约 41 分钟

玩转webpack(一):webpack的基本架构和构建流程

webpack 是一个强大的模块打包工具,之所以强大的一个原因在于它拥有灵活、丰富的插件机制。但是 webpack 的文档不太友好,就个人的学习经历来说,官方的文档并不详细,网上的学习资料又少有完整的概述和例子。所以,在研究了一段时间的 webpack 源码之后,自己希望写个系列文章,结合自己的实践一起来谈谈 webpack 插件这个主题,也希望能够帮助其他人更全面地了解 webpack。

这篇文章是系列文章的第一篇,将会讲述 webpack 的基本架构以及构建流程。

P.S. 以下的分析都基于 webpack 3.6.0

webpack 的基本架构

webpack 的基本架构,是基于一种类似事件的方式。下面的代码中,对象可以使用 plugin 函数来注册一个事件,暂时可以理解为我们熟悉的 addEventListener。但为了区分概念,后续的讨论中会将事件名称为 任务点,比如下面有四个任务点 compilation, optimize, compile, before-resolve:


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 实例添加内部插件:


// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L37
compiler = new Compiler();// 其他代码..compiler.options = new WebpackOptionsApply().process(options, compiler);
复制代码


在 WebpackOptionsApply 这个插件内部会根据我们传入的 webpack 配置来初始化需要的内部插件:


// 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(optionsnode),    new LoaderTargetPlugin(options.target));
// 其他代码..
compiler.apply(new EntryOptionPlugin());compiler.applyPluginsBailResult("entry-option", options.context, options.entry);
compiler.apply(new CompatibilityPlugin(),newHarmonyModulesPlugin(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 风格的代码:


// https://github.com/webpack/webpack/blob/master/lib/JsonpTemplatePlugin.jsmainTemplate.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 的基本架构之后,可能会产生疑问,每个插件应该监听哪个对象的哪个任务点,又如何对实现特定功能呢?


要完全解答这个问题很难,原因在于 webpack 中构建过程中,会涉及到非常多的对象和任务点,要对每个对象和任务点都进行讨论是很困难的。但是,我们仍然可以挑选完整构建流程中涉及到的几个核心对象和任务点,把 webpack 的构建流程讲清楚,当我们需要实现某个特定内容的时候,再去找对应的模块源码查阅任务点。


那么下面我们就来聊一聊 webpack 的构建流程。

webpack 的构建流程

为了更清楚和方便地讨论构建流程,这里按照个人理解整理了 webpack 构建流程中比较重要的几个对象以及对应的任务点,并且按照构建顺序画出了流程图:


  • 图中每一列顶部名称表示该列中任务点所属的对象

  • 图中每一行表示一个阶段

  • 图中每个节点表示任务点名称

  • 图中每个节点括号表示任务点的参数,参数带有 callback 是异步任务点

  • 图中的箭头表示任务点的执行顺序

  • 图中虚线表示存在循环流程



上面展示的只是 webpack 构建的一部分,比如与 Module 相关的对象只画出了 NormalModuleFactory,与 Template 相关的对象也只画出了 MainTemplate 等。原因在于上面的流程图已经足以说明主要的构建步骤,另外有没画出来的对象和任务点跟上述的类似,比如 ContextModuleFactory 跟 NormalModuleFactory 是十分相似的对象,也有相似的任务点。有兴趣的同学可以自行拓展探索流程图。


流程图中已经展示了一些核心任务点对应的对象以及触发顺序,但是我们仍然不明白这些任务点有什么含义。所以剩下的内容会详细讲解 webpack 一些任务点详细的动作,按照个人理解将流程图分成了水平的三行,表示三个阶段,分别是:

webpack 的准备阶段

这个阶段的主要工作,是创建 Compiler 和 Compilation 实例。


首先我们从 webpack 的运行开始讲起,在前面我们大概地讲过,当我们开始运行 webpack 的时候,就会创建 Compiler 实例并且加载内部插件。这里跟构建流程相关性比较大的内部插件是 EntryOptionPlugin,我们来看看它到底做了什么:


// https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.jscompiler.apply(new EntryOptionPlugin());compiler.applyPluginsBailResult("entry-option",  options.context, options.entry); // 马上触发任务点运行 EntryOptionPlugin 内部逻辑// https://github.com/webpack/webpack/blob/master/lib/EntryOptionPlugin.jsmodule.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.jsclass 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 属性是解析完成的:


// 监听任务点 runcompiler.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 和 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-loadercreatedModule = new NormalModule( result.request, //!!/path/to/a.js result.userRequest, //!/path/to/a.js result.rawRequest, // raw-loader!./a.js result.loaders, // [,] 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 的⽣成算法:


  1. webpack 先将 entry 中对应的 module 都⽣成⼀个新的 chunk

  2. 遍历 module 的依赖列表,将依赖的 module 也加⼊到 chunk 中

  3. 如果⼀个依赖 module 是动态引⼊的模块,那么就会根据这个 module 创建⼀个新的 chunk,继续遍历依赖

  4. 重复上⾯的过程,直⾄得到所有的 chunks


在所有 chunks ⽣成之后,webpack 会对 chunks 和 modules 进⾏⼀些优化相关的操作,⽐如分配 id、排序等,并且触发⼀系列相关的任务点:


// ⼊⼝ chunk/******/ (function(modules) { // webpackBootstrap/******/    // install a JSONP callback for chunk loading
/******/ var parentJsonpFunction = window["webpackJsonp"];/******/ window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {/******/ // add "moreModules" to the modules object,/******/ // then flag all "chunkIds" as loaded and fire callback
/******/ var moduleId, chunkId, i = 0, resolves = [], result;/******/ for(;i < chunkIds.length; i++) {/******/ chunkId = chunkIds[i];/******/ if(installedChunks[chunkId]) {/******/ resolves.push(installedChunks[chunkId][0]);/******/ }/******/ installedChunks[chunkId] = 0;/******/ }/******/ for(moduleId in moreModules) {/******/ if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {/******/ modules[moduleId] = moreModules[moduleId];/******/ }/******/ }/******/ if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);/******/ while(resolves.length) {/******/ resolves.shift()();/******/ }/******//******/ };/******/ // 其他代码../******/ })(/* modules代码 */);
// ⾮⼊⼝ chunkwebpackJsonp([0],[ /* modules代码.. */]);
复制代码


这些任务点⼀般是 webpack . optimize 属性下的插件会使⽤到,⽐如 CommonsChunkPlugin 会使⽤到任务点 optimize - chunks,但这⾥我们不深⼊讨论。


⾄此,modules 和 chunks 的⽣成阶段结束。接下来是⽂件⽣成阶段。

⽂件⽣成阶段

这个阶段的主要内容,是根据 chunks ⽣成最终⽂件。主要有三个步骤:模板 hash 更新,模板渲染 chunk,⽣成⽂件


Compilation 在实例化的时候,就会同时实例化三个对象:MainTemplate,ChunkTemplate,ModuleTemplate。这三个对象是⽤来渲染 chunk 对象,得到最终代码的模板。第⼀个对应了在 entry 配置的⼊⼝ chunk 的渲染模板,第⼆个是动态引⼊的⾮⼊⼝ chunk 的渲染模板,最后是 chunk 中的 module 的渲染模板。


在开始渲染之前, Compilation 实例会调⽤ createHash ⽅法来⽣成这次构建的 hash。在 webpack 的配置中,我们可以在 output . filename 中配置 [ hash ] 占位符,最终就会替换成这个 hash。同样, createHash 也会为每⼀个 chunk 也创建⼀个 hash,对应 output . filename 的[ chunkhash ] 占位符。


每个 hash 的影响因素⽐较多,⾸先三个模板对象会调⽤ updateHash ⽅法来更新 hash,在内部还会触发任务点 hash,传递 hash 到其他插件。 chunkhash 也是类似的原理:


// https://github.com/webpack/webpack/blob/master/lib/Compilation.jsclass Compilation extends Tapable {    // 其他代码..    createHash() {        // 其他代码..        const hash = crypto.createHash(hashFunction);        if(outputOptions.hashSalt)        hash.update(outputOptions.hashSalt);        this.mainTemplate.updateHash(hash);        this.chunkTemplate.updateHash(hash);        this.moduleTemplate.updateHash(hash);        // 其他代码..        for(let i = 0; i < chunks.length; i++) {            const chunk = chunks[i];            const chunkHash = crypto.createHash(hashFunction);            if(outputOptions.hashSalt)            chunkHash.update(outputOptions.hashSalt);            chunk.updateHash(chunkHash);            if(chunk.hasRuntime()) {                this.mainTemplate.updateHashForChunk(chunkHash, chunk);            } else {                this.chunkTemplate.updateHashForChunk(chunkHash, chunk);            }            this.applyPlugins2("chunk-hash", chunk, chunkHash);            chunk.hash = chunkHash.digest(hashDigest);            hash.update(chunk.hash);            chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);        }        this.fullHash = hash.digest(hashDigest);        this.hash = this.fullHash.substr(0, hashDigestLength);    }}
复制代码


当 hash 都创建完成之后,下⼀步就会遍历 compilation . chunks 来渲染每⼀个 chunk。如果⼀个 chunk 是⼊⼝ chunk,那么就会调⽤ MainTemplate 实例的 render ⽅法,否则调⽤ ChunkTemplate 的 render ⽅法:


// https://github.com/webpack/webpack/blob/master/lib/Compilation.jsclass Compilation extends Tapable {    // 其他代码..    createChunkAssets() {        // 其他代码..        for(let i = 0; i < this.chunks.length; i++) {            const chunk = this.chunks[i];            // 其他代码..            if(chunk.hasRuntime()) {                source = this.mainTemplate.render(this.hash, chunk,this.moduleTemplate, this.dependencyTemplates);            } else {                source = this.chunkTemplate.render(chunk,this.moduleTemplate,this.dependencyTemplates);            }             file = this.getPath(filenameTemplate, {                noChunkHash: !useChunkHash,                chunk            });            this.assets[file] = source;            // 其他代码..        }    }}
复制代码


这⾥注意到 ModuleTemplate 实例会被传递下去,在实际渲染时将会⽤ ModuleTemplate 来渲染每⼀个 module,其实更多是往 module 前后添加⼀些"包装"代码,因为 module 的源码实际上是已经渲染完毕的(还记得前⾯的 loaders 应⽤吗?)。


MainTemplate 的渲染跟 ChunkTemplate 的不同点在于,⼊⼝ chunk 的源码中会带有启动 webpack 的代码,⽽⾮⼊⼝ chunk 的源码是不需要的。这个只要查看 webpack 构建后的⽂件就可以⽐较清楚地看到区别:


// ⼊⼝ chunk/******/ (function(modules) { // webpackBootstrap/******/     // install a JSONP callback for chunk loading/******/     var parentJsonpFunction = window["webpackJsonp"];/******/     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules/******/         // add "moreModules" to the modules object,/******/         // then flag all "chunkIds" as loaded and fire callback/******/         var moduleId, chunkId, i = 0, resolves = [], result;/******/         for(;i < chunkIds.length; i++) {/******/             chunkId = chunkIds[i];/******/             if(installedChunks[chunkId]) {/******/                 resolves.push(installedChunks[chunkId][0]);/******/             }/******/             installedChunks[chunkId] = 0;/******/         }/******/         for(moduleId in moreModules) {/******/             if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {/******/                 modules[moduleId] = moreModules[moduleId];/******/             }/******/         }/******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, execut/******/         while(resolves.length) {/******/             resolves.shift()();/******/         }/******/        /******/     };/******/     // 其他代码../******/ })(/* modules代码 */);// ⾮⼊⼝ chunkwebpackJsonp([0],[    /* modules代码.. */]);
复制代码


当每个 chunk 的源码⽣成之后,就会添加在 Compilation 实例的 assets 属性中。


assets 对象的 key 是最终要⽣成的⽂件名称,因此这⾥要⽤到前⾯创建的 hash。调⽤Compilation 实例内部的 getPath ⽅法会根据配置中的 output . filename 来⽣成⽂件名称。


assets 对象的 value 是⼀个对象,对象需要包含两个⽅法, source 和 size 分别返回⽂件内容和⽂件⼤⼩。


当所有的 chunk 都渲染完成之后,assets 就是最终更要⽣成的⽂件列表。此时 Compilation 实例还会触发⼏个任务点,例如 addtional - chunk - assets, addintial - assets 等,在这些任务点可以修改 assets 属性来改变最终要⽣成的⽂件。


完成上⾯的操作之后, Compilation 实例的 seal ⽅法结束,进⼊到 Compiler 实例的 emitAssets ⽅法。 Compilation 实例的所有⼯作到此也全部结束,意味着⼀次构建过程已经结束,接下来只有⽂件⽣成的步骤。


在 Compiler 实例开始⽣成⽂件前,最后⼀个修改最终⽂件⽣成的任务点 emit 会被触发:


// 监听 emit 任务点,修改最终⽂件的最后机会compiler.plugin("emit", (compilation, callback) => {    let data = "abcd"    compilation.assets["newFile.js"] = {        source() {            return data        }        size() {            return data.length        }    }})
复制代码


当任务点 emit 被触发之后,接下来 webpack 会直接遍历 compilation . assets ⽣成所有⽂件,然后触发任务点 done,结束构建流程。

总结

经过全⽂的讨论,我们将 webpack 的基本架构以及核⼼的构建流程都过了⼀遍,希望在阅读完全⽂之后,对⼤家了解 webpack 原理有所帮助。


下⼀篇⽂章将会讲解 webpack 核⼼的对象,敬请期待。


作者介绍:


陈柏信,腾讯前端开发,目前主要负责手 Q 游戏中心业务开发,以及项目相关的技术升级、架构优化等工作。


本文转载自公众号小时光茶舍(ID:gh_7322a0f167b5)。


原文链接:


https://mp.weixin.qq.com/s/5Ke5EE1UnnAbj1CPrQH7SQ


2019-08-21 11:012698

评论

发布
暂无评论
发现更多内容

架构实战营模块六作业

zhihai.tu

Spring源码分析(七)扩展接口BeanPostProcessors源码分析

石臻臻的杂货铺

spring 9月月更

拆分电商系统为微服务

张立奎

「知识点」PropTypes提供的验证器

叶一一

JavaScript 前端 9月月更

【精通内核】CPU控制并发原理CPU的中断控制

小明Java问道之路

Linux cpu Linux内核 汇编语言 9月月更

都2022年了,Python Web框架你不会只知道Django和Flask吧?

梦想橡皮擦

Python 9月月更

三种获取URL参数值的方法

devpoint

JavaScript URL参数解析 9月月更

在互联网,摸爬滚打了几年,我悟了。面对如今经济形势,普通打工人如何应对?

HullQin

Go golang 后端 websocket 9月月更

Kubernetes网络插件详解 - Calico篇 - 网络基础

巨子嘉

k8s自定义controller三部曲之三:编写controller代码

程序员欣宸

Kubernetes Controller 9月月更

云原生(三十五) | Prometheus入门和安装

Lansonli

云原生 k8s 9月月更

Spring源码分析(八)Spring 所有BeanFactoryPostProcessor扩展接口

石臻臻的杂货铺

spring

设计模式的艺术 第二十四章策略设计模式练习(开发一款飞机模拟系统,该系统主要模拟不同种类飞机的飞行特征与起飞特征,为了将来能够模拟更多种类的飞机,试采用策略模式设计该飞机模拟系统)

代廉洁

设计模式的艺术

Mavan:自定义骨架及工程初始化

程序员架构进阶

maven 低代码 9月日更 9月月更

npm run 脚本背后的事情

汪子熙

node.js 开源 npm YARN 9月月更

大型网站架构

源字节1号

软件架构 后端开发

FreeRTOS记录(一、熟悉开发环境以及CubeMX下FreeRTOS配置)

矜辰所致

STM32CubeMX FreeRTOS 9月月更

挑战30天学完Python:Day1火力全开-初识Python(含系列大纲)

MegaQi

9月月更 挑战30天学完Python

5 个 JavaScript 写法小技巧分享

掘金安东尼

JavaScript 9月月更

从改善设计的角度理解TDD

Bright

敏捷 TDD

记一次 swap 导致系统盘高 IOPS 问题排查

卫智雄

linux运维

Java 键盘输入n个数进行排序输出

排序 java基础 9月月更

IO多路复用中的Select/poll/epoll总结全乎了

知识浅谈

IO多路复用 9月月更

从改善设计的角度理解TDD (2)

Bright

敏捷 TDD

好代码的五个特质-CUPID

Bright

敏捷 DDD TDD

2022-09-03:n块石头放置在二维平面中的一些整数坐标点上 每个坐标点上最多只能有一块石头 如果一块石头的 同行或者同列 上有其他石头存在,那么就可以移除这块石头。 给你一个长度为 n 的数组

福大大架构师每日一题

算法 rust 福大大

日拱算法:什么是“情感丰富的文字”?

掘金安东尼

9月月更

LeetCode二分查找使用JavaScript解题,前端学算法

大师兄

JavaScript 面试 算法 LeetCode 9月月更

Java问题解决录: 运行时抛出NoSuchMethodError / NoSuchFieldError异常

崔认知

redis数据结构之压缩列表

急需上岸的小谢

9月月更

我理解的Smart Domain与DDD

Bright

敏捷 DDD TDD

玩转webpack(一):webpack的基本架构和构建流程_架构_陈柏信_InfoQ精选文章