淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module

接上文:淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作

四侦副、遞歸編譯生成 module 實(shí)例

4.1 resolve 階段谤逼,解析返回包含當(dāng)前模塊所有信息的一個對象

此階段概述:利用 enhanced-resolve 庫谒兄,得到 resolve 解析方法 ?? 解析 inline loader 和它對應(yīng)資源的 resource,還有項目config的 loader汰蜘,然后對所有 loader 進(jìn)行合并晴弃、排序 ?? 得到 module 對應(yīng)的 parser 和 generator宛瞄,用于后面的 ast 解析及模板生成 ?? 輸出一個包含當(dāng)前模塊上下文、loaders爱咬、絕對路徑尺借、依賴等 module 所有信息的組合對象,提供給 afterResolve 鉤子觸發(fā)后的回調(diào)精拟。這個對象下一步會被用來初始化當(dāng)前文件 的 module 實(shí)例燎斩。

上一步我們已經(jīng)得知 moduleFactory 就是 normalModuleFactory,那么接著看 normalModuleFactory 的 create 方法:
觸發(fā)normalModuleFactory.hooks:beforeResolve蜂绎,在回調(diào)里觸發(fā)NormalModuleFactory.hooks:factory鉤子栅表,再執(zhí)行該 factory 函數(shù),即NormalModuleFactory.hooks:resolver师枣。

// /lib/NormalModuleFactory.js
constructor(context, resolverFactory, options) {
  this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
    // hooks:factory 綁定的方法
    let resolver = this.hooks.resolver.call(null); // 觸發(fā) resolver 鉤子返回一個 resolver 函數(shù)
    resolver(result, (err, data) => {
      //...
      this.hooks.afterResolve.callAsync(data, (err, result) => {
        let createdModule = this.hooks.createModule.call(result);
        if (!createdModule) {
          // 創(chuàng)建 normalModule 實(shí)例
          createdModule = new NormalModule(result);
        }
        createdModule = this.hooks.module.call(createdModule, result);
        return callback(null, createdModule);
      });
    });
  });
  this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
    const loaderResolver = this.getResolver("loader"); // 用于解析 loader 的絕對路徑
    const normalResolver = this.getResolver("normal", data.resolveOptions); // 用于解析 文件 和 module 的絕對路徑
  }
}
create(data, callback) {
  // ...
  this.hooks.beforeResolve.callAsync({...}, (err, result) => {
    // 觸發(fā) NormalModuleFactory.hooks: factory 
    const factory = this.hooks.factory.call(null);
    factory(result, (err, module) => {
      //...
    });
}

上面的 resolver 函數(shù)負(fù)責(zé)解析 構(gòu)建 module 所需 loaders 的絕對路徑 以及每個 module 的相關(guān)構(gòu)建信息(如獲取 module 的 packge.json 等)怪瓶。
this.getResolver 即/lib/ResolverFactory.js的 get 方法,判斷如有緩存返回緩存践美,無則執(zhí)行 _create 方法:

// /lib/ResolverFactory.js
_create(type, resolveOptions) {
  // Factory 指向的文件路徑:node_modules/enhanced-resolve/lib/ResolverFactory.js
  const Factory = require("enhanced-resolve").ResolverFactory;
  resolveOptions = this.hooks.resolveOptions.for(type).call(resolveOptions);
  // enhanced-resolve/lib/ResolverFactory.js  導(dǎo)出的 createResolver 方法
  const resolver = Factory.createResolver(resolveOptions);
  // 利用 enhanced-resolve 庫注冊完鉤子插件后觸發(fā) ResolverFactory 的 resolver 鉤子
  this.hooks.resolver.for(type).call(resolver, resolveOptions);
  return resolver;
}

編譯前準(zhǔn)備我們通過WebpackOptionsApply.jsResolverFactory.hooks: resolveOptions鉤子上注冊了綁定事件洗贰,此刻觸發(fā)后用 cachedCleverMerge 判斷緩存及融合配置(如果 type 是 loader 則為 配置項: options.resolveLoader,如果是 normal 則為 配置項: options.resolve)拨脉,并添加屬性 fileSystem: compiler.inputFileSystem哆姻,最終返回一個 resolveOptions 對象,作為 Factory.createResolver 執(zhí)行的參數(shù)玫膀。
enhanced-resolve createResolver 方法內(nèi)矛缨,先融合處理了項目配置 resolve 與默認(rèn)配置 resolve/resolveLoader,如未傳入項目的 resolver,就自己 new 一個箕昭。接著定義了 Resolver 的生命周期鉤子并根據(jù)配置 push 了一大堆 plugins 實(shí)例灵妨。然后對每一個插件執(zhí)行 apply,在 Resolver 不同生命周期鉤子上注冊一些方法落竹,并在函數(shù)末尾執(zhí)行:

// node_modules/enhanced-resolve/lib/xxxPlugin.js
// 獲取hooks泌霍,target 為事件鉤子
const target = resolver.ensureHook(this.target);
// 觸發(fā)插件后的回調(diào)里,執(zhí)行:
resolver.doResolve(target, obj, ...);

在觸發(fā)完當(dāng)前插件后述召,會通過 doResolve 將 hook 帶入到下一個插件中朱转,實(shí)現(xiàn)遞歸串聯(lián)調(diào)用一系列的插件,包括 UnsafeCachePlugin积暖、ParsePlugin藤为、DescriptionFilePlugin、ModuleKindPlugin 等等夺刑,來完成各自的操作缅疟。

再回到 NormalModuleFactory.hooks: resolver,拿到 loaderResolvernormalResolver遍愿,用于解析路徑存淫。

接下來進(jìn)行 inline loader 和對應(yīng)資源文件 resource 的解析:
比如import Styles from style-loader!css-loader?modules!./styles.css會被解析成:

{
  "resource": "./styles.css",
  "elements": [
    {
      "loader": "style-loader"
    },
    {
      "loader": "css-loader",
      "options": "modules"
    }
  ]
}

然后執(zhí)行asyncLib.parallel(...),它會并行處理參數(shù)數(shù)組各個任務(wù)沼填,都完成之后返回一個 results 列表桅咆,列表順序?yàn)閰?shù)數(shù)組順序,與執(zhí)行順序無關(guān)倾哺。
得到的 results:

