源码分析(七):构建 module(下)
通过 ast 分析依赖关系
runLoaders 运行结束后,回调里得到经 loader 编译后的模块代码字符串 result 和对应的 resourceBuffer。
在回调里执行了 createSource 后,判断 loader 的 result 是否有第三个参数对象并且里面存在 webpackAST 属性,如果有则为 ast 赋值到 _ast 上。这里作为 webpack 性能优化点:可以直接从 loader 返回 AST,从而避免 parse
然后回到 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。
析出 ast
方法里执行:
ast = Parser.parse(source, {
sourceType: this.sourceType,
onComment: comments,
});Parser.parse 即为 Parser 静态方法,该方法里主要执行:
ast = acornParser.parse(code, parserOptions); //即 acorn.Parserwebpack 通过 acorn 得到源码字符串对应的 ast。ast 相关资料:
遍历 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(HarmonyDetectionParserPlugin和UseStrictPlugin) 根据是否有import/export和use strict增加依赖:HarmonyCompatibilityDependency,HarmonyInitDependency,ConstDependencythis.detectStrictMode(ast.body)检测当前执行块是否有
use strict,并设置this.scope.isStrict = truethis.prewalkStatements(ast.body)- 处理
import进来的变量,是import就增加依赖HarmonyImportSideEffectDependency,HarmonyImportSpecifierDependency; - 处理
export出去的变量,是export增加依赖HarmonyExportHeaderDependency,HarmonyExportSpecifierDependency - 还会处理其他相关导入导出的变量
- 处理
this.blockPrewalkStatements(ast.body)处理块遍历
this.walkStatements(ast.body)用于深入函数内部(方法在
walkFunctionDeclaration进行递归),然后递归继续查找ast上的依赖,异步此处深入会增加依赖ImportDependenciesBlock;
上述执行结束后,会根据 import/export 的不同情况即模块间的相互依赖关系,在对应的 module.dependencies 上增加相应的依赖。
各依赖作用解释
在后面 generate 即 render 阶段,调用这些依赖(Dependency)对应的 template.apply 来渲染生成代码资源。
以 demo 入口文件 a.js 和 c.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 是否包含 harmonyInit 和 getHarmonyInitOrder 函数(用于导入的 import 排序),若都存在,则执行:
const order = template.getHarmonyInitOrder(dependency);执行对应的 template.getHarmonyInitOrder 用于获取排序的 order,在不同的依赖里根据需要可能会返回 NaN(如 HarmonyImportSideEffectDependencyTemplate 里判断无副作用(sideEffects)就会返回 NaN),最终筛选出不是 NaN 的依赖组成数组 list,即为含有 import 和 export 的依赖,按 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相关(HarmonyImportSideEffectDependency,HarmonyImportSpecifierDependency)
/* harmony import */
var Src_b__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! Src/b */ './src/b.js');生成 buildHash
parse 结束后,得到 result, 其中 module、current 的 dependencies 属性里已添加对应的依赖。然后在 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, 然后分别 update 了 source,this._value( this._source.updateHash(hash) 获得,为文件源码),meta,this.buildMeta,最后计算出结果挂载到 module._buildHash 上。
然后回到文件 Compilation.js 的 module.build 的回调。对 error 和 warning 的处理后,对 module.dependencies 按照代码在文件中出现的先后顺序进行排序,然后触发 Compilation.hooks: succeedModule,标志该 module 构建完毕。
递归解析依赖
然后执行回调回到 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.dependencies 为 false 即可以避免该模块重复解析创建依赖。
在 processModuleDependencies 里,对 mudule 的 dependencies, blocks(懒加载 import xx 会存入), variables(内部变量 __resourceQuery )分别处理,其中对 blocks 的处理会递归调用。整理过滤没有标识 Identifier 的 module(即找出仅有模块依赖关系的依赖),得到 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,在出现 err 或 iterator 全部执行后执行回调。
批量调用每个依赖的 NormalModuleFactory.create,即与前文moduleFactory.create 功能一致。所以重复开始走 reslove 流程:
NormalModuleFactory.create -> resolve流程 -> 初始化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, 标志着 addEntry 方法执行结束。到此入口 mudule 全部生成结束,module 所依赖的其他同步异步 module 将分别保存在 dependencies 与 blocks 里。
本章小结
- 调用
parser将前面runloaders的编译结果通过acorn转换为ast; - 遍历
ast根据导入导出及异步的情况触发相关钩子插件收集依赖,这些依赖用于解析递归依赖和模板操作; - 根据每个
module的相关信息生成各自唯一的buildHash; - 根据
module间的相互依赖关系,递归解析所有依赖module,最终返回一个入口module,module所依赖的其他同步异步module将分别保存在dependencies与blocks里。