關於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
(1) HarmonyCompatibilityDependency 它對應的Template類爲HarmonyCompatibilityDependency// 調用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(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)之間也是有依賴的。
其餘的先埋個坑,待之後再去探索吧。