webpack的esm

關於webpack我心中有不少疑問。。。 本篇文章就簡單看下webpack如何處理esm,盡力地瞭解下webpack的esm大體實現ass we canjavascript

本次分析的webpack版本爲4.41.2前端

熱身

  1. 瞭解tapable 用途相似EventEmitter (至於爲啥要搞這個,不直接在EventEmitter基礎上擴展sync,async,waterfall...,我猜想多是爲了調試方便)
  2. 瞭解下webpack流程(webpack 3.x)
    瞭解下webpack是 經過若干個關鍵類的"鉤子",分爲各個「核心階段",組成它打包的流程
  3. 瞭解webpack-sources 提供幾種類型CachedSource, PrefixSource, ConcatSource, ReplaceSource, 它們能夠組合使用,方便對代碼進行添加、替換、鏈接等操做 同時又含有一些source-map相關,updateHash等api 供webpack內部調用

示例

簡單點說,本次就是想探知下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過程

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

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)之間也是有依賴的。

其餘的先埋個坑,待之後再去探索吧。

相關文章
相關標籤/搜索