浅析 webpack 打包流程(原理) 二 - 递归构建 module

接上文:浅析 webpack 打包流程(原理) 一 - 准备工作

四、递归编译生成 module 实例

4.1 resolve 阶段,解析返回包含当前模块所有信息的一个对象

此阶段概述:利用 enhanced-resolve 库,得到 resolve 解析方法 ➡️ 解析 inline loader 和它对应资源的 resource,还有项目config的 loader,然后对所有 loader 进行合并、排序 ➡️ 得到 module 对应的 parser 和 generator,用于后面的 ast 解析及模板生成 ➡️ 输出一个包含当前模块上下文、loaders、绝对路径、依赖等 module 所有信息的组合对象,提供给 afterResolve 钩子触发后的回调。这个对象下一步会被用来初始化当前文件 的 module 实例。

上一步我们已经得知 moduleFactory 就是 normalModuleFactory,那么接着看 normalModuleFactory 的 create 方法:
触发normalModuleFactory.hooks:beforeResolve,在回调里触发NormalModuleFactory.hooks:factory钩子,再执行该 factory 函数,即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); // 触发 resolver 钩子返回一个 resolver 函数
    resolver(result, (err, data) => {
      //...
      this.hooks.afterResolve.callAsync(data, (err, result) => {
        let createdModule = this.hooks.createModule.call(result);
        if (!createdModule) {
          // 创建 normalModule 实例
          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) => {
    // 触发 NormalModuleFactory.hooks: factory 
    const factory = this.hooks.factory.call(null);
    factory(result, (err, module) => {
      //...
    });
}

上面的 resolver 函数负责解析 构建 module 所需 loaders 的绝对路径 以及每个 module 的相关构建信息(如获取 module 的 packge.json 等)。
this.getResolver 即/lib/ResolverFactory.js的 get 方法,判断如有缓存返回缓存,无则执行 _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  导出的 createResolver 方法
  const resolver = Factory.createResolver(resolveOptions);
  // 利用 enhanced-resolve 库注册完钩子插件后触发 ResolverFactory 的 resolver 钩子
  this.hooks.resolver.for(type).call(resolver, resolveOptions);
  return resolver;
}

编译前准备我们通过WebpackOptionsApply.jsResolverFactory.hooks: resolveOptions钩子上注册了绑定事件,此刻触发后用 cachedCleverMerge 判断缓存及融合配置(如果 type 是 loader 则为 配置项: options.resolveLoader,如果是 normal 则为 配置项: options.resolve),并添加属性 fileSystem: compiler.inputFileSystem,最终返回一个 resolveOptions 对象,作为 Factory.createResolver 执行的参数。
enhanced-resolve createResolver 方法内,先融合处理了项目配置 resolve 与默认配置 resolve/resolveLoader,如未传入项目的 resolver,就自己 new 一个。接着定义了 Resolver 的生命周期钩子并根据配置 push 了一大堆 plugins 实例。然后对每一个插件执行 apply,在 Resolver 不同生命周期钩子上注册一些方法,并在函数末尾执行:

// node_modules/enhanced-resolve/lib/xxxPlugin.js
// 获取hooks,target 为事件钩子
const target = resolver.ensureHook(this.target);
// 触发插件后的回调里,执行:
resolver.doResolve(target, obj, ...);

在触发完当前插件后,会通过 doResolve 将 hook 带入到下一个插件中,实现递归串联调用一系列的插件,包括 UnsafeCachePlugin、ParsePlugin、DescriptionFilePlugin、ModuleKindPlugin 等等,来完成各自的操作。

再回到 NormalModuleFactory.hooks: resolver,拿到 loaderResolvernormalResolver,用于解析路径。

接下来进行 inline loader 和对应资源文件 resource 的解析:
比如import Styles from style-loader!css-loader?modules!./styles.css会被解析成:

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

然后执行asyncLib.parallel(...),它会并行处理参数数组各个任务,都完成之后返回一个 results 列表,列表顺序为参数数组顺序,与执行顺序无关。
得到的 results:

{
  "results": [
    [
      {
        "loader": "loader的绝对路径1",
        "options": "loader参数1"
      },
      {
        "loader": "loader的绝对路径2",
        "options": "loader参数2"
      }
    ],
    {
      "resource": "模块绝对路径",
      "resourceResolveData": "模块基本信息(即enhanced-resolve执行结果)"
    }
  ]
}

const result = this.ruleSet.exec({...}) 解析 config module rules 里的 loader,递归过滤匹配出对应的 loader:

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

对 loader 进行合并、排序
接着处理inline loader带有前缀!,!!,-!result项带有enforce参数的情况,用来决定怼 loader的禁用和排序。

又通过 asyncLib.parallel 与 this.resolveRequestArray 并行处理上一步得到的useLoadersPost、useLoadersPre、useLoaders,拿到对应的 resolve 结果即路径信息,再在回调里排序、合并,即 loaders 配置顺序为 postLoader,inlineLoader,loader(normal),preLoader,执行顺序则相反。

最后输出以下组合对象:

// /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), // 创建 parser 并缓存
  generator: this.getGenerator(type, settings.generator), // 创建 generator 并缓存
  resolveOptions
});

其中 getParser 的主要作用是为 module 提供解析模块为 ast 的 parser。
createParser 时会根据不同 type 返回不同的 parser 实例。
getGenerator 主要作用是为 module 提供模版生成时的 generator (的)方法。
createGenerator 时根据 type 不同返回不同的 generator 实例(目前代码里都是返回一致的 new JavascriptGenerator() )。

跳出 NormalModuleFactory 的 resolver 钩子函数,执行 resolver 函数回调,至此 resolve 流程结束。

4.2 执行 loader 阶段,初始化模块 module,并用 loader 倒序转译

开启构建 module 流程。 new NormalModule(result)得到初始化的 module ➡️ 在 build 过程中执行 runLoaders 处理源码,先正序读取每个 loader 并执行它的 pitch,再倒序执行每个 loader 的 normal,最后得到一个编译后的字符串或 Buffer。

(继续看/lib/NormalModuleFactory.js) 触发 normalModuleFactory.hooks:afterResolve 和 normalModuleFactory.hooks:createModule,let createdModule = this.hooks.createModule.call(result);的这个 result 参数就是normalModuleFactory.hooks.resolver.tap 输出的组合 object。如果不存在项目配置的自定义 module,就使用new NormalModule(result)生成的 module。

跳出 factory 钩子 tap 绑定的函数,执行factory(result, (err, module) => {})的回调,传入的 module 就是我们初始化的 NormalModule 实例,进行依赖缓存后,结束 create 方法,回到/lib/Compilation.js执行 moduleFactory.create 的回调。

// /lib/Compilation.js
addModule(module, cacheGroup) {
  const identifier = module.identifier(); // 即 module.request
  //  根据 identifie 判断`compilation._modules`是否有该 module
  const alreadyAddedModule = this._modules.get(identifier);
  if (alreadyAddedModule) { // 如果已经存在则返回如下 object
    return {
      module: alreadyAddedModule,
      issuer: false,
      build: false,
      dependencies: false
    };
  }
  // ...
  // 将这个 module 保存到全局的 `Compilation`的`modules` 数组和`_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 执行完的回调
    // 用初始化的 module 作为参数调用 addModule
    const addModuleResult = this.addModule(module);
    module = addModuleResult.module;
    // 如果是入口文件还会将 module 保存到 `Compilation.entries`
    onModule(module);
    dependency.module = module;
    module.addReason(null, dependency); // 添加该`module`被哪些模块依赖的信息,会存到 module.reasons 数组里
    if (addModuleResult.build) { // 没有添加过的模块 build 属性默认是 true
      this.buildModule(module, false, null, null, err => {
        afterBuild();
      })
    }
  })
}

