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

Posted 小时光茶社

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了玩转webpack:webpack的基本架构和构建流程相关的知识,希望对你有一定的参考价值。

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

前言

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

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

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

webpack的基本架构

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

 
   
   
 
  1. compiler.plugin("compilation", (compilation, callback) => {

  2.    // 当Compilation实例生成时

  3.    compilation.plugin("optimize", () => {

  4.        // 当所有modules和chunks已生成,开始优化时

  5.    })

  6. })

  7. compiler.plugin("compile", (params) => {

  8.    // 当编译器开始编译模块时

  9.    let nmf = params.normalModuleFactory

  10.    nmf.plugin("before-resolve", (data) => {

  11.        // 在factory开始解析模块前

  12.    })

  13. })

webpack 内部的大部分功能,都是通过这种注册任务点的形式来实现的,这在后面中我们很容易发现这一点。所以这里直接抛出结论:webpack 的核心功能,是抽离成很多个内部插件来实现的。那这些内部插件是如何对 webpack 产生作用的呢?在我们开始运行 webpack 的时候,它会先创建一个 Compiler 实例,然后调用 WebpackOptionsApply 这个模块给 Compiler 实例添加内部插件:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/webpack.js#L37

  2. compiler = new Compiler();

  3. // 其他代码..

  4. compiler.options = new WebpackOptionsApply().process(options, compiler);

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

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js

  2. JsonpTemplatePlugin = require("./JsonpTemplatePlugin");

  3. NodeSourcePlugin = require("./node/NodeSourcePlugin");

  4. compiler.apply(

  5.    new JsonpTemplatePlugin(options.output),

  6.    new FunctionModulePlugin(options.output),

  7.    new NodeSourcePlugin(options.node),

  8.    new LoaderTargetPlugin(options.target)

  9. );

  10. // 其他代码..

  11. compiler.apply(new EntryOptionPlugin());

  12. compiler.applyPluginsBailResult("entry-option", options.context, options.entry);

  13. compiler.apply(

  14.    new CompatibilityPlugin(),

  15.    new HarmonyModulesPlugin(options.module),

  16.    new AMDPlugin(options.module, options.amd || {}),

  17.    new CommonJsPlugin(options.module),

  18.    new LoaderPlugin(),

  19.    new NodeStuffPlugin(options.node),

  20.    new RequireJsStuffPlugin(),

  21.    new APIPlugin(),

  22.    new ConstPlugin(),

  23.    new UseStrictPlugin(),

  24.    new RequireIncludePlugin(),

  25.    new RequireEnsurePlugin(),

  26.    new RequireContextPlugin(options.resolve.modules, options.resolve.extensions, options.resolve.mainFiles),

  27.    new ImportPlugin(options.module),

  28.    new SystemPlugin(options.module)

  29. );

每一个内部插件,都是通过监听任务点的方式,来实现自定义的逻辑。比如 JsonpTemplatePlugin 这个插件,是通过监听 mainTemplate 对象的 require-ensure 任务点,来生成 jsonp 风格的代码:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/JsonpTemplatePlugin.js

  2. mainTemplate.plugin("require-ensure", function(_, chunk, hash) {

  3.    return this.asString([

  4.        "var installedChunkData = installedChunks[chunkId];",

  5.        "if(installedChunkData === 0) {",

  6.        this.indent([

  7.            "return new Promise(function(resolve) { resolve(); });"

  8.        ]),

  9.        "}",

  10.        "",

  11.        "// a Promise means \"currently loading\".",

  12.        "if(installedChunkData) {",

  13.        this.indent([

  14.            "return installedChunkData[2];"

  15.        ]),

  16.        "}",

  17.        "",

  18.        "// setup Promise in chunk cache",

  19.        "var promise = new Promise(function(resolve, reject) {",

  20.        this.indent([

  21.            "installedChunkData = installedChunks[chunkId] = [resolve, reject];"

  22.        ]),

  23.        "});",

  24.        "installedChunkData[2] = promise;",

  25.        "",

  26.        "// start chunk loading",

  27.        "var head = document.getElementsByTagName('head')[0];",

  28.        this.applyPluginsWaterfall("jsonp-script", "", chunk, hash),

  29.        "head.appendChild(script);",

  30.        "",

  31.        "return promise;"

  32.    ]);

  33. });

