关于webpack我心中有不少疑问。。。 本篇文章就简单看下webpack如何处理esm,尽力地了解下webpack的esm大体实现ass we canjavascript
本次分析的webpack版本为4.41.2前端
简单点说,本次就是想探知下webpack如何将以下示例(单entry的esm模块),通过build后生成bundle.jsjava
src/index.jsnode
// 引入hello函数
import sayHello from './hello.js'
let str = sayHello('1')
console.log(str)
复制代码
上面代码共有3条语句webpack
src/hello.jsgit
export default function sayHelloFn(...args) {
return 'hello, 参数值: ' + args[0]
}
复制代码
上面代码共有1条语句 webpack配置就采用最基础的就好了github
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')
}
}
复制代码
通过分析,大体能够分为两个过程web
parse过程流程上是发生在Compilation的buildModule钩子以后,具体代码在NormalModule类的doBuild回调中 Parser.parse, 调用的是Parser的parse静态方法,而此方法中能够看出webpack使用了 acorn 模块来进行解析成 astexpress
接下来就是核心的部分bootstrap
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
第一条语句
第二条语句
第三条语句
prewalk过程 第一条语句
预解析语句类型时,即statement.type为ImportDeclaration,由调用prewalkImportDeclaration方法来处理
获取source = statement.source.value,这里为 './hello.js,this.hooks.import.call(statement, source)
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)
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)
parser.state.harmonySpecifier.get('sayHello'),其在importSpecifier钩子中set,做为参数传给HarmonyImportSpecifierDependency
添加HarmonyImportSpecifierDependency到模块依赖中
hello.js
一条语句
prewalk过程
预解析语句函数声明的类型时,即statement.declaration.type为FunctionDeclartion,会调用钩子this.hooks.exportSpecifier.call(statement, 'sayHelloFn', 'default')
添加HarmonyExportSpecifierDependency到模块依赖
walk过程
解析语句的类型时,即statement.type为ExportDefaultDeclaration, 会调用钩子 this.hooks.export.call(statement)
添加HarmonyExportHeaderDependency到模块依赖
解析语句声明类型时,即statement.declarion.type为FunctionDeclartion,会调用钩子this.hooks.exportDeclaration.call(statement, statement.declaration)
钩子空载 即空函数目前不作处理
能够看出这些hook调用后,主要在作scope相关处理及添加模块依赖即添加到当前模块对象的属性dependencies列表中,留待后面的流程处理,而generate过程时就会对dependencies 中的Dependecy类对应的模板类进行调用
genernate过程发生在,Compilation流程的beforeChunkAssets钩子后chunkAsset钩子以前,具体则是在MainTemplate的renderManifest钩子及modules钩子内
至于到这里为何会先处理hello.js,应该是在buildChunkGraph的时候,判断出了hello.js是index.js依赖的模块,关于这个问题,之后再分析吧
接下来简述模板调用过程 (注意哦,这里发现了一个"bug",HarmonyCompatibilityDependency对应的模板类叫HarmonyExportDependencyTemplate,HarmonyExportHeaderDependency对应的模板类也叫HarmonyExportDependencyTemplate,什么是国际找bug工程师啊?战术后仰)
hello.js
// 调用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为-表示没有)并不在模块依赖中,这里仅展现
(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)之间也是有依赖的。
其余的先埋个坑,待之后再去探索吧。