{
  "results": [
    [
      {
        "loader": "loader的絕對路徑1",
        "options": "loader參數(shù)1"
      },
      {
        "loader": "loader的絕對路徑2",
        "options": "loader參數(shù)2"
      }
    ],
    {
      "resource": "模塊絕對路徑",
      "resourceResolveData": "模塊基本信息(即enhanced-resolve執(zhí)行結(jié)果)"
    }
  ]
}

const result = this.ruleSet.exec({...}) 解析 config module rules 里的 loader轧邪,遞歸過濾匹配出對應(yīng)的 loader:

{
  "result": [
    { "type": "type", "value": "javascript/auto" },
    { "type": "resolve", "value": {} },
    { "type": "use", "value": { "loader": "babel-loader" } }
  ]
}

對 loader 進(jìn)行合并、排序
接著處理inline loader帶有前綴!,!!,-!result項帶有enforce參數(shù)的情況羞海,用來決定懟 loader的禁用和排序忌愚。

又通過 asyncLib.parallel 與 this.resolveRequestArray 并行處理上一步得到的useLoadersPost、useLoadersPre却邓、useLoaders硕糊,拿到對應(yīng)的 resolve 結(jié)果即路徑信息,再在回調(diào)里排序腊徙、合并简十,即 loaders 配置順序?yàn)?postLoader,inlineLoader撬腾,loader(normal)螟蝙,preLoader,執(zhí)行順序則相反民傻。

最后輸出以下組合對象:

// /lib/NormalModuleFactory.js
callback(null, {
  context: context,
  request: loaders
    .map(loaderToIdent)
    .concat([resource])
    .join("!"),
  dependencies: data.dependencies,
  userRequest,
  rawRequest: request,
  loaders,
  resource,
  matchResource,
  resourceResolveData,
  settings,
  type,
  parser: this.getParser(type, settings.parser), // 創(chuàng)建 parser 并緩存
  generator: this.getGenerator(type, settings.generator), // 創(chuàng)建 generator 并緩存
  resolveOptions
});

其中 getParser 的主要作用是為 module 提供解析模塊為 ast 的 parser胰默。
createParser 時會根據(jù)不同 type 返回不同的 parser 實(shí)例场斑。
getGenerator 主要作用是為 module 提供模版生成時的 generator (的)方法。
createGenerator 時根據(jù) type 不同返回不同的 generator 實(shí)例(目前代碼里都是返回一致的 new JavascriptGenerator() )牵署。

跳出 NormalModuleFactory 的 resolver 鉤子函數(shù)漏隐,執(zhí)行 resolver 函數(shù)回調(diào),至此 resolve 流程結(jié)束奴迅。

4.2 執(zhí)行 loader 階段青责,初始化模塊 module,并用 loader 倒序轉(zhuǎn)譯

開啟構(gòu)建 module 流程取具。 new NormalModule(result)得到初始化的 module ?? 在 build 過程中執(zhí)行 runLoaders 處理源碼脖隶,先正序讀取每個 loader 并執(zhí)行它的 pitch,再倒序執(zhí)行每個 loader 的 normal者填,最后得到一個編譯后的字符串或 Buffer浩村。