先执行this.addModule,返回一个对象 addModuleResult
如果这个 module 之前未被添加到compilation,将它保存到全局compilation对象的modules 数组和_modules Map 对象中,返回结果的 module 属性为当前模块,issuer、build、dependencies 的值都为 true;如果已存在,则 module 属性为查到的值,其他三个属性都为 false。

调用this.buildModule进入 build 阶段。做了回调缓存后,触发compilation.hooks:buildModule,然后执行module.build()

module 是 NormalModule 的实例,我们来到/lib/NormalModule.js看 build 方法:在设置一些属性后调用了 NormalModule 的 doBuild 方法。

// /lib/NormalModule.js
doBuild(options, compilation, resolver, fs, callback) {
  // 为所有的 loader 提供上下文环境
  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,作用是按规定流程执行各种 loader,将模块源码后处理成一个 String 或 Buffer 格式的 JavaScript (可能还有个 SourceMap)。
关于 loader 本身的机制可以看下这篇: webpack 之 Loader 详解

主要流程
runLoaders ➡️ iteratePitchingLoaders (正序 require 每个 loader) ➡️ loadLoader (将当前 loader 的模块导出函数赋值到loaderContext.loaders[index].normalloader 模块的pitch 函数赋值到loaderContext.loaders[index].pitch,然后执行pitch 函数[如果有的话]) ➡️ 读取完当前模块的全部 loader,执行 processResource (设置 loaderIndex 为最后一个 loader 的 index / 转换 buffer) ➡️ iterateNormalLoaders (倒序执行所有 loader [normal])

其中执行 pitchnormal 都调用了 runSyncOrAsync (同步或者异步执行 loader) 方法,如果在 iteratePitchingLoaders 阶段某个 pitch 有返回值,则直接进入 iterateNormalLoaders 阶段 (将该pitch返回值作为参数),从前一个读取的 loader 开始倒序执行

// node_modules/loader-runner/lib/LoaderRunner.js
// 同步或者异步执行 loader 函数
function runSyncOrAsync(fn, context, args, callback) {
  try {
    var result = (function LOADER_EXECUTION() {
      return fn.apply(context, args); // 执行 loader 函数,参数传递前一个 loader 的执行结果
    })();
    if (isSync) {
      // ...
      return callback(null, result);
    }
  } catch(e) {
    callback(e)
  }
}
// 核心方法,按正序 require 每个 loader
function iteratePitchingLoaders(options, loaderContext, callback) {
  // 发现读取完所有 loader 后,执行 processResource 方法
  // 第一次执行 loaderIndex 是 0,如果 loaders 数组的个数是 0 才走processResource,如果当前模块有 loader 则继续往下走。
  if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback);
  // 根据 loaderIndex 获取当前要读取的 loader 对象
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];

  // 如果当前 loader 的 pitch 阶段已经执行过,则继续迭代执行
  if(currentLoaderObject.pitchExecuted) {
    // 增序后递归读取下一个 loader
    loaderContext.loaderIndex++;
    return iteratePitchingLoaders(options, loaderContext, callback);
  }
  // node_modules/loader-runner/lib/loadLoader.js
  // loadLoader 这个方法负责加载当前 loader 模块,将 loader 模块导出的函数赋值到  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 函数直接 require 下一个 loader
    if(!fn) return iteratePitchingLoaders(options, loaderContext, callback);
    // 有 pitch 则执行 pitch 函数,根据 runSyncOrAsync 的回调在没报错的情况下有无返回其他参数,决定是否继续读取剩下的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) { // 执行 pitch 有返回结果则将 loaderIndex 减序,并将返回结果作为 iterateNormalLoaders 的参数,开始倒序执行前面已经 require 的 loader
          loaderContext.loaderIndex--; // 这个减序是为了从前一个读取的 loader 开始执行,不执行当前 loader 的 normal
          iterateNormalLoaders(options, loaderContext, args, callback);
        } else { // pitch 没有返回值继续读取下一个 loader
          iteratePitchingLoaders(options, loaderContext, callback);
        }
      }
    );
  });
}
// 设置 loaderIndex 为最后一个 loader 的 index
// 转换 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);
  }
}
// 倒序执行所有 loader
function iterateNormalLoaders(options, loaderContext, args, callback) {
  // 执行完所有 loader return,去执行 callback 即 runLoaders 的回调
  if(loaderContext.loaderIndex < 0) return callback(null, args);
  // 获取当前 loader 模块对象
  var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex];
  // 如果当前 loader 的 normal 阶段已经执行过,则继续迭代:
  // 减序后递归执行前一个 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);
  }
  // 执行 loader 函数
  runSyncOrAsync(fn, loaderContext, args, function(err) {
    var args = Array.prototype.slice.call(arguments, 1); // arg:[] 为 loader 转换结果(String或者Buffer+可能的SourceMap)
    iterateNormalLoaders(options, loaderContext, args, callback); // 递归执行 loader,将 loader 转换结果一并传入
  });
}