现在我们理解了 webpack 的基本架构之后,可能会产生疑问,每个插件应该监听哪个对象的哪个任务点,又如何对实现特定功能呢?

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

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

webpack的构建流程

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

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

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

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

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

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

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

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

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

  1. webpack的准备阶段

  2. modules和chunks的生成阶段

  3. 文件生成阶段

webpack的准备阶段

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

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

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js

  2. compiler.apply(new EntryOptionPlugin());

  3. compiler.applyPluginsBailResult("entry-option", options.context, options.entry); // 马上触发任务点运行 EntryOptionPlugin 内部逻辑

  4. // https://github.com/webpack/webpack/blob/master/lib/EntryOptionPlugin.js

  5. module.exports = class EntryOptionPlugin {

  6.    apply(compiler) {

  7.        compiler.plugin("entry-option", (context, entry) => {

  8.            if(typeof entry === "string" || Array.isArray(entry)) {

  9.                compiler.apply(itemToPlugin(context, entry, "main"));

  10.            } else if(typeof entry === "object") {

  11.                Object.keys(entry).forEach(name => compiler.apply(itemToPlugin(context, entry[name], name)));

  12.            } else if(typeof entry === "function") {

  13.                compiler.apply(new DynamicEntryPlugin(context, entry));

  14.            }

  15.            return true;

  16.        });

  17.    }

  18. };

EntryOptionPlugin 的代码只有寥寥数行但是非常重要,它会解析传给 webpack 的配置中的 entry 属性,然后生成不同的插件应用到 Compiler 实例上。这些插件可能是 SingleEntryPlugin, MultiEntryPlugin 或者 DynamicEntryPlugin。但不管是哪个插件,内部都会监听 Compiler 实例对象的 make 任务点,以 SingleEntryPlugin 为例:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/SingleEntryPlugin.js

  2. class SingleEntryPlugin {

  3.    // 其他代码..

  4.    apply(compiler) {

  5.        // 其他代码..

  6.        compiler.plugin("make", (compilation, callback) => {

  7.            const dep = SingleEntryPlugin.createDependency(this.entry, this.name);

  8.            compilation.addEntry(this.context, dep, this.name, callback);

  9.        });

  10.    }

  11. }

这里的 make 任务点将成为后面解析 modules 和 chunks 的起点。

除了 EntryOptionPlugin,其他的内部插件也会监听特定的任务点来完成特定的逻辑,但我们这里不再仔细讨论。当 Compiler 实例加载完内部插件之后,下一步就会直接调用 compiler.run 方法来启动构建,任务点 run 也是在此时触发,值得注意的是此时基本只有 options 属性是解析完成的:

 
   
   
 
  1. // 监听任务点 run

  2. compiler.plugin("run", (compiler, callback) => {

  3.    console.log(compiler.options) // 可以看到解析后的配置

  4.    callback()

  5. })

另外要注意的一点是,任务点 run 只有在 webpack 以正常模式运行的情况下会触发,如果我们以监听(watch)的模式运行 webpack,那么任务点 run 是不会触发的,但是会触发任务点 watch-run

接下来, Compiler 对象会开始实例化两个核心的工厂对象,分别是 NormalModuleFactoryContextModuleFactory。工厂对象顾名思义就是用来创建实例的,它们后续用来创建 NormalModule 以及 ContextModule 实例,这两个工厂对象会在任务点 compile 触发时传递过去,所以任务点 compile 是间接监听这两个对象的任务点的一个入口:

 
   
   
 
  1. // 监听任务点 compile

  2. compiler.plugin("compile", (params) => {

  3.    let nmf = params.normalModuleFactory

  4.    nmf.plugin("before-resolve", (data, callback) => {

  5.        // ...

  6.    })

  7. })