(繼續(xù)看/lib/NormalModuleFactory.js) 觸發(fā) normalModuleFactory.hooks:afterResolve 和 normalModuleFactory.hooks:createModule,let createdModule = this.hooks.createModule.call(result);的這個 result 參數(shù)就是normalModuleFactory.hooks.resolver.tap 輸出的組合 object占哟。如果不存在項目配置的自定義 module,就使用new NormalModule(result)生成的 module酿矢。

跳出 factory 鉤子 tap 綁定的函數(shù)榨乎,執(zhí)行factory(result, (err, module) => {})的回調(diào),傳入的 module 就是我們初始化的 NormalModule 實(shí)例瘫筐,進(jìn)行依賴緩存后蜜暑,結(jié)束 create 方法,回到/lib/Compilation.js執(zhí)行 moduleFactory.create 的回調(diào)策肝。

// /lib/Compilation.js
addModule(module, cacheGroup) {
  const identifier = module.identifier(); // 即 module.request
  //  根據(jù) identifie 判斷`compilation._modules`是否有該 module
  const alreadyAddedModule = this._modules.get(identifier);
  if (alreadyAddedModule) { // 如果已經(jīng)存在則返回如下 object
    return {
      module: alreadyAddedModule,
      issuer: false,
      build: false,
      dependencies: false
    };
  }
  // ...
  // 將這個 module 保存到全局的 `Compilation`的`modules` 數(shù)組和`_modules` Map 對象中
  this._modules.set(identifier, module);
  this.modules.push(module); 
  return { // 如是從未添加到`compilation`的模塊肛捍,返回如下對象
    module: module,
    issuer: true,
    build: true,
    dependencies: true
  };
}
_addModuleChain(context, dependency, onModule, callback) {
  // ...
  moduleFactory.create({...}, (err, module) => { 
    // create 執(zhí)行完的回調(diào)
    // 用初始化的 module 作為參數(shù)調(diào)用 addModule
    const addModuleResult = this.addModule(module);
    module = addModuleResult.module;
    // 如果是入口文件還會將 module 保存到 `Compilation.entries`
    onModule(module);
    dependency.module = module;
    module.addReason(null, dependency); // 添加該`module`被哪些模塊依賴的信息,會存到 module.reasons 數(shù)組里
    if (addModuleResult.build) { // 沒有添加過的模塊 build 屬性默認(rèn)是 true
      this.buildModule(module, false, null, null, err => {
        afterBuild();
      })
    }
  })
}

先執(zhí)行this.addModule之众,返回一個對象 addModuleResult
如果這個 module 之前未被添加到compilation拙毫,將它保存到全局compilation對象的modules 數(shù)組和_modules Map 對象中,返回結(jié)果的 module 屬性為當(dāng)前模塊棺禾,issuer缀蹄、build、dependencies 的值都為 true膘婶;如果已存在缺前,則 module 屬性為查到的值,其他三個屬性都為 false悬襟。

調(diào)用this.buildModule進(jìn)入 build 階段衅码。做了回調(diào)緩存后,觸發(fā)compilation.hooks:buildModule脊岳,然后執(zhí)行module.build()逝段。

module 是 NormalModule 的實(shí)例垛玻,我們來到/lib/NormalModule.js看 build 方法:在設(shè)置一些屬性后調(diào)用了 NormalModule 的 doBuild 方法。

// /lib/NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
  // 為所有的 loader 提供上下文環(huán)境
  const loaderContext = this.createLoaderContext(
    resolver,
    options,
    compilation,
    fs
  );

  runLoaders(
    {
      resource: this.resource,
      loaders: this.loaders,
      context: loaderContext,
      readResource: fs.readFile.bind(fs)
    },
    (err, result) => {
     //...
    }
  );
}

runLoaders 方法來自 loader-runner惹恃,作用是按規(guī)定流程執(zhí)行各種 loader夭谤,將模塊源碼后處理成一個 String 或 Buffer 格式的 JavaScript (可能還有個 SourceMap)。
關(guān)于 loader 本身的機(jī)制可以看下這篇: webpack 之 Loader 詳解

主要流程
runLoaders ?? iteratePitchingLoaders (正序 require 每個 loader) ?? loadLoader (將當(dāng)前 loader 的模塊導(dǎo)出函數(shù)賦值到loaderContext.loaders[index].normal巫糙、loader 模塊的pitch 函數(shù)賦值到loaderContext.loaders[index].pitch朗儒,然后執(zhí)行pitch 函數(shù)[如果有的話]) ?? 讀取完當(dāng)前模塊的全部 loader,執(zhí)行 processResource (設(shè)置 loaderIndex 為最后一個 loader 的 index / 轉(zhuǎn)換 buffer) ?? iterateNormalLoaders (倒序執(zhí)行所有 loader [normal])