exports.runLoaders = function runLoaders(options, callback) {
  // 读取 options
  var resource = options.resource || "";
  var loaders = options.loaders || [];
  var loaderContext = options.context || {};
  var readResource = options.readResource || readFile;
  // 准备 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 阶段,收集依赖

调用parser将上一步runLoaders的编译结果利用 acorn 库转换为 ast。生成的 AST 划分为三部分:ImportDeclarationFunctionDeclarationVariablesDeclaration。➡️ 遍历 ast,根据导入导出及异步的情况触发相关钩子插件来收集依赖,这些依赖用于解析递归依赖和模板操作 ➡️ 根据每个 module 的相关信息生成各自唯一的 buildHash

runLoaders运行后的回调里执行了createSource,然后判断经 loaders 编译的 result 是否有第三个参数(为 object 格式)并且含有 webpackAST 属性,如果都符合则将 webpackAST 的值赋值到 _ast 上。

然后执行doBuild的回调,根据项目配置项判断是否需要 parse,若需要则执行:

// /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 方法,它调用 NormalModuleFactory.hooks: createParser, parse
编译前准备注册的 webpack 默认插件 JavascriptModulesPlugin 监听了 createParser 钩子,提供了 /lib/Parser.js 的实例。

// /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 实例的 parse 方法
    ast = Parser.parse(source, {...}); // 执行 Parser 类的静态方法
    // 触发 program 钩子上的插件(HarmonyDetectionParserPlugin 和 UseStrictPlugin) 回调
    // 根据是否有 import/export 和 use strict 增加依赖:HarmonyCompatibilityDependency, HarmonyInitDependency,ConstDependency
    if (this.hooks.program.call(ast, comments) === undefined) {
      this.detectMode(ast.body); // 检测当前执行块是否有 use strict,并设置 this.scope.isStrict = true
      this.prewalkStatements(ast.body); // 处理 import 进来的变量,是 import 就增加依赖 HarmonyImportSideEffectDependency,HarmonyImportSpecifierDependency;
      // 处理 export 出去的变量,是 export 就增加依赖 HarmonyExportHeaderDependency,HarmonyExportSpecifierDependency;还会处理其他相关导入导出的变量
      this.blockPrewalkStatements(ast.body); // 处理块遍历
      this.walkStatements(ast.body); // 深入函数内部在 walkFunctionDeclaration 进行递归,继续查找 ast 上的依赖,异步此处深入会增加依赖 ImportDependenciesBlock
    }
  }
  static parse(code, options) {
    try { // acorn 的 parse
      ast = acornParser.parse(code, parserOptions);
    } catch (e) {}
  }
}