下一步 Compiler 实例将会开始创建 Compilation 对象,这个对象是后续构建流程中最核心最重要的对象,它包含了一次构建过程中所有的数据。也就是说一次构建过程对应一个 Compilation 实例。在创建 Compilation 实例时会触发任务点 compilaiionthis-compilation

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/Compiler.js

  2. class Compiler extends Tapable {

  3.    // 其他代码..

  4.    newCompilation(params) {

  5.        const compilation = this.createCompilation();

  6.        compilation.fileTimestamps = this.fileTimestamps;

  7.        compilation.contextTimestamps = this.contextTimestamps;

  8.        compilation.name = this.name;

  9.        compilation.records = this.records;

  10.        compilation.compilationDependencies = params.compilationDependencies;

  11.        this.applyPlugins("this-compilation", compilation, params);

  12.        this.applyPlugins("compilation", compilation, params);

  13.        return compilation;

  14.    }

  15. }

这里为什么会有 compilationthis-compilation 两个任务点?其实是跟子编译器有关, Compiler 实例通过 createChildCompiler 方法可以创建子编译器实例 childCompiler,创建时 childCompiler 会复制 compiler 实例的任务点监听器。任务点 compilation 的监听器会被复制,而任务点 this-compilation的监听器不会被复制。 更多关于子编译器的内容,将在下一篇文章中讨论。

compilationthis-compilation 是最快能够获取到 Compilation 实例的任务点,如果你的插件功能需要尽早对 Compilation 实例进行一些操作,那么这两个任务点是首选:

 
   
   
 
  1. // 监听 this-compilation 任务点

  2. compiler.plugin("this-compilation", (compilation, params) => {

  3.    console.log(compilation.options === compiler.options) // true

  4.    console.log(compilation.compiler === compiler) // true

  5.    console.log(compilation)

  6. })

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

 
   
   
 
  1. // 监听 finish-modules 任务点

  2. compiler.plugin("this-compilation", (compilation) => {

  3.    compilation.plugin("finish-modules", (modules) => {

  4.        console.log(modules === compilation.modules) // true

  5.        modules.forEach(module => {

  6.            console.log(module._source.source()) // 处理后的源码

  7.        })

  8.    })

  9. })

下面将以 NormalModule 为例讲解一下 module 的解析过程, ContextModule 等其他模块实例的处理是类似的。 第一个步骤是创建 NormalModule 实例。这里需要用到上一个阶段讲到的 NormalModuleFactory 实例, NormalModuleFactorycreate 方法是创建 NormalModule 实例的入口,内部的主要过程是解析 module 需要用到的一些属性,比如需要用到的 loaders, 资源路径 resource 等等,最终将解析完毕的参数传给 NormalModule 构建函数直接实例化:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/NormalModuleFactory.js

  2. // 以 require("raw-loader!./a") 为例

  3. // 并且对 .js 后缀配置了 babel-loader

  4. createdModule = new NormalModule(

  5.    result.request,      // ! !/path/to/a.js

  6.    result.userRequest,  // !/path/to/a.js

  7.    result.rawRequest,   // raw-loader!./a.js

  8.    result.loaders,      // [ , ]

  9.    result.resource,     // /path/to/a.js

  10.    result.parser

  11. );

这里在解析参数的过程中,有两个比较实用的任务点 before-resolveafter-resolve,分别对应了解析参数前和解析参数后的时间点。举个例子,在任务点 before-resolve 可以做到忽略某个 module 的解析,webpack 内部插件 IgnorePlugin 就是这么做的:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/IgnorePlugin.js

  2. class IgnorePlugin {

  3.    checkIgnore(result, callback) {

  4.        // check if result is ignored

  5.        if(this.checkResult(result)) {

  6.            return callback(); // callback第二个参数为 undefined 时会终止module解析

  7.        }

  8.        return callback(null, result);

  9.    }

  10.    apply(compiler) {

  11.        compiler.plugin("normal-module-factory", (nmf) => {

  12.            nmf.plugin("before-resolve", this.checkIgnore);

  13.        });

  14.        compiler.plugin("context-module-factory", (cmf) => {

  15.            cmf.plugin("before-resolve", this.checkIgnore);

  16.        });

  17.    }

  18. }