其中執(zhí)行 pitchnormal 都調(diào)用了 runSyncOrAsync (同步或者異步執(zhí)行 loader) 方法参淹,如果在 iteratePitchingLoaders 階段某個 pitch 有返回值醉锄,則直接進(jìn)入 iterateNormalLoaders 階段 (將該pitch返回值作為參數(shù)),從前一個讀取的 loader 開始倒序執(zhí)行浙值。

// node_modules/loader-runner/lib/LoaderRunner.js
// 同步或者異步執(zhí)行 loader 函數(shù)
function runSyncOrAsync(fn, context, args, callback) {
  try {
    var result = (function LOADER_EXECUTION() {
      return fn.apply(context, args); // 執(zhí)行 loader 函數(shù)恳不,參數(shù)傳遞前一個 loader 的執(zhí)行結(jié)果
    })();
    if (isSync) {
      // ...
      return callback(null, result);
    }
  } catch(e) {
    callback(e)
  }
}
// 核心方法,按正序 require 每個 loader
function iteratePitchingLoaders(options, loaderContext, callback) {
  // 發(fā)現(xiàn)讀取完所有 loader 后开呐,執(zhí)行 processResource 方法
  // 第一次執(zhí)行 loaderIndex 是 0烟勋,如果 loaders 數(shù)組的個數(shù)是 0 才走processResource,如果當(dāng)前模塊有 loader 則繼續(xù)往下走筐付。
  if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
  // 根據(jù) loaderIndex 獲取當(dāng)前要讀取的 loader 對象
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // 如果當(dāng)前 loader 的 pitch 階段已經(jīng)執(zhí)行過卵惦,則繼續(xù)迭代執(zhí)行
  if(currentLoaderObject.pitchExecuted) {
    // 增序后遞歸讀取下一個 loader
    loaderContext.loaderIndex++;
    return iteratePitchingLoaders(options, loaderContext, callback);
  }
  // node_modules/loader-runner/lib/loadLoader.js
  // loadLoader 這個方法負(fù)責(zé)加載當(dāng)前 loader 模塊,將 loader 模塊導(dǎo)出的函數(shù)賦值到  loader.normal, 模塊的 pitch 方法賦值到 loader.pitch
  loadLoader(currentLoaderObject, function(err) {
    if(err) {
      loaderContext.cacheable(false);
      return callback(err);
    }
    // 獲取 loader 模塊的 pitch 方法
    var fn = currentLoaderObject.pitch;
    currentLoaderObject.pitchExecuted = true;
    // 如沒有 pitch 函數(shù)直接 require 下一個 loader
    if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
    // 有 pitch 則執(zhí)行 pitch 函數(shù)瓦戚,根據(jù) runSyncOrAsync 的回調(diào)在沒報錯的情況下有無返回其他參數(shù)沮尿,決定是否繼續(xù)讀取剩下的loader
    runSyncOrAsync(
      fn,
      loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}],
      function(err) {
        if(err) return callback(err);
        var args = Array.prototype.slice.call(arguments, 1);
        if(args.length > 0) { // 執(zhí)行 pitch 有返回結(jié)果則將 loaderIndex 減序,并將返回結(jié)果作為 iterateNormalLoaders 的參數(shù)较解,開始倒序執(zhí)行前面已經(jīng) require 的 loader
          loaderContext.loaderIndex--; // 這個減序是為了從前一個讀取的 loader 開始執(zhí)行畜疾,不執(zhí)行當(dāng)前 loader 的 normal
          iterateNormalLoaders(options, loaderContext, args, callback);
        } else { // pitch 沒有返回值繼續(xù)讀取下一個 loader
          iteratePitchingLoaders(options, loaderContext, callback);
        }
      }
    );
  });
}
// 設(shè)置 loaderIndex 為最后一個 loader 的 index
// 轉(zhuǎn)換 buffer 后再走 iterateNormalLoaders
function processResource(options, loaderContext, callback) {
  loaderContext.loaderIndex = loaderContext.loaders.length - 1;
  if(loaderContext.resourcePath) {
  iterateNormalLoaders(options, loaderContext, [buffer], callback);
  } else {
    iterateNormalLoaders(options, loaderContext, [null], callback);
  }
}
// 倒序執(zhí)行所有 loader
function iterateNormalLoaders(options, loaderContext, args, callback) {
  // 執(zhí)行完所有 loader return,去執(zhí)行 callback 即 runLoaders 的回調(diào)
  if(loaderContext.loaderIndex < 0) return callback(null, args);
  // 獲取當(dāng)前 loader 模塊對象
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  // 如果當(dāng)前 loader 的 normal 階段已經(jīng)執(zhí)行過印衔,則繼續(xù)迭代:
  // 減序后遞歸執(zhí)行前一個 loader
  if(currentLoaderObject.normalExecuted) {
    loaderContext.loaderIndex--;
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }
  var fn = currentLoaderObject.normal;
  currentLoaderObject.normalExecuted = true;
  if(!fn) {
    return iterateNormalLoaders(options, loaderContext, args, callback);
  }
  // 執(zhí)行 loader 函數(shù)
  runSyncOrAsync(fn, loaderContext, args, function(err) {
    var args = Array.prototype.slice.call(arguments, 1); // arg:[] 為 loader 轉(zhuǎn)換結(jié)果(String或者Buffer+可能的SourceMap)
    iterateNormalLoaders(options, loaderContext, args, callback); // 遞歸執(zhí)行 loader啡捶,將 loader 轉(zhuǎn)換結(jié)果一并傳入
  });
}