由上分析可见:this.parser.parse就是 Parser 实例的原型方法,而实际的处理函数是 acorn 库提供的。由此通过 acorn.Parser.parse 方法等一系列处理,得到了源码对应的 ast

触发 Parser 的 program 钩子,根据 import/export 即模块间的相互依赖关系遍历 ast 收集依赖,之后在对应的module.dependencies上增加相应的依赖
在后面 generate / render 阶段,会调用这些依赖 (dependencies) 对应的 template.apply 来渲染生成代码资源。(放在本文最后结合示例截图说明)

parse 处理完毕后,执行handleParseResult,调用this._initBuildHash(compilation)。采用 nodeJS 提供的加密模块 crypto 进行 hash 加密,将结果赋值给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 内容
    this._source.updateHash(hash); // this._value
  }
  hash.update("meta"); // 更新 hash meta 内容
  hash.update(JSON.stringify(this.buildMeta)); // 更新 hash this.buildMeta
  this._buildHash = /** @type {string} */ (hash.digest("hex")); // 得到 hash 值
}

又回到 Compilation.js 执行module.build()的回调,按照在文件中出现的先后顺序对module.dependencies进行排序,然后触发 Compilation.hooks: succeedModule。接着执行this.buildModule的回调,运行afterBuild()

4.4 递归处理依赖阶段 (重复以上步骤)

