美文网首页
浅析 webpack 打包流程(原理) 五 - 构建资源

浅析 webpack 打包流程(原理) 五 - 构建资源

作者: AizawaSayo | 来源:发表于2021-07-22 16:56 被阅读0次

    接上文:浅析 webpack 打包流程(原理) 四 - chunk 优化

    七、构建资源

    本阶段概述:
    1.获取 compilation 每个 module 的 buildInfo.assets,然后调用 this.emitAsset 生成 module 资源;
    2.遍历compilation.chunks生成 chunk 资源时,先根据是否含有 runtime (webpackBootstrap 代码) 选择不同的 template (有则 mainTemplate,不然一概 chunkTemplate),得到各自的 manifest 数据pathAndInfo,然后调用不同的 render 渲染代码;
    3.最后建立文件名与资源之间的映射,并将得到的所有目标资源信息挂载到compilation.assets上。
    4.如果有配置诸如terser-webpack-plugin的代码压缩插件(一般都有),在optimizeChunkAssets钩子触发后,对生成的资源根据seal阶段(生成chunk之前)做的标记进行 treeshaking。

    7.1 生成 module 资源

    继续执行:

    // /lib/Compilation.js
    this.hooks.beforeModuleAssets.call();
    this.createModuleAssets();
    

    createModuleAssets 方法获取每个 module 的 buildInfo.assets,然后触发compilation.emitAsset生成 module 资源,得到的相关数据存储在compilationassetsassetsInfo。buildInfo.assets 相关数据可在 loader 里调用 Api: this.emitFile 来生成 (loaderContext.emitFile 方法,详见/lib/NormalModule.js)。

    7.2 生成 chunk 资源

    7.2.1 生成前的准备

    得到 manifest 数据对象

    当 compiler 处理、解析和映射应用代码时,manifest 会记录每个模块的详细要点(如 module identifier、路径等)。程序运行时,runtime 会根据 manifest 中的数据来解析和加载模块:__webpack_require__方法接收模块标识符(identifier)作为参数,简称 moduleId,通过这个标识符可以检索出 manifest 集合中对应的模块。

    // /lib/Compilation.js
    this.hooks.beforeChunkAssets.call();
    this.createChunkAssets();
    

    createChunkAssets 方法循环对每个 chunk 执行:

    // /lib/Compilation.js
    createChunkAssets() {
      // ...
      for (let i = 0; i < this.chunks.length; i++) {
        const chunk = this.chunks[i];
        // 判断 chunk 是否包含 runtime 代码,获取到对应的 template
        const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate;
        // 得到相应的 manifest 数据对象
        const manifest = template.getRenderManifest({
          chunk,
          hash: this.hash,
          fullHash: this.fullHash,
          outputOptions,
          moduleTemplates: this.moduleTemplates,
          dependencyTemplates: this.dependencyTemplates,
    }); // manifest 为 `render` 所需要的全部信息:`[{ render(), filenameTemplate, pathOptions, identifier, hash }]`
        // ...
      }
    }
    

    判断 chunk 是否含有 runtime 代码 (它所属的 chunkGroup 是否是初始的那个 EntryPoint,chunkGroup.runtimeChunk 是否就是当前 chunk),从而获取到对应的 template:默认情况下包含 runtime 和同步依赖的 入口 chunk 对应 mainTemplate异步 chunk 对应 chunkTemplate

    默认配置下,即未手动抽离 runtime 和 配置 splitChunks,同步依赖都会被合并在入口 chunk 中,并且从属于一个 EntryPoint(继承自chunkGroup)。而每个异步模块都会单独生成一个 chunk 和 chunkGroup。
    一旦从入口 chunk 单独抽出 runtime chunk,则只有 runtime chunk 由mainTemplate渲染,其余都属于由chunkTemplate渲染的普通 chunk 了(无论同步异步)。

    然后执行对应的 getRenderManifest,触发template.hooks:renderManifest,执行插件 JavascriptModulesPlugin 相关事件:
    (如果有配置 MiniCssExtractPlugin 插件,也会在此时执行从当前 chunk 分离出所有 CssModule [thisCompilation时生成],并在当前 chunk 清单文件中添加一个单独的 css 文件,即抽离 css 样式到单独的*.css)

    // /lib/JavascriptModulesPlugin.js
    // 运行时 chunk (默认是入口 chunk) 相关的插件事件
    compilation.mainTemplate.hooks.renderManifest.tap(...) 
    // 普通 chunk 相关的插件事件
    compilation.chunkTemplate.hooks.renderManifest.tap(  
      "JavascriptModulesPlugin",
      (result, options) => {
        // ...
        result.push({
          render: () =>
            this.renderJavascript(
              compilation.chunkTemplate,
              chunk,
              moduleTemplates.javascript,
              dependencyTemplates
            ),
           filenameTemplate,
           pathOptions: {
             chunk,
             contentHashType: "javascript"
           },
           identifier: `chunk${chunk.id}`,
           hash: chunk.hash
        });
        return result;
      }
    );
    

    得到 manifest 即 render 所需要的全部信息:[{ render(), filenameTemplate, pathOptions, identifier, hash }]
    如果是 chunkTemplate 还会触发插件 WebAssemblyModulesPlugin 去处理 WebAssembly 相关。

    得到 pathAndInfo

    然后遍历 manifest 数组,在里面执行:

    // /lib/Compilation.js
    const pathAndInfo = this.getPathWithInfo(
      filenameTemplate,
      fileManifest.pathOptions
    );
    

    getPathWithInfo 用于得到路径和相关信息,会触发mainTemplate.hooks: assetPath,去执行插件 TemplatedPathPlugin 相关事件,使用若干 replace 将如[name].[chunkhash:8].js替换为0.c7687fbe.js

    7.2.2 构建资源

    然后判断有无 source 缓存,若无则执行:source = fileManifest.render();
    即执行 manifest 每一项里的 render 函数。

    (1) chunkTemplate
    生成主体 chunk 代码

    如果是普通 chunk,render 会调用 JavascriptModulesPlugin.js 插件里的renderJavascript方法:先执行 Template.renderChunkModules 静态方法:

    // /lib/JavascriptModulesPlugin.js
    renderJavascript(chunkTemplate, chunk, moduleTemplate, dependencyTemplates) {
      // 生成每个 module 代码
      const moduleSources = Template.renderChunkModules(
        chunk,
        m => typeof m.source === "function",
        moduleTemplate,
        dependencyTemplates
      );
    }
    
    renderChunkModules 生成每个 module 代码
    // /lib/Template.js
    static renderChunkModules(chunk, filterFn, moduleTemplate, dependencyTemplates, prefix = "" ) {
      const allModules = modules.map((module) => {
        return {
          id: module.id,
          // ModuleTemplate.js 的 render 方法,循环对每一个 module 执行 render
          source: moduleTemplate.render(module, dependencyTemplates, { chunk }),
        };
      });
    }
    
    // /lib/ModuleTemplate.js 的 render 方法
    const moduleSource = module.source(
      dependencyTemplates,
      this.runtimeTemplate,
      this.type
    );
    

    module.source/lib/NormalModule.js的 source 方法,内部执行:const source = this.generator.generate(this, dependencyTemplates, runtimeTemplate, type);
    这个 generator 就是在 reslove 流程 ➡️ getGenerator 所获得,即在/lib/JavascriptGenerator.js执行:
    this.sourceBlock(module, module, [], dependencyTemplates, source, runtimeTemplate);

    这里循环处理 module 的每个依赖(module.dependencies),获得依赖所对应的 template 模板类,然后执行该类的 apply:

    // /lib/JavascriptGenerator.js
    const template = dependencyTemplates.get(dependency.constructor);
    template.apply(dependency, source, runtimeTemplate, dependencyTemplates);
    

    这里的 dependencyTemplates 就是在【浅析 webpack 打包流程(原理) 一 - 准备工作】 ➡️ 实例化 compilation 时添加的依赖模板模块。
    template.apply会根据依赖的不同做相应的源码转化处理。但方法里并没有直接执行源码转化,而是将其转化对象 push 到ReplaceSource.replacements里,转化对象的格式为:

    注:webpack-sources 提供若干类型的 source 类,如 CachedSource、PrefixSource、ConcatSource、ReplaceSource 等。它们可以组合使用,方便对代码进行添加、替换、连接等操作。同时又含有一些 source-map 相关、updateHash 等 Api 供 webpack 内部调用。

    // Replacement
    {
      "content": "__webpack_require__.r(__webpack_exports__);\n", // 替换的内容
      "end": -11, // 替换源码的终止位置
      "insertIndex": 0, // 优先级
      "name": undefined, // 名称
      "start": -10 // 替换源码的起始位置
    }
    

    各模版的转化处理见【浅析 webpack 打包流程(原理) 二 - 递归构建 module】 最末:各依赖作用简单解释。

    包裹代码

    收集完依赖相关的转化对象 Replacement 后,对得到的结果进行cachedSource缓存包装,回到 ModuleTemplate.js 的 render 方法得到 moduleSource。
    然后触发ModuleTemplate.hooks:content、module、render、package,content、module 钩子主要是可以让我们完成对 module 源码的再次处理;然后在 render 钩子里执行插件 FunctionModuleTemplatePlugin 的订阅事件:对处理后的 module 源码进行包裹,即生成代码:

    /***/
    (function (module, __webpack_exports__, __webpack_require__) {
      'use strict';
      // children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
      /***/
    });
    
    拿到的 moduleSourcePostRender 值
    添加注释

    再触发 package 钩子执行插件 FunctionModuleTemplatePlugin 的订阅事件,主要是添加相关注释,即生成代码:

    /*!***************************************************************!*\
      !*** ./src/c.js ***!
      \***************************************************************/
    /*! exports provided: sub */
    /***/
    (function (module, __webpack_exports__, __webpack_require__) {
      'use strict';
      // children 数组中的 CachedSource 即为`module`源码,里面包含 replacements
      /***/
    });
    

    将所有的 module 都处理完毕后,回到 Template.js 的 renderChunkModules 继续处理生成代码,最终将每个 module 生成的代码串起来回到 JavascriptModulesPlugin.js 的 renderJavascript 方法里得到了 moduleSources。

    生成 jsonp 包裹代码

    接着触发chunkTemplate.hooks: modules,为修改生成的 chunk 代码提供钩子。得到 core 后,触发chunkTemplate.hooks: render执行插件 JsonpChunkTemplatePlugin.js 订阅事件,主要是添加 jsonp 包裹代码,得到:

    美化一下就和我们常见的打包文件无异了:

    (window['webpackJsonp'] = window['webpackJsonp'] || []).push([
      [0], 
      {
      // 前面生成的 chunk 代码
    /***/ "./src/c.js":
    /*!******************!*\
      !*** ./src/c.js ***!
      \******************/
    /*! exports provided: sub */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    /***/ })
      }
    ]);
    

    最后 return 一个new ConcatSource(source, ";")。至此普通 (非初始) chunk 代码 chunkTemplate 的fileManifest.render(/lib/Compilation.js 中) 构建完成。
    注意:非初始不代表异步,当入口 chunk 被拆成多个同步 chunk,初始 chunk 就指代包含 runtime 的那个 chunk。
    可以配合【webpack 模块加载原理及打包文件分析 (一)】服用,便能清晰知晓window['webpackJsonp'].push不光用来加载异步 chunk。

    (2) mainTemplate

    如果是初始 chunk,render 会执行 JavascriptModulesPlugin.js 里的 compilation.mainTemplate.render 即/lib/MainTemplate.js的 render 方法。

    生成 runtime 代码

    内部执行:const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates);
    得到 webpack runtime bootstrap 代码数组,过程中会判断 chunks 中是否存在异步 chunk,如果有,则代码里还会包含异步相关的 runtime 代码。如果还有延迟加载的同步 chunk,都会在这里处理为相应的 runtime。

    包裹 runtime 与 chunk 代码

    然后执行:

    let source = this.hooks.render.call(
      new OriginalSource(
        Template.prefix(buf, " \t") + "\n",
        "webpack/bootstrap"
      ),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates
    );
    

    先通过 Template.js 的 prefix 方法合并 runtime 代码字符串,得到 OriginalSource 实例,然后将其作为参数执行MainTemplate.hooks: render,该 hook 事件注册在 MainTemplate 自身的 constructor 中,代码如下:

    const source = new ConcatSource();
    source.add('/******/ (function(modules) { // webpackBootstrap\n');
    source.add(new PrefixSource('/******/', bootstrapSource));
    source.add('/******/ })\n');
    source.add('/************************************************************************/\n');
    source.add('/******/ (');
    source.add(this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates));
    source.add(')');
    return source;
    

    对 runtime bootstrap 代码进行了包装 (bootstrapSource 即前面生成的 runtime 代码),过程中触发MainTemplate.hooks: modules得到 chunk 的生成代码,即最终返回一个包含 runtime 代码和 chunk 代码的 ConcatSource 实例。

    生成 chunk 代码

    这里来看通过this.hooks.modules.call()钩子得到 chunk 生成代码的实现:
    触发插件 JavascriptModulesPlugin 的注册事件,即执行 Template 类的静态方法renderChunkModules。与前文 chunkTemplate ➡️ 生成主体 chunk 代码的实现一致。

    最终经过包裹后得到的代码大致如下:

    "/******/ (function(modules) { // webpackBootstrap
    // runtime 代码的 PrefixSource 实例
    /******/ })
    /************************************************************************/
    /******/ ({
    
    /***/ "./src/a.js":
    /*!******************!*\
      !*** ./src/a.js ***!
      \******************/
    /*! no exports provided */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    // module a 的 CachedSource 实例
    
    /***/ }),
    
    /***/ "./src/b.js":
    /*!******************!*\
      !*** ./src/b.js ***!
      \******************/
    /*! exports provided: add, unusedAdd */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    // module b 的 CachedSource 实例
    
    /***/ }),
    
    /***/ "./src/d.js":
    /*!******************!*\
      !*** ./src/d.js ***!
      \******************/
    /*! exports provided: default */
    /***/ (function(module, __webpack_exports__, __webpack_require__) {
    
    "use strict";
    // module d 的 CachedSource 实例
    
    /***/ })
    
    /******/ })"
    

    最后返回一个new ConcatSource(source, ";")。至此入口 chunk (包含 runtime 的 chunk) 代码 mainTemplate 的fileManifest.render(/lib/Compilation.js 中) 构建完成。

    7.2.3 文件名映射资源

    接下来,无论是包含 runtime 的主 chunk 还是普通 chunk,都回到 Compilation.js 的 createChunkAssets 方法,在compilation.cache[cacheName]做了 source 缓存,然后执行:

    this.emitAsset(file, source, assetInfo);
    chunk.files.push(file);
    

    建立起文件名与对应源码的联系,以this.assets[file] = source的形式将该映射对象挂载到 compilation.assets 下。 把文件名称存入对应的 chunk.files 数组中,即compilation.chunks下。然后设置了 alreadyWrittenFiles (Map 对象),以防重复构建代码。至此一个 chunk 的资源构建结束。

    所有 chunk 遍历结束后,得到的compilation.assetscompilation.assetsInfo:

    // compilation
    {
      //...
      "assets": {
        "0.92bfd615.js": CachedSource, // CachedSource 里包含 chunk 资源
        "index.1d678a.js": CachedSource
      },
      "assetsInfo": { // Map结构
        0: {
          "key": '0.92bfd615.js',
          "value": {
            immutable:true
          }
        },
        1: {
          "key": 'index.1d678a.js',
          "value": {
            immutable:true
          }
        }
      }
    }
    
    compilation.assets 和 compilation.assetsInfo compilation.assets[chunkName]._source

    7.3 对生成资源进行 TreeShaking (需配置相应插件)

    compilation.hooks.additionalAssets钩子触发后,如果有配置进一步处理生成资源的插件,则会对资源再度优化。
    比如compilation.hooks.optimizeChunkAssets会触发terser-webpack-plugin代码压缩插件(一般都会配置,webpack 5 内置),对生成的 chunk 资源根据(seal阶段compilation.hooks.optimizeDependencies)生成的unused harmony export标记等信息进行 treeshaking

    下文:浅析 webpack 打包流程(原理) 六 - 生成文件

    webpack 打包流程系列(未完):
    浅析 webpack 打包流程(原理) - 案例 demo
    浅析 webpack 打包流程(原理) 一 - 准备工作
    浅析 webpack 打包流程(原理) 二 - 递归构建 module
    浅析 webpack 打包流程(原理) 三 - 生成 chunk
    浅析 webpack 打包流程(原理) 四 - chunk 优化
    浅析 webpack 打包流程(原理) 五 - 构建资源
    浅析 webpack 打包流程(原理) 六 - 生成文件

    参考鸣谢:
    webpack打包原理 ? 看完这篇你就懂了 !
    webpack 透视——提高工程化(原理篇)
    webpack 透视——提高工程化(实践篇)
    webpack 4 源码主流程分析
    [万字总结] 一文吃透 Webpack 核心原理
    关于Webpack详述系列文章 (第三篇)

    相关文章

      网友评论

          本文标题:浅析 webpack 打包流程(原理) 五 - 构建资源

          本文链接:https://www.haomeiwen.com/subject/vyvmeltx.html