webpack 4 源码主流程分析(七):构建 module(下)
通过 ast 分析依赖关系
runLoaders
运行结束后,回调里得到经 loader
编译后的模块代码字符串 result
和对应的 resourceBuffer
。
在回调里执行了 createSource
后,判断 loader
的 result
是否有第三个参数对象并且里面存在 webpackAST
属性,如果有则为 ast
赋值到 _ast
上。这里作为 webpack 性能优化点:可以直接从 loader 返回 AST,从而避免 parse
然后回到 this.doBuild
执行回调,在根据项目配置项判断是否需要 parse
后,若需要解析则执行:
1 | const result = this.parser.parse( |
this.parser
即是在 reslove 流程
里的组合对象里得到的 parser
。
在 this.parser.parse
里如果 this._ast
不存在则传 this._source._value
即代码字符串。 然后进入文件 node_modules/webpack/lib/Parser.js
执行 Parser.parse
。
析出 ast
方法里执行:
1 | ast = Parser.parse(source, { |
Parser.parse
即为 Parser
静态方法,该方法里主要执行:
1 | ast = acornParser.parse(code, parserOptions); //即 acorn.Parser |
webpack
通过 acorn 得到源码字符串对应的 ast
。ast
相关资料:
- estree
- ast 类型查阅
- 在线测试
遍历 ast 收集依赖
回到 Parser.parse
,对 ast
进行遍历,执行:
1 | if (this.hooks.program.call(ast, comments) === undefined) { |
this.hooks.program.call(ast, comments)
触发回调
plugin
(HarmonyDetectionParserPlugin
和UseStrictPlugin
) 根据是否有import/export
和use strict
增加依赖:HarmonyCompatibilityDependency
,HarmonyInitDependency
,ConstDependency
this.detectStrictMode(ast.body)
检测当前执行块是否有
use strict
,并设置this.scope.isStrict = true
this.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
为例,则依赖为:
1 | //a.js module |
1 | //d.js module |
HarmonyInitDependency
执行其对应 template.apply
(文件 HarmonyInitDependency.js
)中,先遍历 module.dependencies
,判断各依赖对应的 template
是否包含 harmonyInit
和 getHarmonyInitOrder
函数(用于导入的 import
排序),若都存在,则执行:
1 | const order = template.getHarmonyInitOrder(dependency); |
执行对应的 template.getHarmonyInitOrder
用于获取排序的 order
,在不同的依赖里根据需要可能会返回 NaN
(如 HarmonyImportSideEffectDependencyTemplate
里判断无副作用(sideEffects
)就会返回 NaN
),最终筛选出不是 NaN
的依赖组成数组 list
,即为含有 import
和 export
的依赖,按 order
排序后,
执行:
1 | for (const item of list) { |
执行对应的 template.harmonyInit
,对应模板在源码前加入以下代码:
export
相关(HarmonyExportSpecifierDependency
)
1 | /* harmony export (binding) */ |
import
相关(HarmonyImportSideEffectDependency
,HarmonyImportSpecifierDependency
)
1 | /* harmony import */ |
生成 buildHash
parse
结束后,得到 result
, 其中 module、current
的 dependencies
属性里已添加对应的依赖。然后在 handleParseResult
里执行 this._initBuildHash(compilation)
:
1 | _initBuildHash(compilation) { |
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
:
1 | const afterBuild = () => { |
即判断如果该模块是首次解析则执行 processModuleDependencies
。
一旦某个模块被解析创建后,在 this.addModule(module)
(上文已提到)里会设置 addModuleResult.dependencies
为 false
即可以避免该模块重复解析创建依赖。
在 processModuleDependencies
里,对 mudule
的 dependencies
, blocks
(懒加载 import xx
会存入), variables
(内部变量 __resourceQuery
)分别处理,其中对 blocks
的处理会递归调用。整理过滤没有标识 Identifier
的 module
(即找出仅有模块依赖关系的依赖),得到 sortedDependencies
(以 module a
为例):
1 | sortedDependencies = [ |
然后调用 this.addModuleDependencies
:
1 | addModuleDependencies(module, dependencies, bail, cacheGroup, recursive, callback) { |
通过 asyncLib.forEach forEach
会将回调传给 iterator
,在出现 err
或 iterator
全部执行后执行回调。
批量调用每个依赖的 NormalModuleFactory.create
,即与前文moduleFactory.create
功能一致。所以重复开始走 reslove 流程
:
1 | NormalModuleFactory.create -> resolve流程 -> 初始化module -> module build -> afterBuild -> processModuleDependencies ... |
就这样,从入口 module
开始,根据 module
间的依赖关系,递归调用将所有的 module
都转换编译。
入口 module 生成
在依赖转换完成后,执行:
1 | return process.nextTick(callback); |
将在 nodejs
下一次事件循环时调用 callback
即执行 this.processModuleDependencies
的回调:
1 | this.processModuleDependencies(module, (err) => { |
此时返回一个入口 module
:
1 | { |
执行 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
里。