exports.runLoaders = function runLoaders(options, callback) {
  // 讀取 options
  var resource = options.resource || "";
  var loaders = options.loaders || [];
  var loaderContext = options.context || {};
  var readResource = options.readResource || readFile;
  // 準(zhǔn)備 loader 對象
  loaders = loaders.map(createLoaderObject);
  loaderContext.loaderIndex = 0;
  loaderContext.loaders = loaders;
  // ... 
  var processOptions = {
    resourceBuffer: null,
    readResource: readResource
  };
  iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {});
}
4.3 parse 階段,收集依賴

調(diào)用parser將上一步runLoaders的編譯結(jié)果利用 acorn 庫轉(zhuǎn)換為 ast当编。生成的 AST 劃分為三部分:ImportDeclaration届慈、FunctionDeclarationVariablesDeclaration。?? 遍歷 ast忿偷,根據(jù)導(dǎo)入導(dǎo)出及異步的情況觸發(fā)相關(guān)鉤子插件來收集依賴金顿,這些依賴用于解析遞歸依賴和模板操作 ?? 根據(jù)每個 module 的相關(guān)信息生成各自唯一的 buildHash

runLoaders運(yùn)行后的回調(diào)里執(zhí)行了createSource,然后判斷經(jīng) loaders 編譯的 result 是否有第三個參數(shù)(為 object 格式)并且含有 webpackAST 屬性鲤桥,如果都符合則將 webpackAST 的值賦值到 _ast 上揍拆。

然后執(zhí)行doBuild的回調(diào),根據(jù)項目配置項判斷是否需要 parse茶凳,若需要則執(zhí)行:

// /lib/NormalModule.js
const result = this.parser.parse(
  // 如果 this._ast 不存在則傳 this._source._value嫂拴,即代碼字符串
  this._ast || this._source.source(),
  {
    current: this,
    module: this,
    compilation: compilation,
    options: options
  },
  (err, result) => {
    handleParseResult(result);
  }
);

this.parser 即是 resolve 階段最終得到對象里的 parser播揪,即 NormalModuleFactory 的 getParser 方法,它調(diào)用 NormalModuleFactory.hooks: createParser, parse筒狠。
編譯前準(zhǔn)備注冊的 webpack 默認(rèn)插件 JavascriptModulesPlugin 監(jiān)聽了 createParser 鉤子猪狈,提供了 /lib/Parser.js 的實(shí)例。

// /lib/JavascriptModulesPlugin.js`
const Parser = require("./Parser");
normalModuleFactory.hooks.createParser
  .for("javascript/auto")
  .tap("JavascriptModulesPlugin", options => {
    return new Parser(options, "auto");
  });
// /lib/Parser.js
const acorn = require("acorn"); // node_modules/acorn/dist/acorn.js
const acornParser = acorn.Parser;
class Parser extends Tapable {
  parse(source, initialState) { // 提供給 Parser 實(shí)例的 parse 方法
    ast = Parser.parse(source, {...}); // 執(zhí)行 Parser 類的靜態(tài)方法
    // 觸發(fā) program 鉤子上的插件(HarmonyDetectionParserPlugin 和 UseStrictPlugin) 回調(diào)
    // 根據(jù)是否有 import/export 和 use strict 增加依賴:HarmonyCompatibilityDependency, HarmonyInitDependency辩恼,ConstDependency
    if (this.hooks.program.call(ast, comments) === undefined) {
      this.detectMode(ast.body); // 檢測當(dāng)前執(zhí)行塊是否有 use strict雇庙,并設(shè)置 this.scope.isStrict = true
      this.prewalkStatements(ast.body); // 處理 import 進(jìn)來的變量,是 import 就增加依賴 HarmonyImportSideEffectDependency灶伊,HarmonyImportSpecifierDependency;
      // 處理 export 出去的變量疆前,是 export 就增加依賴 HarmonyExportHeaderDependency,HarmonyExportSpecifierDependency聘萨;還會處理其他相關(guān)導(dǎo)入導(dǎo)出的變量
      this.blockPrewalkStatements(ast.body); // 處理塊遍歷
      this.walkStatements(ast.body); // 深入函數(shù)內(nèi)部在 walkFunctionDeclaration 進(jìn)行遞歸竹椒,繼續(xù)查找 ast 上的依賴,異步此處深入會增加依賴 ImportDependenciesBlock
    }
  }
  static parse(code, options) {
    try { // acorn 的 parse
      ast = acornParser.parse(code, parserOptions);
    } catch (e) {}
  }
}

