关于webpack我心中有很多疑问。。。 本篇文章就简单看下webpack如何处理esm,尽力地了解下webpack的esm大致实现ass we can
本次分析的webpack版本为4.41.2
热身
- 了解tapable 用途类似EventEmitter (至于为啥要搞这个,不直接在EventEmitter基础上扩展sync,async,waterfall...,我猜测可能是为了调试方便)
- 了解下webpack流程(webpack 3.x) 了解下webpack是 通过若干个关键类的"钩子",分为各个“核心阶段",组成它打包的流程
- 了解webpack-sources 提供几种类型CachedSource, PrefixSource, ConcatSource, ReplaceSource, 它们可以组合使用,方便对代码进行添加、替换、连接等操作 同时又含有一些source-map相关,updateHash等api 供webpack内部调用
示例
简单点说,本次就是想探知下webpack如何将如下示例(单entry的esm模块),经过build后生成bundle.js
src/index.js
// 引入hello函数
import sayHello from './hello.js'
let str = sayHello('1')
console.log(str)
上面代码共有3条语句
src/hello.js
export default function sayHelloFn(...args) {
return 'hello, 参数值: ' + args[0]
}
上面代码共有1条语句 webpack配置就采用最基础的就行了
module.exports = {
mode: 'development',
devtool: 'source-map',
entry: {
app: path.resolve(__dirname, './src/index.js')
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
}
}
经过分析,大致可以分为两个过程
parse过程
parse过程流程上是发生在Compilation的buildModule钩子之后,具体代码在NormalModule类的doBuild回调中 Parser.parse, 调用的是Parser的parse静态方法,而此方法中可以看出webpack使用了 acorn 模块来进行解析成 ast
接下来就是核心的部分
if (this.hooks.program.call(ast, comments) === undefined) {
this.detectStrictMode(ast.body);
// Prewalking iterates the scope for variable declarations
this.prewalkStatements(ast.body);
// Block-Prewalking iterates the scope for block variable declarations
this.blockPrewalkStatements(ast.body);
// Walking iterates the statements and expressions and processes them
this.walkStatements(ast.body);
}
这里主要工作就是 遍历模块ast中的一条一条statement, 遇到一些类型做一些处理,并且可能调用预先在evaluate钩子(Parser构造函数中定义的HookMap)上为各种"表达式"tap的处理函数,这些表达式为"Literal", "LogicalExpression", "BinaryExpression", "UnaryExpression"... 同时会调用插件(HarmonyImportDependencyParserPlugin, HarmonyExportDependencyParserPlugin...)中的一些钩子,让外部插件做一些相对应的处理
其中prewalk, blockPrewalk, walk过程均要对每条语句进行解析
预解析时会操作scope对象,所以在这里处理import, export有点类似js的作用域提升
this.scope = {
topLevelScope: true,
inTry: false,
inShorthand: false,
isStrict: false,
definitions: new StackedSetMap(),
renames: new StackedSetMap()
};
也就是即使这样写,也是没问题的
let str = sayHello('1')
console.log(str)
import sayHello from './hello.js'
下面做些主要的分析
index.js
index ast
第一条语句
第二条语句
第三条语句
prewalk过程 第一条语句
预解析语句类型时,即statement.type为ImportDeclaration,由调用prewalkImportDeclaration方法来处理
获取source = statement.source.value,这里为 './hello.js,this.hooks.import.call(statement, source)
调用import钩子
parser.state.lastHarmonyImportOrder = (parser.state.lastHarmonyImportOrder || 0) + 1
添加ConstDependency到模块依赖中
添加HarmonyImportSideEffectDependency到模块依赖中
遍历 .specifiers, 即预解析指示符
this.scope.renames.set('sayHello', null)
this.scope.definitions.add('sayHello')
判断类型,即specifier.type为ImportDefaultSpecifier,会调用this.hooks.importSpecifier.call(statement, source, 'default', name)
调用importSpecifier钩子
parser.scope.definitions.delete('sayHello')
parser.scope.renames.set('sayHello', 'imported var')
parser.state.harmonySpecifier.set('sayHello', {
source: './hello.js',
id: 'default',
// 1
sourceOrder: parser.state.lastHarmonyImportOrder
})
blockPrewalk过程 第二条语句
预解析语句类型,即statement.type为VariableDeclaration,会调用blockPrewalkVariableDeclaration方法
遍历 .declarations,这里就一个
根据declarator的情况,this.scope.renames.set('str', null); this.scope.definitions.add('str')
walk过程 第二条语句
解析到sayHello, 发现其类型为Identifier,会获取钩子并调用 callHook = this.hooks.call.get('imported var'); callHook.call(expression)
调用call钩子
parser.state.harmonySpecifier.get('sayHello'),其在importSpecifier钩子中set,作为参数传给HarmonyImportSpecifierDependency
添加HarmonyImportSpecifierDependency到模块依赖中
hello.js
hello.js ast
一条语句
prewalk过程
预解析语句函数声明的类型时,即statement.declaration.type为FunctionDeclartion,会调用钩子this.hooks.exportSpecifier.call(statement, 'sayHelloFn', 'default')
调用exportSpecifier钩子
添加HarmonyExportSpecifierDependency到模块依赖
walk过程
解析语句的类型时,即statement.type为ExportDefaultDeclaration, 会调用钩子 this.hooks.export.call(statement)
调用export钩子
添加HarmonyExportHeaderDependency到模块依赖
解析语句声明类型时,即statement.declarion.type为FunctionDeclartion,会调用钩子this.hooks.exportDeclaration.call(statement, statement.declaration)
调用exportDeclaration钩子
钩子空载 即空函数目前不做处理
可以看出这些hook调用后,主要在做scope相关处理及添加模块依赖即添加到当前模块对象的属性dependencies列表中,留待后面的流程处理,而generate过程时就会对dependencies 中的Dependecy类对应的模板类进行调用
generate过程
genernate过程发生在,Compilation流程的beforeChunkAssets钩子后chunkAsset钩子之前,具体则是在MainTemplate的renderManifest钩子及modules钩子内
至于到这里为什么会先处理hello.js,应该是在buildChunkGraph的时候,判断出了hello.js是index.js依赖的模块,关于这个问题,以后再分析吧
接下来简述模板调用过程 (注意哦,这里发现了一个"bug",HarmonyCompatibilityDependency对应的模板类叫HarmonyExportDependencyTemplate,HarmonyExportHeaderDependency对应的模板类也叫HarmonyExportDependencyTemplate,什么是国际找bug工程师啊?战术后仰)
hello.js
(1) HarmonyCompatibilityDependency 它对应的Template类为HarmonyCompatibilityDependency调用apply
// 调用runtimeTemplate的defineEsModuleFlagStatement, 其中参数exportsArgument为"__webpack_exports__"
// 得到 content = "__webpack_require__.r(__webpack_exports__)"
const content = runtime.defineEsModuleFlagStatement({
exportsArgument: dep.originModule.exportsArgument
});
source.insert(-10, content) // -10表示优先级,越小越会靠前执行
(2) HarmonyInitDependency 它对应的Template类为HarmonyInitDependencyTemplate
调用apply
对module.dependencies遍历,尝试调用对应的template的getHarmonyInitOrder方法,获取order
接着优先根据order,其次根据template所在位置,排序list,按从小到大的顺序
再对list遍历,依次调用template的harmonyInit方法
(2.1) HarmonyExportSpecifierDependency 它对应的Template类 HarmonyExportSpecifierDependencyTemplate
调用getHarmonyInitOrder
直接返回0 返回给HarmonyInitDependencyTemplate中
调用harmonyInit
const content = `/* harmony export (binding) */ webpack_require.d({exportsName}, {JSON.stringify(
used
)}, function() { return ${dep.id}; });\n`
source.insert(-1, content); // -1表示优先级,越小越会靠前执行
(3) HarmonyExportHeaderDependency 它对应的Template类为HarmonyExportDependencyTemplate (x)
调用apply
source.replace(dep.rangeStatement[0], replaceUntil, content);
实际作用就是将 "export default"替换为空字符串
经过处理后的代码
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return sayHelloFn; });
function sayHelloFn(...args) {
return 'hello, 参数值: ' + args[0]
}
index.js 其中第5行(Dependency为-表示没有)并不在模块依赖中,这里仅展示
(1) HarmonyCompatibilityDependency 它对应的Template类为HarmonyCompatibilityDependency调用apply
同hello.js (1)
(2) HarmonyInitDependency 它对应的Template类为HarmonyInitDependencyTemplate
调用apply
同hello.js (2) 调用模板的getHarmonyInitOrder,harmonyInit方法
(2.1) HarmonyImportSideEffectDependency继承自HarmonyImportDependency,它对应的Template类HarmonyImportSideEffectDependencyTemplate它继承自HarmonyImportDependencyTemplate
调用getHarmonyInitOrder
返回dep.sourceOrder,该属性是添加模块依赖new HarmonyImportSideEffectDependency时传入,其值为parser.state.lastHarmonyImportOrder,此处为1
调用getHarmonyInit
let sourceInfo = importEmittedMap.get(source);
if (!sourceInfo) {
importEmittedMap.set(
source,
(sourceInfo = {
emittedImports: new Map()
})
);
}
const key = dep._module || dep.request;
if (key && sourceInfo.emittedImports.get(key)) return;
sourceInfo.emittedImports.set(key, true);
// dep为HarmonyImportSideEffectDependency实例,getImportStatement方法是在HarmonyImportSideEffectDependency父类中定义
const content = dep.getImportStatement(false, runtime);
source.insert(-1, content);
这里的content为/* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello.js */ "./analysis/harmonymodule-analysis/hello.js");
(2.2) HarmonyImportSpecifierDependency继承自HarmonyImportDependency,它对应的Template类HarmonyImportSideEffectDependencyTemplate它继承自HarmonyImportDependencyTemplate
调用getHarmonyInitOrder
同(2.1)
调用getHarmonyInit
// 由于(2.1)
// 此条件为true 不做处理返回
if (key && sourceInfo.emittedImports.get(key)) return;
(3) ConstDependency 它对应的Template类为ConstDependencyTemplate
调用apply
// dep即为ConstDependency实例
source.replace(dep.range[0], dep.range[1] - 1, dep.expression);
实际作用是将位置13~46(import sayHello from './hello.js'
)替换为空字符串
(4) HarmonyImportSpecifierDependency继承自HarmonyImportDependency,它对应的Template类HarmonyImportSideEffectDependencyTemplate它继承自HarmonyImportDependencyTemplate
HarmonyImportSpecifierDependency (2.2)不是处理过了吗? 没看错它的模板类继承自HarmonyImportDependencyTemplate其apply方法为空函数,getHarmonyInitOrder,getHarmonyInit却有定义,所以这里apply调用是HarmonyImportSpecifierDependencyTemplate中的apply方法
调用apply
// dep即为HarmonyImportSpecifierDependency实例
const content = this.getContent(dep, runtime);
source.replace(dep.range[0], dep.range[1] - 1, content)
它的作用是将位置58~65(sayHello
)替换为Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__["default"])
经过处理后的代码
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _hello_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./hello.js */ "./analysis/harmonymodule-analysis/hello.js");
// 引入hello函数
let str = Object(_hello_js__WEBPACK_IMPORTED_MODULE_0__["default"])('1')
console.log(str)
最后它们会在MainTemplate的render钩子中与bootstrap代码拼接成整个bundle
一个疑问
前面不是说有很多疑问吗?看到这,你心中有什么疑问?come in... 没有?好吧,我来提个问题吧
问:webpack会解析esm,babel-loader也会让babel将ES6转为ES5,那它们在转换esm时岂不是"冲突"了?
webpack添加配置
module: {
rules: [{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env'
]
}
}
}]
}
答:实际上babel-loader会扩展选项
{ caller: Object.assign({ name: "babel-loader", supportsStaticESM: true, supportsDynamicImport: true }, opts.caller) }
这是babel7提供的caller metadata特性,这样@babel/core就会传递给presets/plugins,这里@babel/preset-env就不会使用@babel/transform-modules-commonjs插件去转换import export代码了,而这里的parse过程都在runLoader
之后。
总结
联系我们日常工作,就好比一个项目(module),分析需求后(parse),来决定需要几个角色(Dependency),如设计、前端、后端,它们各司其职来完成(generate)项目。有的项目呢可能只需要前端和后端,而有些前端呢甚至还能帮一下后端,后端可能就说了,你帮了我就没饭碗了,后端就赶紧先完成了手头工作(HarmonyImportSideEffectDependency和HarmonyImportSpecifierDependency),当然项目(hello.js)和项目(index.js)之间也是有依赖的。
其他的先埋个坑,待以后再去探索吧。