阅读 127

webpack 4 源码主流程分析(七):构建 module(下)

原文首发于 blog.flqin.com。如有错误,请联系笔者。分析码字不易,转载请表明出处,谢谢!

parse 源码

runLoaders 运行结束后,在回调里执行了 createSource 后,判断 loaderresult 是否有第三个参数对象并且里面存在 webpackAST 属性,如果有则为 ast 赋值到 _ast 上。

然后回到 this.doBuild 执行回调,在根据项目配置项判断是否需要 parse 后,若需要解析,则执行:

const result = this.parser.parse(
  this._ast || this._source.source(),
  {
    current: this,
    module: this,
    compilation: compilation,
    options: options
  },
  (err, result) => {
    //...
  }
);
if (result !== undefined) {
  // parse is sync
  handleParseResult(result);
}
复制代码

this.parser 即是在 reslove 流程 里的组合对象里得到的 parser

this.parser.parse,在该方法里如果 this._ast 不存在则传 this._source._value 即代码字符串。 然后进入文件 node_modules/webpack/lib/Parser.js 执行 Parser.parse

parser.parse

析出 ast

方法里执行:

ast = Parser.parse(source, {
  sourceType: this.sourceType,
  onComment: comments
});
复制代码

Parser.parse 即为 Parser 静态方法,该方法里主要执行:

ast = acornParser.parse(code, parserOptions); //即 acorn.Parser
复制代码

webpack 通过 acorn 得到源码对应的 astast 相关资料:

遍历 ast 收集依赖

回到 Parser.parse ,对 ast 进行遍历,执行:

if (this.hooks.program.call(ast, comments) === undefined) {
  this.detectStrictMode(ast.body);
  this.prewalkStatements(ast.body);
  this.blockPrewalkStatements(ast.body);
  this.walkStatements(ast.body);
}
复制代码
  • this.hooks.program.call(ast, comments)

    触发回调 plugin(HarmonyDetectionParserPluginUseStrictPlugin) 根据是否有 import/exportuse strict 增加依赖:HarmonyCompatibilityDependency, HarmonyInitDependencyConstDependency

  • this.detectStrictMode(ast.body)

    检测当前执行块是否有 use strict,并设置 this.scope.isStrict = true

  • this.prewalkStatements(ast.body)

    • 处理 import 进来的变量,是 import 就增加依赖 HarmonyImportSideEffectDependencyHarmonyImportSpecifierDependency;
    • 处理 export 出去的变量,是 export 增加依赖 HarmonyExportHeaderDependencyHarmonyExportSpecifierDependency
    • 还会处理其他相关导入导出的变量
  • this.blockPrewalkStatements(ast.body)

    处理块遍历

  • this.walkStatements(ast.body)

    用于深入函数内部(方法在 walkFunctionDeclaration 进行递归),然后递归继续查找 ast 上的依赖,异步此处深入会增加依赖 ImportDependenciesBlock;

上述执行结束后,会根据 import/export 的不同情况即模块间的相互依赖关系,在对应的 module.dependencies 上增加相应的依赖。

各依赖作用解释

在后面 generaterender 阶段,调用这些依赖(Dependency)对应的 template.apply 来渲染生成代码资源。

demo 入口文件 a.jsc.js 为例,则依赖为:

//a.js module
{
  "dependencies": [
    "HarmonyCompatibilityDependency", //对应模板 `HarmonyExportDependencyTemplate` 会在源码里最前面添加如:`__webpack_require__.r(__webpack_exports__);` 的代码,用于定义 exports:__esModule
    "HarmonyInitDependency", // 对应模板 `HarmonyInitDependencyTemplate`, 下文单独说明其作用
    "ConstDependency", // 对应模板 `ConstDependencyTemplate` 操作会在源码里将同步 import 语句删掉
    "HarmonyImportSideEffectDependency", //对应模板 `HarmonyImportSideEffectDependencyTemplate`,执行 apply 调用父类 HarmonyImportDependencyTemplate 的 apply,即为空。
    "HarmonyImportSpecifierDependency" //对应模板 `HarmonyImportSpecifierDependencyTemplate`,会在源码里将引入的变量替换为 webpack 对应的包装变量
  ],
  "blocks": ["ImportDependenciesBlock"] //异步模块  对应模板 `ImportDependencyTemplate`, 会在源码里将本 demo 中的 `import('./c.js')`替换为 `Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))`
}
复制代码
//d.js module
{
  "dependencies": [
    "HarmonyCompatibilityDependency",
    "HarmonyInitDependency",
    "HarmonyExportHeaderDependency", // 对应模板 `HarmonyExportDependencyTemplate` 会在源码里将关键字 export 删掉
    "HarmonyExportSpecifierDependency" //对应模板 `HarmonyExportSpecifierDependencyTemplate` 执行 apply 为空
  ],
  "blocks": []
}
复制代码

HarmonyInitDependency

执行其对应 template.apply(文件 HarmonyInitDependency.js)中,先遍历 module.dependencies,判断各依赖对应的 template 是否包含 harmonyInitgetHarmonyInitOrder 函数(用于导入的 import 排序),若都存在,则执行:

const order = template.getHarmonyInitOrder(dependency);
复制代码