由上分析可見:this.parser.parse就是 Parser 實(shí)例的原型方法米辐,而實(shí)際的處理函數(shù)是 acorn 庫提供的胸完。由此通過 acorn.Parser.parse 方法等一系列處理,得到了源碼對應(yīng)的 ast翘贮。

觸發(fā) Parser 的 program 鉤子舶吗,根據(jù) import/export 即模塊間的相互依賴關(guān)系遍歷 ast 收集依賴,之后在對應(yīng)的module.dependencies上增加相應(yīng)的依賴择膝。
在后面 generate / render 階段,會調(diào)用這些依賴 (dependencies) 對應(yīng)的 template.apply 來渲染生成代碼資源检激。(放在本文最后結(jié)合示例截圖說明)

parse 處理完畢后肴捉,執(zhí)行handleParseResult,調(diào)用this._initBuildHash(compilation)叔收。采用 nodeJS 提供的加密模塊 crypto 進(jìn)行 hash 加密齿穗,將結(jié)果賦值給this._buildHash

// /lib/NormalModule.js
_initBuildHash(compilation) {
  // createHash 即 new BulkUpdateDecorator(require("crypto").createHash(algorithm))
  const hash = createHash(compilation.outputOptions.hashFunction);
  if (this._source) {
    hash.update("source"); // 更新 hash source 內(nèi)容
    this._source.updateHash(hash); // this._value
  }
  hash.update("meta"); // 更新 hash meta 內(nèi)容
  hash.update(JSON.stringify(this.buildMeta)); // 更新 hash this.buildMeta
  this._buildHash = /** @type {string} */ (hash.digest("hex")); // 得到 hash 值
}

又回到 Compilation.js 執(zhí)行module.build()的回調(diào)饺律,按照在文件中出現(xiàn)的先后順序?qū)?code>module.dependencies進(jìn)行排序窃页,然后觸發(fā) Compilation.hooks: succeedModule。接著執(zhí)行this.buildModule的回調(diào)复濒,運(yùn)行afterBuild()

4.4 遞歸處理依賴階段 (重復(fù)以上步驟)

根據(jù) module 間的相互依賴關(guān)系脖卖,遞歸解析所有依賴 module。即 resolve ?? 執(zhí)行 loader ?? parse ?? 收集并處理該模塊依賴的模塊巧颈,直到所有入口依賴 (直接或間接) 的文件都經(jīng)過了這些步驟的處理畦木。最終返回一個入口 module。