在创建完 NormalModule 实例之后会调用 build 方法继续进行内部的构建。我们熟悉的 loaders 将会在这里开始应用, NormalModule 实例中的 loaders 属性已经记录了该模块需要应用的 loaders。应用 loaders 的过程相对简单,直接调用了 loader-runner 这个模块,可自行查阅其源码:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/NormalModule.js

  2. const runLoaders = require("loader-runner").runLoaders;

  3. // 其他代码..

  4. class NormalModule extends Module {

  5.    // 其他代码..

  6.    doBuild(options, compilation, resolver, fs, callback) {

  7.        this.cacheable = false;

  8.        const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);

  9.        runLoaders({

  10.            resource: this.resource,

  11.            loaders: this.loaders,

  12.            context: loaderContext,

  13.            readResource: fs.readFile.bind(fs)

  14.        }, (err, result) => {

  15.            // 其他代码..

  16.        });

  17.    }

  18. }

webpack 中要求 NormalModule 最终都是 js 模块,所以 loader 的作用之一是将不同的资源文件转化成 js 模块。比如 html-loader 是将 html 转化成一个 js 模块。在应用完 loaders 之后, NormalModule 实例的源码必然就是 js 代码,这对下一个步骤很重要。

下一步我们需要得到这个 module 所依赖的其他模块,所以就有一个依赖收集的过程。webpack 的依赖收集过程是将 js 源码传给 js parser(webpack 使用的 parser 是 acorn):

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/NormalModule.js

  2. class NormalModule extends Module {

  3.    // 其他代码..

  4.    build(options, compilation, resolver, fs, callback) {

  5.        // 其他代码..

  6.        return this.doBuild(options, compilation, resolver, fs, (err) => {

  7.            // 其他代码..

  8.            try {

  9.                this.parser.parse(this._source.source(), {

  10.                    current: this,

  11.                    module: this,

  12.                    compilation: compilation,

  13.                    options: options

  14.                });

  15.            } catch(e) {

  16.                const source = this._source.source();

  17.                const error = new ModuleParseError(this, source, e);

  18.                this.markModuleAsErrored(error);

  19.                return callback();

  20.            }

  21.            return callback();

  22.        });

  23.    }

  24. }

parser 将 js 源码解析后得到对应的AST(抽象语法树, Abstract Syntax Tree)。然后 webpack 会遍历 AST,按照一定规则触发任务点。 比如 js 源码中有一个表达式: a.b.c,那么 parser 对象就会触发任务点 expression a.b.c。更多相关的规则 webpack 在官网有罗列出来,大家可以对照着使用。

有了AST对应的任务点,依赖收集就相对简单了,比如遇到任务点 callrequire,说明在代码中是有调用了 require函数,那么就应该给 module 添加新的依赖。webpack 关于这部分的处理是比较复杂的,因为 webpack 要兼容多种不同的依赖方式,比如 AMD 规范、CommonJS规范,然后还要区分动态引用的情况,比如使用了 require.ensure, require.context。但这些细节对于我们讨论构建流程并不是必须的,因为不展开细节讨论。

parser 解析完成之后,module 的解析过程就完成了。每个 module 解析完成之后,都会触发 Compilation 实例对象的任务点 succeed-module,我们可以在这个任务点获取到刚解析完的 module 对象。正如前面所说,module 接下来还要继续递归解析它的依赖模块,最终我们会得到项目所依赖的所有 modules。此时任务点 make 结束。

继续往下走, Compialtion 实例的 seal 方法会被调用并马上触发任务点 seal。在这个任务点,我们可以拿到所有解析完成的 module:

 
   
   
 
  1. // 监听 seal 任务点

  2. compiler.plugin("this-compilation", (compilation) => {

  3.    console.log(compilation.modules.length === 0) // true

  4.    compilation.plugin("seal", () => {

  5.        console.log(compilation.modules.length > 0) // true

  6.    })

  7. })