根据 module 间的相互依赖关系,递归解析所有依赖 module。即 resolve ➡️ 执行 loader ➡️ parse ➡️ 收集并处理该模块依赖的模块,直到所有入口依赖 (直接或间接) 的文件都经过了这些步骤的处理。最终返回一个入口 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) 时如果发现`module.request`存在`identifier`标识,则会设置 addModuleResult.dependencies 为 false,即可避免该模块被重复解析/创建
        if (addModuleResult.dependencies) { // 如果该模块是首次解析,即从未被添加过
          this.processModuleDependencies(module, err => { // 去处理依赖
            if (err) return callback(err);
            callback(null, module); // 8. 执行 addEntry 方法中 this._addModuleChain 的回调,生成一个入口 module。归根究底起来就是 hooks.make 钩子的回调,调用 compilation.finish 方法
          });
        } else {
          return callback(null, module);
        }
      }
  );
}

processModuleDependencie会分别处理 module 的 dependencies、blocks (import()引入的异步依赖)和 variables(内部变量 __resourceQuery),其中 blocks 会递归调用处理。整理过滤出无 Identifier 标识的 module,得到处理结果 sortedDependencies。
跟着调用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 是当前 item 所有迭代功能完成或发生错误时调用的回调,调用 callback() 可以手动触发
      const dependencies = item.dependencies;
      const semaphore = this.semaphore;
      semaphore.acquire(() => { // 并发编译队列控制
        const factory = item.factory;
        // 1. 并行调用每个依赖的 NormalModuleFactory.create
        factory.create({...}, (err, dependentModule) => {
          // 在经过`factory.create`的 2. resolve 阶段 ➡️ 3. 初始化`module` 后`create`完成,开始执行`create`的回调
          // 错误处理等...
          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. 执行 addModule,得到处理后的包含当前 module 信息的对象
          const addModuleResult = this.addModule(dependentModule,cacheGroup);
          dependentModule = addModuleResult.module; 
          iterationDependencies(dependencies);

          const afterBuild = () => {
            if (recursive && addModuleResult.dependencies) { // 7. 如果是递归遍历(调用 addModuleDependencies 时传的 recursive 是 true )且该模块从未被添加过
              // 执行 processModuleDependencies 处理该模块的依赖,再将流程递归走下去
              // 第一次肯定是走这里,这时传递的 callback 是 asyncLib.forEach 的回调,这个回调不出错的话,调用后是在当前轮依赖遍历完执行的
              this.processModuleDependencies(dependentModule, callback);
            } else {
              return callback(); // 7. 如该模块已被添加过,则等本轮迭代任务执行完再执行 asyncLib.forEach 的回调
            }
          };
          if (addModuleResult.build) { // 5. 执行 buildModule
            this.buildModule(dependentModule, ... module, dependencies, err => {
              semaphore.release();
              afterBuild(); // 6. 执行 afterBuild()
            });
          } 
        });
      });
    },
    err => {
      // 错误是在一个 Compilation 的引用的闭包中创建的,因此 errors 会暴露出 Compilation 对象。
      if (err)  return callback(err);
      // 当迭代器`(item, callback) => {}`的第二个参数(回调)被调用,会触发这里;如果调用的时候传递了参数`callback(sth)`,那么这个参数会被作为 err 值传递
      // 这里的 callback 是`addModuleDependencies`的最后一个参数,也就是`_addModuleChain`内的 build 完成后 this.processModuleDependencies(...) 传的那个函数,见上方代码块 8.标注...
      // process.nextTick 是把 callback 放到当前宏任务出栈前执行,即当前模块的依赖遍历完 add 完执行
      // 面递归处理依赖就是 asyncLib.forEach 的 callback了,就是把它上一轮模块遍历完成的回调一直放到到下一栈宏任务开始前执行,就一直套娃放 callback 直到所有模块都被添加过,8.标注的回调执行
      return process.nextTick(callback);
    }
  );
}