// /lib/Compilation.js
_addModuleChain(context, dependency, onModule, callback) {
  // ...
  moduleFactory.create(
    context,
    entry,
    module => this.entries.push(module), // 提供把 module 添加 compilation.entries 的方法
    (err, module) => { 
      const afterBuild = () => {
        // 在 this.addModule(module) 時如果發(fā)現(xiàn)`module.request`存在`identifier`標(biāo)識砸泛,則會設(shè)置 addModuleResult.dependencies 為 false十籍,即可避免該模塊被重復(fù)解析/創(chuàng)建
        if (addModuleResult.dependencies) { // 如果該模塊是首次解析蛆封,即從未被添加過
          this.processModuleDependencies(module, err => { // 去處理依賴
            if (err) return callback(err);
            callback(null, module); // 8. 執(zhí)行 addEntry 方法中 this._addModuleChain 的回調(diào),生成一個入口 module勾栗。歸根究底起來就是 hooks.make 鉤子的回調(diào)惨篱,調(diào)用 compilation.finish 方法
          });
        } else {
          return callback(null, module);
        }
      }
  );
}

processModuleDependencie會分別處理 module 的 dependencies、blocks (import()引入的異步依賴)和 variables(內(nèi)部變量 __resourceQuery)围俘,其中 blocks 會遞歸調(diào)用處理砸讳。整理過濾出無 Identifier 標(biāo)識的 module,得到處理結(jié)果 sortedDependencies楷拳。
跟著調(diào)用this.addModuleDependencies(module, sortedDependencies, this.bail, null, true, callback) 并傳入 module 和 sortedDependencies绣夺。

demo 的 a.js 得到的 sortedDependencies
// /lib/Compilation.js
addModuleDependencies(module, dependencies, bail, cacheGroup, cacheGroup, callback) {
  asyncLib.forEach(
    dependencies,
    (item, callback) => { // callback 是當(dāng)前 item 所有迭代功能完成或發(fā)生錯誤時調(diào)用的回調(diào),調(diào)用 callback() 可以手動觸發(fā)
      const dependencies = item.dependencies;
      const semaphore = this.semaphore;
      semaphore.acquire(() => { // 并發(fā)編譯隊列控制
        const factory = item.factory;
        // 1. 并行調(diào)用每個依賴的 NormalModuleFactory.create
        factory.create({...}, (err, dependentModule) => {
          // 在經(jīng)過`factory.create`的 2. resolve 階段 ?? 3. 初始化`module` 后`create`完成欢揖,開始執(zhí)行`create`的回調(diào)
          // 錯誤處理等...
          if (!dependentModule) { // 如果 create 沒有返回 module
            semaphore.release();
            return process.nextTick(callback);
          }
          const iterationDependencies = depend => {
            for (let index = 0; index < depend.length; index++) {
              const dep = depend[index];
              dep.module = dependentModule;
              dependentModule.addReason(module, dep);
            }
          };
          
          // 4. 執(zhí)行 addModule陶耍,得到處理后的包含當(dāng)前 module 信息的對象
          const addModuleResult = this.addModule(dependentModule,cacheGroup);
          dependentModule = addModuleResult.module; 
          iterationDependencies(dependencies);

          const afterBuild = () => {
            if (recursive && addModuleResult.dependencies) { // 7. 如果是遞歸遍歷(調(diào)用 addModuleDependencies 時傳的 recursive 是 true )且該模塊從未被添加過
              // 執(zhí)行 processModuleDependencies 處理該模塊的依賴,再將流程遞歸走下去
              // 第一次肯定是走這里她混,這時傳遞的 callback 是 asyncLib.forEach 的回調(diào)烈钞,這個回調(diào)不出錯的話,調(diào)用后是在當(dāng)前輪依賴遍歷完執(zhí)行的
              this.processModuleDependencies(dependentModule, callback);
            } else {
              return callback(); // 7. 如該模塊已被添加過坤按,則等本輪迭代任務(wù)執(zhí)行完再執(zhí)行 asyncLib.forEach 的回調(diào)
            }
          };
          if (addModuleResult.build) { // 5. 執(zhí)行 buildModule
            this.buildModule(dependentModule, ... module, dependencies, err => {
              semaphore.release();
              afterBuild(); // 6. 執(zhí)行 afterBuild()
            });
          } 
        });
      });
    },
    err => {
      // 錯誤是在一個 Compilation 的引用的閉包中創(chuàng)建的毯欣,因此 errors 會暴露出 Compilation 對象。
      if (err)  return callback(err);
      // 當(dāng)?shù)鱜(item, callback) => {}`的第二個參數(shù)(回調(diào))被調(diào)用臭脓,會觸發(fā)這里酗钞;如果調(diào)用的時候傳遞了參數(shù)`callback(sth)`,那么這個參數(shù)會被作為 err 值傳遞
      // 這里的 callback 是`addModuleDependencies`的最后一個參數(shù)来累,也就是`_addModuleChain`內(nèi)的 build 完成后 this.processModuleDependencies(...) 傳的那個函數(shù)砚作,見上方代碼塊 8.標(biāo)注...
      // process.nextTick 是把 callback 放到當(dāng)前宏任務(wù)出棧前執(zhí)行,即當(dāng)前模塊的依賴遍歷完 add 完執(zhí)行
      // 面遞歸處理依賴就是 asyncLib.forEach 的 callback了嘹锁,就是把它上一輪模塊遍歷完成的回調(diào)一直放到到下一棧宏任務(wù)開始前執(zhí)行葫录,就一直套娃放 callback 直到所有模塊都被添加過,8.標(biāo)注的回調(diào)執(zhí)行
      return process.nextTick(callback);
    }
  );
}

這里的 asyncLib.forEach 就是 neo-async 庫的 each 方法 领猾。它一般用來對集合進(jìn)行異步迭代米同,它的回調(diào)(最后一個參數(shù)即err=> {}部分)傳給了 iterator 迭代器(第二個參數(shù)的最后一個參數(shù)),在迭代器函數(shù)內(nèi)手動調(diào)用這個回調(diào)的話摔竿,會在傳遞 err 或 iterator 全部執(zhí)行完成后執(zhí)行該回調(diào)面粮。詳細(xì)可以看這里 ?? 詳細(xì)說明
以及 process.nextTick 的用法 ?? 理解 process.nextTick()

它并行調(diào)用每個依賴的 NormalModuleFactory.create(),與前文 【執(zhí)行 loader 階段拯坟,初始化模塊 module】部分提到的moduleFactory.create功能一致但金,因此重復(fù)為每個依賴走以下流程:

1. 執(zhí)行 NormalModuleFactory.create ?? 2. resolve 階段 ?? 3. 初始化 module ?? 4. NormalModuleFactory.create 完成,執(zhí)行它的回調(diào): 主要內(nèi)容為 addModule ?? 5. buildModule ?? 6. afterBuild ?? 7. 如果該模塊從未 add 過則走 processModuleDependencies 處理依賴郁季,繼續(xù)遞歸 asyncLib.forEach 并行流程冷溃。

就這樣钱磅,從入口module開始,根據(jù)module之間的依賴關(guān)系似枕,遞歸將所有的module都轉(zhuǎn)換編譯盖淡。
直到層層依賴都轉(zhuǎn)換完成,執(zhí)行return process.nextTick(callback);凿歼,將在下一次事件循環(huán)tick之前調(diào)用 callback褪迟,即執(zhí)行_addModuleChainafterBuild方法的this.processModuleDependencies的回調(diào),即this._addModuleChain傳入的回調(diào)函數(shù):
未出錯的話能拿到一個入口 module

我們可以看到入口模塊的 dependencies 和 blocks 存放了名為"HarmonyCompatibilityDependency"答憔、"HarmonyExportHeaderDependency"味赃、"ImportDependenciesBlock"之類的依賴。上文我們提到在 render 階段會調(diào)用這些依賴對應(yīng)的模版來生成代碼資源虐拓,這里對這些依賴作一個簡單的解釋:

  • HarmonyCompatibilityDependency:對應(yīng)模板 HarmonyExportDependencyTemplate心俗,會在源碼的最前面添加像:__webpack_require__.r(__webpack_exports__); 這樣的代碼,用于定義 exports:__esModule
  • HarmonyInitDependency:對應(yīng)模板HarmonyInitDependencyTemplate
  • ConstDependency:對應(yīng)模板ConstDependencyTemplate蓉驹,會在源碼里將同步 import 語句刪掉
  • HarmonyImportSideEffectDependency":對應(yīng)模板HarmonyImportSideEffectDependencyTemplate城榛,調(diào)用父類 HarmonyImportDependencyTemplate 的 apply,即為空
  • HarmonyImportSpecifierDependency:對應(yīng)模板HarmonyImportSpecifierDependencyTemplate态兴,會在源碼里將引入的變量替換為 webpack 對應(yīng)的包裝變量
  • HarmonyExportHeaderDependency:對應(yīng)模板HarmonyExportDependencyTemplate狠持,會在源碼里將關(guān)鍵字 export 刪掉
  • HarmonyExportSpecifierDependency:對應(yīng)模板HarmonyExportSpecifierDependencyTemplate,執(zhí)行 apply 為空
  • ImportDependenciesBlock(異步模塊):對應(yīng)模板ImportDependencyTemplate瞻润, 會在源碼里將本 demo 中的 import('./c.js')替換為 Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))