有了所有的 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、排序等,并且触发一系列相关的任务点:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/Compilation.js

  2. class Compilation extends Tapable {

  3.    // 其他代码 ..

  4.    seal(callback) {

  5.        // 生成 chunks 代码..

  6.        self.applyPlugins0("optimize");

  7.        while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) ||

  8.            self.applyPluginsBailResult1("optimize-modules", self.modules) ||

  9.            self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)) { /* empty */ }

  10.        self.applyPlugins1("after-optimize-modules", self.modules);

  11.        while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) ||

  12.            self.applyPluginsBailResult1("optimize-chunks", self.chunks) ||

  13.            self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)) { /* empty */ }

  14.        self.applyPlugins1("after-optimize-chunks", self.chunks);

  15.        self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) {

  16.            if(err) {

  17.                return callback(err);

  18.            }

  19.            self.applyPlugins2("after-optimize-tree", self.chunks, self.modules);

  20.            while(self.applyPluginsBailResult("optimize-chunk-modules-basic", self.chunks, self.modules) ||

  21.                self.applyPluginsBailResult("optimize-chunk-modules", self.chunks, self.modules) ||

  22.                self.applyPluginsBailResult("optimize-chunk-modules-advanced", self.chunks, self.modules)) { /* empty */ }

  23.            self.applyPlugins2("after-optimize-chunk-modules", self.chunks, self.modules);

  24.            const shouldRecord = self.applyPluginsBailResult("should-record") !== false;

  25.            self.applyPlugins2("revive-modules", self.modules, self.records);

  26.            self.applyPlugins1("optimize-module-order", self.modules);

  27.            self.applyPlugins1("advanced-optimize-module-order", self.modules);

  28.            self.applyPlugins1("before-module-ids", self.modules);

  29.            self.applyPlugins1("module-ids", self.modules);

  30.            self.applyModuleIds();

  31.            self.applyPlugins1("optimize-module-ids", self.modules);

  32.            self.applyPlugins1("after-optimize-module-ids", self.modules);

  33.            self.sortItemsWithModuleIds();

  34.            self.applyPlugins2("revive-chunks", self.chunks, self.records);

  35.            self.applyPlugins1("optimize-chunk-order", self.chunks);

  36.            self.applyPlugins1("before-chunk-ids", self.chunks);

  37.            self.applyChunkIds();

  38.            self.applyPlugins1("optimize-chunk-ids", self.chunks);

  39.            self.applyPlugins1("after-optimize-chunk-ids", self.chunks);

  40.            // 其他代码..

  41.        })

  42.    }

  43. }

这些任务点一般是 webpack.optimize 属性下的插件会使用到,比如 CommonsChunkPlugin 会使用到任务点 optimize-chunks,但这里我们不深入讨论。

至此,modules 和 chunks 的生成阶段结束。接下来是文件生成阶段。

文件生成阶段

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

Compilation 在实例化的时候,就会同时实例化三个对象: MainTemplate, ChunkTemplateModuleTemplate。这三个对象是用来渲染 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 也是类似的原理:

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/Compilation.js

  2. class Compilation extends Tapable {

  3.    // 其他代码..

  4.    createHash() {

  5.        // 其他代码..

  6.        const hash = crypto.createHash(hashFunction);

  7.        if(outputOptions.hashSalt)

  8.        hash.update(outputOptions.hashSalt);

  9.        this.mainTemplate.updateHash(hash);

  10.        this.chunkTemplate.updateHash(hash);

  11.        this.moduleTemplate.updateHash(hash);

  12.        // 其他代码..

  13.        for(let i = 0; i < chunks.length; i++) {

  14.            const chunk = chunks[i];

  15.            const chunkHash = crypto.createHash(hashFunction);

  16.            if(outputOptions.hashSalt)

  17.            chunkHash.update(outputOptions.hashSalt);

  18.            chunk.updateHash(chunkHash);

  19.            if(chunk.hasRuntime()) {

  20.                this.mainTemplate.updateHashForChunk(chunkHash, chunk);

  21.            } else {

  22.                this.chunkTemplate.updateHashForChunk(chunkHash, chunk);

  23.            }

  24.            this.applyPlugins2("chunk-hash", chunk, chunkHash);

  25.            chunk.hash = chunkHash.digest(hashDigest);

  26.            hash.update(chunk.hash);

  27.            chunk.renderedHash = chunk.hash.substr(0, hashDigestLength);

  28.        }

  29.        this.fullHash = hash.digest(hashDigest);

  30.        this.hash = this.fullHash.substr(0, hashDigestLength);

  31.    }

  32. }

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

 
   
   
 
  1. // https://github.com/webpack/webpack/blob/master/lib/Compilation.js

  2. class Compilation extends Tapable {

  3.    // 其他代码..

  4.    createChunkAssets() {

  5.        // 其他代码..

  6.        for(let i = 0; i < this.chunks.length; i++) {

  7.            const chunk = this.chunks[i];

  8.            // 其他代码..

  9.            if(chunk.hasRuntime()) {

  10.                source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);

  11.            } else {

  12.                source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);

  13.            }

  14.            file = this.getPath(filenameTemplate, {

  15.                noChunkHash: !useChunkHash,

  16.                chunk

  17.            });

  18.            this.assets[file] = source;

  19.            // 其他代码..

  20.        }

  21.    }

  22. }

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