这里的 asyncLib.forEach 就是 neo-async 库的 each 方法 。它一般用来对集合进行异步迭代,它的回调(最后一个参数即err=> {}部分)传给了 iterator 迭代器(第二个参数的最后一个参数),在迭代器函数内手动调用这个回调的话,会在传递 err 或 iterator 全部执行完成后执行该回调。详细可以看这里 ➡️ 详细说明
以及 process.nextTick 的用法 ➡️ 理解 process.nextTick()

它并行调用每个依赖的 NormalModuleFactory.create(),与前文 【执行 loader 阶段,初始化模块 module】部分提到的moduleFactory.create功能一致,因此重复为每个依赖走以下流程:

1. 执行 NormalModuleFactory.create ➡️ 2. resolve 阶段 ➡️ 3. 初始化 module ➡️ 4. NormalModuleFactory.create 完成,执行它的回调: 主要内容为 addModule ➡️ 5. buildModule ➡️ 6. afterBuild ➡️ 7. 如果该模块从未 add 过则走 processModuleDependencies 处理依赖,继续递归 asyncLib.forEach 并行流程。

就这样,从入口module开始,根据module之间的依赖关系,递归将所有的module都转换编译。
直到层层依赖都转换完成,执行return process.nextTick(callback);,将在下一次事件循环tick之前调用 callback,即执行_addModuleChainafterBuild方法的this.processModuleDependencies的回调,即this._addModuleChain传入的回调函数:
未出错的话能拿到一个入口 module

我们可以看到入口模块的 dependencies 和 blocks 存放了名为"HarmonyCompatibilityDependency"、"HarmonyExportHeaderDependency"、"ImportDependenciesBlock"之类的依赖。上文我们提到在 render 阶段会调用这些依赖对应的模版来生成代码资源,这里对这些依赖作一个简单的解释:

  • HarmonyCompatibilityDependency:对应模板 HarmonyExportDependencyTemplate,会在源码的最前面添加像:__webpack_require__.r(__webpack_exports__); 这样的代码,用于定义 exports:__esModule
  • HarmonyInitDependency:对应模板HarmonyInitDependencyTemplate
  • ConstDependency:对应模板ConstDependencyTemplate,会在源码里将同步 import 语句删掉
  • HarmonyImportSideEffectDependency":对应模板HarmonyImportSideEffectDependencyTemplate,调用父类 HarmonyImportDependencyTemplate 的 apply,即为空
  • HarmonyImportSpecifierDependency:对应模板HarmonyImportSpecifierDependencyTemplate,会在源码里将引入的变量替换为 webpack 对应的包装变量
  • HarmonyExportHeaderDependency:对应模板HarmonyExportDependencyTemplate,会在源码里将关键字 export 删掉
  • HarmonyExportSpecifierDependency:对应模板HarmonyExportSpecifierDependencyTemplate,执行 apply 为空
  • ImportDependenciesBlock(异步模块):对应模板ImportDependencyTemplate, 会在源码里将本 demo 中的 import('./c.js')替换为 Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! ./c.js */ "./src/c.js"))

再触发compilation.hooks: succeedEntry,最后执行调用compilation.addEntry时传入的回调,到此 module 生成结束

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

webpack 打包流程系列(未完):
浅析 webpack 打包流程(原理) - 案例 demo
浅析 webpack 打包流程(原理) 一 - 准备工作
浅析 webpack 打包流程(原理) 二 - 递归构建 module
浅析 webpack 打包流程(原理) 三 - 生成 chunk
浅析 webpack 打包流程(原理) 四 - chunk 优化
浅析 webpack 打包流程(原理) 五 - 构建资源
浅析 webpack 打包流程(原理) 六 - 生成文件

参考鸣谢:
webpack打包原理 ? 看完这篇你就懂了 !
webpack 透视——提高工程化(原理篇)
webpack 透视——提高工程化(实践篇)
webpack 4 源码主流程分析
[万字总结] 一文吃透 Webpack 核心原理
`

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容