再觸發(fā)compilation.hooks: succeedEntry喘垂,最后執(zhí)行調(diào)用compilation.addEntry時傳入的回調(diào),到此 module 生成結(jié)束绍撞。

下文:淺析 webpack 打包流程(原理) 三 - 生成 chunk

webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
淺析 webpack 打包流程(原理) 六 - 生成文件

參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實(shí)踐篇)
webpack 4 源碼主流程分析
[萬字總結(jié)] 一文吃透 Webpack 核心原理
`

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末王污,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子楚午,更是在濱河造成了極大的恐慌,老刑警劉巖尿招,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矾柜,死亡現(xiàn)場離奇詭異,居然都是意外死亡就谜,警方通過查閱死者的電腦和手機(jī)怪蔑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧荐,“玉大人缆瓣,你說我怎么就攤上這事『缤常” “怎么了弓坞?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵隧甚,是天一觀的道長。 經(jīng)常有香客問我渡冻,道長戚扳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任族吻,我火速辦了婚禮帽借,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘超歌。我一直安慰自己砍艾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布巍举。 她就那樣靜靜地躺著脆荷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪禀综。 梳的紋絲不亂的頭發(fā)上简烘,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機(jī)與錄音定枷,去河邊找鬼孤澎。 笑死,一個胖子當(dāng)著我的面吹牛欠窒,可吹牛的內(nèi)容都是我干的覆旭。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼岖妄,長吁一口氣:“原來是場噩夢啊……” “哼型将!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起荐虐,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤七兜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后福扬,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕铸,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年铛碑,在試婚紗的時候發(fā)現(xiàn)自己被綠了狠裹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡汽烦,死狀恐怖涛菠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤俗冻,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布礁叔,位于F島的核電站,受9級特大地震影響言疗,放射性物質(zhì)發(fā)生泄漏晴圾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一噪奄、第九天 我趴在偏房一處隱蔽的房頂上張望死姚。 院中可真熱鬧,春花似錦勤篮、人聲如沸都毒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽账劲。三九已至,卻和暖如春金抡,著一層夾襖步出監(jiān)牢的瞬間瀑焦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工梗肝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留榛瓮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓巫击,卻偏偏與公主長得像禀晓,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子坝锰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容