MainTemplate 的渲染跟 ChunkTemplate 的不同点在于,入口 chunk 的源码中会带有启动 webpack 的代码,而非入口 chunk 的源码是不需要的。这个只要查看 webpack 构建后的文件就可以比较清楚地看到区别:

 
   
   
 
  1. // 入口 chunk

  2. /******/ (function(modules) { // webpackBootstrap

  3. /******/     // install a JSONP callback for chunk loading

  4. /******/     var parentJsonpFunction = window["webpackJsonp"];

  5. /******/     window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {

  6. /******/         // add "moreModules" to the modules object,

  7. /******/         // then flag all "chunkIds" as loaded and fire callback

  8. /******/         var moduleId, chunkId, i = 0, resolves = [], result;

  9. /******/         for(;i < chunkIds.length; i++) {

  10. /******/             chunkId = chunkIds[i];

  11. /******/             if(installedChunks[chunkId]) {

  12. /******/                 resolves.push(installedChunks[chunkId][0]);

  13. /******/             }

  14. /******/             installedChunks[chunkId] = 0;

  15. /******/         }

  16. /******/         for(moduleId in moreModules) {

  17. /******/             if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {

  18. /******/                 modules[moduleId] = moreModules[moduleId];

  19. /******/             }

  20. /******/         }

  21. /******/         if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);

  22. /******/         while(resolves.length) {

  23. /******/             resolves.shift()();

  24. /******/         }

  25. /******/        

  26. /******/     };

  27. /******/     // 其他代码..

  28. /******/ })(/* modules代码 */);


  29. // 非入口 chunk

  30. webpackJsonp([0],[

  31.    /* modules代码.. */

  32. ]);

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

assets 对象的 key 是最终要生成的文件名称,因此这里要用到前面创建的 hash。调用 Compilation 实例内部的 getPath 方法会根据配置中的 output.filename 来生成文件名称。

assets 对象的 value 是一个对象,对象需要包含两个方法, sourcesize 分别返回文件内容和文件大小。

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

完成上面的操作之后, Compilation 实例的 seal 方法结束,进入到 Compiler 实例的 emitAssets 方法。 Compilation 实例的所有工作到此也全部结束,意味着一次构建过程已经结束,接下来只有文件生成的步骤。

Compiler 实例开始生成文件前,最后一个修改最终文件生成的任务点 emit 会被触发:

 
   
   
 
  1. // 监听 emit 任务点,修改最终文件的最后机会

  2. compiler.plugin("emit", (compilation, callback) => {

  3.    let data = "abcd"

  4.    compilation.assets["newFile.js"] = {

  5.        source() {

  6.            return data

  7.        }

  8.        size() {

  9.            return data.length

  10.        }

  11.    }

  12. })

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

总结

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

下一篇文章将会讲解 webpack 核心的对象,敬请期待。


以上是关于玩转webpack:webpack的基本架构和构建流程的主要内容,如果未能解决你的问题,请参考以下文章

玩转webpack:webpack的核心对象

玩转 webpack视频课程学习笔记合集(未完结)

webpack原理篇(五十六):webpack流程:模块构建和chunk生成阶段

webpack配置篇(三十三):冒烟测试介绍和实际运用

webpack原理篇(五十三):Tapable插件架构与Hooks设计

2 webpack 进阶用法