执行对应的 template.getHarmonyInitOrder 用于获取排序的 order,在不同的依赖里根据需要可能会返回 NaN(如 HarmonyImportSideEffectDependencyTemplate 里判断无副作用(sideEffects)就会返回 NaN),最终筛选出不是 NaN 的依赖组成数组 list,即为含有 importexport 的依赖,按 order 排序后, 执行:

for (const item of list) {
  item.template.harmonyInit(item.dependency, source, runtime, dependencyTemplates);
}
复制代码

执行对应的 template.harmonyInit ,对应模板在源码前加入以下代码:

  • export 相关(HarmonyExportSpecifierDependency
/* harmony export (binding) */

__webpack_require__.d(__webpack_exports__, 'mul', function() {
  return mul;
});
复制代码
  • import 相关(HarmonyImportSideEffectDependencyHarmonyImportSpecifierDependency
/* harmony import */

var Src_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! Src/b */ './src/b.js');
复制代码

生成 buildHash

parse 结束后,在 handleParseResult 里执行 this._initBuildHash(compilation)

_initBuildHash(compilation) {
    const hash = createHash(compilation.outputOptions.hashFunction); // compilation.outputOptions.hashFunction : md4
    if (this._source) {
        hash.update("source");
        this._source.updateHash(hash); // this._value
    }
    hash.update("meta");
    hash.update(JSON.stringify(this.buildMeta));
    this._buildHash = /** @type {string} */ (hash.digest("hex"));
}
复制代码

webpack 采用 nodejs 提供的加密模块 crypto 进行 hash 加密。

  • createHash(): 即执行 new BulkUpdateDecorator(require("crypto").createHash(algorithm))
  • hash.update(): 更新 hash 内容
  • hash.digest("hex"):得到 hash

先初始化了 hash, 然后分别 updatesourcethis._valuethis._source.updateHash(hash) 获得,为文件源码),metathis.buildMeta,最后计算出结果赋给 this._buildHash

然后回到文件 Compilation.jsmodule.build 的回调。对 errorwarning 的处理后,对 module.dependencies 按照代码在文件中出现的先后顺序进行排序,然后触发 Compilation.hooks: succeedModule

递归解析依赖

然后执行回调回到 this.buildModule 的回调里执行 afterBuild

const afterBuild = () => {
  if (addModuleResult.dependencies) {
    this.processModuleDependencies(module, err => {
      if (err) return callback(err);
      callback(null, module);
    });
  } else {
    return callback(null, module);
  }
};
复制代码

即判断如果该模块是首次解析则执行 processModuleDependencies

一旦某个模块被解析创建后,在 this.addModule(module)(上文已提到)里会设置 addModuleResult.dependenciesfalse 即可以避免该模块重复解析创建依赖。

processModuleDependencies 里,对 muduledependencies, blocks(懒加载 import xx 会存入), variables(内部变量 __resourceQuery )分别处理,其中对 blocks 的处理会递归调用。整理过滤没有标识 Identifiermodule,得到 sortedDependencies(以 module a 为例):

sortedDependencies = [
  {
    factory: NormalModuleFactory,
    dependencies: [HarmonyImportSideEffectDependency, HarmonyImportSpecifierDependency]
  },
  {
    factory: NormalModuleFactory,
    dependencies: [ImportDependency]
  }
];
复制代码

然后调用 this.addModuleDependencies:

addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) {
// dependencies 即为上文中的 sortedDependencies
//...
  asyncLib.forEach(
      dependencies,
      (item, callback) => {
        //...
        const semaphore = this.semaphore;
        semaphore.acquire(() => {
          const factory = item.factory;
          factory.create(
            {
                //...
            },(err, dependentModule) => {
                // 回调内容
            }
          );
        });
      },
      err => {
        //...
        return process.nextTick(callback);
      }
  );
}
复制代码

通过 asyncLib.forEach forEach 会将回调传给 iterator,在出现 erriterator 全部执行后执行回调。

批量调用每个依赖的 NormalModuleFactory.create,即与前文moduleFactory.create 功能一致。所以重复开始走 reslove 流程

NormalModuleFactory.create -> resolve流程 -> 初始化module -> add module -> module build -> afterBuild -> processModuleDependencies
复制代码

就这样,从入口 module 开始,根据 module 间的依赖关系,递归调用将所有的 module 都转换编译。

入口 module 生成

在依赖转换完成后,执行:

return process.nextTick(callback);
复制代码

将在 nodejs 下一次事件循环时调用 callback 即执行 this.processModuleDependencies 的回调:

this.processModuleDependencies(module, err => {
  if (err) return callback(err);
  callback(null, module);
});
复制代码

此时返回一个入口 module

{
  "module": {
    //...
    //同步模块
    "dependencies": ["HarmonyImportSideEffectDependency", "HarmonyImportSpecifierDependency"],
    //异步模块
    "blocks": ["ImportDependenciesBlock"]
  }
}
复制代码

执行 this._addModuleChain 的回调,触发 compilation.hooks:succeedEntry, 到此 mudule 生成结束!

本章小结

  1. 调用 parser 将前面 runloaders 的编译结果通过 acorn 转换为 ast
  2. 遍历 ast 根据导入导出及异步的情况触发相关钩子插件收集依赖,这些依赖用于解析递归依赖和模板操作;
  3. 根据每个 module 的相关信息生成各自唯一的 buildHash
  4. 根据 module 间的相互依赖关系,递归解析所有依赖 module,最终返回一个入口 module