Webpack4源码解析之 compiler.run 做了什么

699 阅读16分钟

Webpack源码解析

// 使用webpack版本

"html-webpack-plugin": "^4.5.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"

打包主流程分析

调用run方法做了什么

1. run

run(callback) {
  // 如果有一个 Compilation 进行中,那么就不能开启第二个 Compilation
  // 因为webpack一次只允许有且仅有一个Compilation运行
  if (this.running) return callback(new ConcurrentCompilationError());

  // 运行完毕执行回调(如果存在回调)
  const finalCallback = (err, stats) => {
   // 表示一个Compilation已经完成
   this.running = false;

   // 如果Compilation失败调用failed钩子
   if (err) {
    this.hooks.failed.call(err);
   }

   // 成功后存在回调就执行它
   if (callback !== undefined) return callback(err, stats);
  };

  // 记录任务启动时间
  const startTime = Date.now();

  // 标记一个Compilation任务运行中
  this.running = true;

  // compiler 完成的回调,最终会产生一个compilation
  const onCompiled = (err, compilation) => {
   // 如果编译失败就调用finalCallback
   if (err) return finalCallback(err);

   // 如果不需要输出打包文件,就直接执行done钩子注册的函数,参数是打包统计数据
   // 执行完注册的钩子函数后执行最终的回调,将打包统计数据返回
   if (this.hooks.shouldEmit.call(compilation) === false) {
    const stats = new Stats(compilation);
    stats.startTime = startTime;
    stats.endTime = Date.now();
    this.hooks.done.callAsync(stats, err => {
     if (err) return finalCallback(err);
     return finalCallback(null, stats);
    });
    return;
   }

   // 调用输出assets方法,参数就是生成的compilation和最终回调
   this.emitAssets(compilation, err => {
    // 处理错误
    if (err) return finalCallback(err);

    // 处理二次打包
    if (compilation.hooks.needAdditionalPass.call()) {
     // 如果需要二次打包,标注为true
     compilation.needAdditionalPass = true;

     // 生成当前打包统计数据
     const stats = new Stats(compilation);
     stats.startTime = startTime;
     stats.endTime = Date.now();
     // 执行当前打包done钩子上注册的回调函数
     this.hooks.done.callAsync(stats, err => {
      if (err) return finalCallback(err);

      // 所有回调执行完毕执行additionalPass钩子,进行二次打包
      this.hooks.additionalPass.callAsync(err => {
       if (err) return finalCallback(err);
       this.compile(onCompiled);
      });
     });
     return;
    }

    // 不需要二次打包的话,就调用emitRecords记录输出
    this.emitRecords(err => {
     if (err) return finalCallback(err);

     const stats = new Stats(compilation);
     stats.startTime = startTime;
     stats.endTime = Date.now();
     // 执行当前打包done钩子上注册的回调函数
     // 执行完毕调用打包回调输出统计数据
     this.hooks.done.callAsync(stats, err => {
      if (err) return finalCallback(err);
      return finalCallback(null, stats);
     });
    });
   });
  };

  // 触发run之前的钩子,执行run之前注册的任务,参数是compiler实例
  // 在webpack/lib/node/NodeEnvironmentPlugin.js里面会在这个钩子上注册一个事件:
  // compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {
  //  if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();
  // });
  // 主要就是重置compiler的inputFileSystem,为下一次的compiler做准备
  this.hooks.beforeRun.callAsync(this, err => {
   if (err) return finalCallback(err);

   // 执行beforeRun完毕时立即执行run钩子上注册的任务
   // 在webpack/lib/CachePlugin.js里面会在这个run钩子上注册一个事件:
   // compiler.hooks.run.tapAsync("CachePlugin", (compiler, callback) => {
   //  if (!compiler._lastCompilationFileDependencies) {
   //   return callback();
   //  }
   //  ...
   // });
   // 主要是处理上一次compilation的缓存
   this.hooks.run.callAsync(this, err => {
    if (err) return finalCallback(err);

    // run钩子上注册的任务也执行完毕时读取记录进行编译
    this.readRecords(err => {
     if (err) return finalCallback(err);

     this.compile(onCompiled);
    });
   });
  });
}

run 也就是进行编译,生成 compilation ,run之前做了两个事情,也就是注册在 beforeRun上的重置 inputFileSystem 和注册在 run 上的用来处理缓存的 CachePlugin。这两件事情做完了后又调用了 readRecords 方法,然后才进行编译,下面我们就看一下 readRecords 方法。

2. readRecords

开启这个选项可以生成一个 JSON 文件,其中含有 webpack 的 "records" 记录 - 即「用于存储跨多次构建(across multiple builds)的模块标识符」的数据片段。可以使用此文件来跟踪在每次构建之间的模块变化。只要简单的设置一下路径,就可以生成这个 JSON 文件:

如果你使用了代码分离(code splittnig)这样的复杂配置,records 会特别有用。这些数据用于确保拆分 bundle,以便实现你需要的缓存(caching)行为。

注意,虽然这个文件是由编译器(compiler)生成的,但你可能仍然希望在源代码管理中追踪它,以便随时记录它的变化情况。

设置 recordsPath 本质上会把 recordsInputPath 和 recordsOutputPath 都设置成相同的路径。通常来讲这也是符合逻辑的,除非你决定改变记录文件的名称。

综上所述,records即上次编译打包的模块信息,为后续编译缓存做好准备,也能更方便、更准确地发现模块的变化。

首先我们需要配置records的路径 recordsPath: path.resolve(__dirname, './recordsPath.json'),这样在我们打包过后就会生成records记录文件:

{
  "HtmlWebpackCompiler": [
    {
      "modules": {
        "byIdentifier": {},
        "usedIds": {}
      },
      "chunks": {
        "byName": {},
        "bySource": {},
        "usedIds": []
      }
    }
  ],
  "modules": {
    "byIdentifier": {},
    "usedIds": {}
  },
  "chunks": {
    "byName": {},
    "bySource": {},
    "usedIds": []
  }
}

后续我们就可以在 compiler 之前获取到上一次编译的模块信息。

/**
  * 读取编译记录
  * @param {*} callback 也就是执行compiler
  * @returns compiler的返回值
  */
 readRecords(callback) {
  // 如果不存在records json文件或者未启用record,就设置一个空的record,执行compiler
  if (!this.recordsInputPath) {
   this.records = {};
   return callback();
  }
  // 如果存在路径,就检验路径是否有效(防止文件被删)
  this.inputFileSystem.stat(this.recordsInputPath, err => {
   // 如果无效直接执行compiler
   if (err) return callback();

   // 路径有效就读取record json
   this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => {
    // 读取失败抛出失败信息,不进行compiler
    if (err) return callback(err);

    // 读取内容转为一个对象,转换失败说明文件有问题,抛出错误信息
    try {
     this.records = parseJson(content.toString("utf-8"));
    } catch (e) {
     e.message = "Cannot parse records: " + e.message;
     return callback(e);
    }

    // 转换正常,records被正常赋值,执行compiler
    // callback ---> this.compile(onCompiled);
    return callback();
   });
  });
}

3. compile

records 读取完毕就执行 compiler,下面我们看一下这个 compile 方法:

// node_modules/webpack/lib/Compiler.js

createCompilation() {
  // 返回一个compilation实例
  return new Compilation(this);
}

newCompilation(params) {
  // 生成compilation实例
  const compilation = this.createCompilation();
  // 文件时间戳
  compilation.fileTimestamps = this.fileTimestamps;
  // 上下文时间戳(文件依赖关系)
  compilation.contextTimestamps = this.contextTimestamps;
  // 当前compilation名称(compiler的名称)
  compilation.name = this.name;
  // 上一次打包的记录
  compilation.records = this.records;
  // 编译依赖
  compilation.compilationDependencies = params.compilationDependencies;
  // 执行初始化compilation钩子上注册的任务
  this.hooks.thisCompilation.call(compilation, params);
  // 执行compilation钩子上注册的任务
  this.hooks.compilation.call(compilation, params);
  // 返回这个compilation
  return compilation;
}

createNormalModuleFactory() {
  const normalModuleFactory = new NormalModuleFactory(
    this.options.context,
    this.resolverFactory,
    this.options.module || {}
  );
  this.hooks.normalModuleFactory.call(normalModuleFactory);
  return normalModuleFactory;
}

createContextModuleFactory() {
  const contextModuleFactory = new ContextModuleFactory(this.resolverFactory);
  this.hooks.contextModuleFactory.call(contextModuleFactory);
  return contextModuleFactory;
}

newCompilationParams() {
  const params = {
    // 各类模块
    normalModuleFactory: this.createNormalModuleFactory(),
    // 模块依赖关系
    contextModuleFactory: this.createContextModuleFactory(),
    // 打包依赖
    compilationDependencies: new Set()
  };
  return params;
}

compile(callback) {
  // 生成编译参数
  const params = this.newCompilationParams();
  // 执行编译前钩子上注册的任务
  this.hooks.beforeCompile.callAsync(params, err => {
    if (err) return callback(err);
    // 执行compile钩子上注册的任务
    this.hooks.compile.call(params);
    // 生成一个compilation
    const compilation = this.newCompilation(params);
    // 执行make钩子上注册的异步任务,参数是这个compilation
    this.hooks.make.callAsync(compilation, err => {
      if (err) return callback(err);
      // 调用compilation上的finish方法,结束编译
      compilation.finish(err => {
        if (err) return callback(err);
        // 封装compilation结果
        compilation.seal(err => {
          if (err) return callback(err);
          // 调用编译后钩子上注册的任务
          this.hooks.afterCompile.callAsync(compilation, err => {
            if (err) return callback(err);
            // 最终调用onCompiled进行二次打包或者输出打包结果,整个compiler结束
            return callback(null, compilation);
          });
        });
      });
    });
  });
}

首先会生成一个编译参数,主要存放模块和模块的依赖关系、编译依赖:

{
  normalModuleFactory: NormalModuleFactory {
    _pluginCompat: SyncBailHook {
      _args: [Array],
      taps: [Array],
      interceptors: [],
      call: [Function: lazyCompileHook],
      promise: [Function: lazyCompileHook],
      callAsync: [Function: lazyCompileHook],
      _x: undefined
    },
    hooks: {
      resolver: [SyncWaterfallHook],
      factory: [SyncWaterfallHook],
      beforeResolve: [AsyncSeriesWaterfallHook],
      afterResolve: [AsyncSeriesWaterfallHook],
      createModule: [SyncBailHook],
      module: [SyncWaterfallHook],
      createParser: [HookMap],
      parser: [HookMap],
      createGenerator: [HookMap],
      generator: [HookMap]
    },
    resolverFactory: ResolverFactory {
      _pluginCompat: [SyncBailHook],
      hooks: [Object],
      cache1: [WeakMap],
      cache2: Map {}
    },
    ruleSet: RuleSet {
      references: {},
      rules: [Array]
    },
    cachePredicate: [Function: bound Boolean],
    context: '',
    parserCache: {},
    generatorCache: {}
  },
  contextModuleFactory: ContextModuleFactory {
    _pluginCompat: SyncBailHook {
      _args: [Array],
      taps: [Array],
      interceptors: [],
      call: [Function: lazyCompileHook],
      promise: [Function: lazyCompileHook],
      callAsync: [Function: lazyCompileHook],
      _x: undefined
    },
    hooks: {
      beforeResolve: [AsyncSeriesWaterfallHook],
      afterResolve: [AsyncSeriesWaterfallHook],
      contextModuleFiles: [SyncWaterfallHook],
      alternatives: [AsyncSeriesWaterfallHook]
    },
    resolverFactory: ResolverFactory {
      _pluginCompat: [SyncBailHook],
      hooks: [Object],
      cache1: [WeakMap],
      cache2: Map {}
    }
  },
  compilationDependencies: Set {}
}

compile 做了三件核心事情: 生成compilation(模块和模块的依赖关系)、make(根据模块和模块的依赖关系进行递归编译)、seal(封装编译结果,生成最终输出)

4. Compilation实例

Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。

Compilation 和 Compiler 一样继承至 Tapable,一开始同样先执行 Tapable 的构造函数,生成插件兼容的钩子,然后挂载钩子 hooks,大体包括 加载入口文件、打包、依赖分析、编译结果封存、代码chunk、优化等等功能钩子。

新的 Compilation 生成完毕就会执行 compiler 上的 make 钩子进行编译,以下就是 make 钩子的执行调用栈:

// VM46947640

(function anonymous(compilation, _callback) {
  "use strict";
  var _context;
  var _x = this._x;
  do {
    var _counter = 2;
    var _done = () => {
      _callback();
    };
    if (_counter <= 0) break;
    var _fn0 = _x[0];
    _fn0(compilation, (_err0) => {
      if (_err0) {
        if (_counter > 0) {
          _callback(_err0);
          _counter = 0;
        }
      } else {
        if (--_counter === 0) _done();
      }
    });
    if (_counter <= 0) break;
    var _fn1 = _x[1];
    _fn1(compilation, (_err1) => {
      if (_err1) {
        if (_counter > 0) {
          _callback(_err1);
          _counter = 0;
        }
      } else {
        if (--_counter === 0) _done();
      }
    });
  } while (false);
});

5. make

make 钩子在一个新的 compilation 生成完毕后执行,根据 compilation 进行编译工作,从上面看一共注册了两个任务。

1) 第一个注册钩子函数 PersistentChildCompilerSingletonPlugin 主要处理子编译,最终将结果交由主编译优化、输出,这里主要就是打包HTML模板:

// node_modules/html-webpack-plugin/lib/cached-child-compiler.js

compiler.hooks.make.tapAsync(
  "PersistentChildCompilerSingletonPlugin",
  (mainCompilation, callback) => {
    // 如果子编译已经进行,或者正在验证缓存,不可以再次开启
    if (
      this.compilationState.isCompiling ||
      this.compilationState.isVerifyingCache
    ) {
      return callback(new Error("Child compilation has already started"));
    }

    // 更新当前编译的开始时间
    compilationStartTime = new Date().getTime();

    // 编译开始 - 现在不再可能添加新模板
    // entries --> 0:'/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/index.html'
    // 这里是使用 html-webpack-plugin 加载HTML模板
    this.compilationState = {
      isCompiling: false,
      isVerifyingCache: true,
      previousEntries: this.compilationState.compiledEntries,
      previousResult: this.compilationState.compilationResult,
      entries: this.compilationState.entries,
    };

    // 检测缓存是否有效,是一个promise
    const isCacheValidPromise = this.isCacheValid(
      previousFileSystemSnapshot,
      mainCompilation
    );

    let cachedResult = childCompilationResultPromise;
    childCompilationResultPromise = isCacheValidPromise.then((isCacheValid) => {
      // 如果缓存有效就返回缓存
      // 一般我们的html模板不会经常修改,编译缓存就显得非常有意义,能够减少编译时间
      if (isCacheValid) {
        return cachedResult;
      }
      // 不存在缓存就开始编译
      const compiledEntriesPromise = this.compileEntries(
        mainCompilation,
        this.compilationState.entries
      );
      // 子编译完成时立即创建快照,也就是缓存编译结果的依赖关系
      compiledEntriesPromise
        .then((childCompilationResult) => {
          return fileWatcherApi.createSnapshot(
            childCompilationResult.dependencies,
            mainCompilation,
            compilationStartTime
          );
        })
        .then((snapshot) => {
          previousFileSystemSnapshot = snapshot;
        });
      return compiledEntriesPromise;
    });

    // 将需要监视的文件添加到主编译中
    // 在优化依赖树之前调用,插件可以 tap 此钩子执行依赖树优化
    // 将编译结果的依赖关系添加到监听,任何依赖发生改变都需要重新优化代码
    mainCompilation.hooks.optimizeTree.tapAsync(
      "PersistentChildCompilerSingletonPlugin",
      (chunks, modules, callback) => {
        const handleCompilationDonePromise = childCompilationResultPromise.then(
          (childCompilationResult) => {
            this.watchFiles(
              mainCompilation,
              childCompilationResult.dependencies
            );
          }
        );
        handleCompilationDonePromise.then(
          () => callback(null, chunks, modules),
          callback
        );
      }
    );

    // 一旦知道主编译哈希,就存储最终编译
    mainCompilation.hooks.additionalAssets.tapAsync(
      "PersistentChildCompilerSingletonPlugin",
      (callback) => {
        const didRecompilePromise = Promise.all([
          childCompilationResultPromise,
          cachedResult,
        ]).then(([childCompilationResult, cachedResult]) => {
          // 子编译发生改变就重新编译
          return cachedResult !== childCompilationResult;
        });

        const handleCompilationDonePromise = Promise.all([
          childCompilationResultPromise,
          didRecompilePromise,
        ]).then(([childCompilationResult, didRecompile]) => {
          // 一旦发现子编译发生改变,立即更新主编译hash
          if (didRecompile) {
            mainCompilationHashOfLastChildRecompile = mainCompilation.hash;
          }
          // 重置子编译状态
          this.compilationState = {
            isCompiling: false,
            isVerifyingCache: false,
            entries: this.compilationState.entries,
            compiledEntries: this.compilationState.entries,
            compilationResult: childCompilationResult,
            mainCompilationHash: mainCompilationHashOfLastChildRecompile,
          };
        });
        handleCompilationDonePromise.then(() => callback(null), callback);
      }
    );

    // 继续编译
    callback(null);
  }
);
// node_modules/html-webpack-plugin/lib/cached-child-compiler.js

/**
 * 编译HTML模板
 *
 * @private
 * @param {WebpackCompilation} mainCompilation
 * @param {string[]} entries
 * @returns {Promise<ChildCompilationResult>}
 */
compileEntries (mainCompilation, entries) {
  const compiler = new HtmlWebpackChildCompiler(entries);
  return compiler.compileTemplates(mainCompilation).then((result) => {
    return {
    // The compiled sources to render the content
      compiledEntries: result,
      // 是否存在文件依赖,以确定依赖发生改变时是否需要重新编译
      dependencies: compiler.fileDependencies,
      // 主编译的hash可以用来表示这个编译是在当前编译期间完成的
      mainCompilationHash: mainCompilation.hash
    };
  }, error => ({
    // The compiled sources to render the content
    error,

    dependencies: compiler.fileDependencies,

    mainCompilationHash: mainCompilation.hash
  }));
}

这个 PersistentChildCompilerSingletonPlugin 钩子注册的任务就是从缓存中查找 HTML 模板的编译结果,没有缓存就重新编译模板,最终注册两个钩子函数,一个是 optimizeTree 将编译结果的依赖关系添加到优化树上,一旦依赖发生改变就需要重新优化编译结果;另一个 additionalAssets 一旦重新编译就更新主编译 hash ,添加到输出。

以上 compileEntries 方法中类 htmlWebpackPlugincompileTemplates 方法就是对 HTML 模板进行编译:

// node_modules/html-webpack-plugin/lib/child-compiler.js

/**
 * 该函数将启动模板编译
 * 一旦启动就不能再添加模板
 *
 * @param {WebpackCompilation} mainCompilation
 * @returns {Promise<{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}>}
 */
compileTemplates (mainCompilation) {
  // 防止对同一个模板进行多次编译
  // 编译被缓存在一个 promise 中
  // 如果已经存在则返回
  if (this.compilationPromise) {
    return this.compilationPromise;
  }
  // 入口文件只是一个空的辅助作为动态模板
  // 需要在“loader.js”中添加
  const outputOptions = {
    filename: '__child-[name]',
    publicPath: mainCompilation.outputOptions.publicPath
  };
  // 编译名称是HTML模板编译
  const compilerName = 'HtmlWebpackCompiler';
  // 创建一个额外的子编译器,它接受模板
  // 并将其转换为 Node.JS html 工厂。
  // 这允许我们在编译期间使用加载器
  const childCompiler = mainCompilation.createChildCompiler(compilerName, outputOptions);
  // 文件路径
  childCompiler.context = mainCompilation.compiler.context;
  // 将模板编译为 nodejs javascript
  // 模块被包装成nodejs模块进行捆绑导出,入口模块可以通过require导入模块
  new NodeTemplatePlugin(outputOptions).apply(childCompiler);
  // 如果您在 Node.js 环境中运行包,则应使用这些插件。如果确保本地模块即使捆绑也能正确加载
  new NodeTargetPlugin().apply(childCompiler);
  new LibraryTemplatePlugin('HTML_WEBPACK_PLUGIN_RESULT', 'var').apply(childCompiler);
  new LoaderTargetPlugin('node').apply(childCompiler);
  // 添加模板
  this.templates.forEach((template, index) => {
    new SingleEntryPlugin(childCompiler.context, template, `HtmlWebpackPlugin_${index}`).apply(childCompiler);
  });
  this.compilationStartedTimestamp = new Date().getTime();
  // 将模板编译作为一个promise,以子编译的方式运行编译,将最终编译结果返回
  this.compilationPromise = new Promise((resolve, reject) => {
    childCompiler.runAsChild((err, entries, childCompilation) => {
      // 提取模板
      const compiledTemplates = entries
        ? extractHelperFilesFromCompilation(mainCompilation, childCompilation, outputOptions.filename, entries)
        : [];
      // 提取依赖关系
      if (entries) {
        this.fileDependencies = { fileDependencies: Array.from(childCompilation.fileDependencies), contextDependencies: Arrayfrom(childCompilation.contextDependencies), missingDependencies: Array.from(childCompilation.missingDependencies) };
      }
      // 子编译出错导致编译失败
      if (childCompilation && childCompilation.errors && childCompilation.errors.length) {
        ...
        reject(new Error('Child compilation failed:\n' + errorDetails));
        return;
      }
      // 编译回调错误对象包含错误信息则reject
      if (err) {
        reject(err);
        return;
      }
      // 否则认为编译成功,返回编译结果
      /**
       * @type {{[templatePath: string]: { content: string, hash: string, entry: WebpackChunk }}}
       */
      const result = {};
      compiledTemplates.forEach((templateSource, entryIndex) => {
        // 多个模板对应多个入口文件
        result[this.templates[entryIndex]] = {
          content: templateSource,
          hash: childCompilation.hash,
          entry: entries[entryIndex]
        };
      });
      this.compilationEndedTimestamp = new Date().getTime();
      resolve(result);
    });
  });
  return this.compilationPromise;
}

这里就涉及到创建一个子编译的问题,它是通过调用主编译 mainCompilation 上的 createChildCompiler 方法来创建一个子编译任务,因为 Compilation 类复制了 Compiler(this.compiler = compiler;),所以 compiler 全程都会携带 Compiler,拥有 Compiler 上的所有属性、方法:

// node_modules/webpack/lib/Compiler.js

createChildCompiler(
  compilation,
  compilerName,
  compilerIndex,
  outputOptions,
  plugins
) {
  // 生成一个新的Compiler
  const childCompiler = new Compiler(this.context);
  // 挂载plugins至子编译
  if (Array.isArray(plugins)) {
    for (const plugin of plugins) {
      plugin.apply(childCompiler);
    }
  }
  // 挂载主编译中的hooks至子编译,除了数组中的
  // 子编译不存在make
  for (const name in this.hooks) {
    if (
      ![
        "make",
        "compile",
        "emit",
        "afterEmit",
        "invalid",
        "done",
        "thisCompilation"
      ].includes(name)
    ) {
      if (childCompiler.hooks[name]) {
        childCompiler.hooks[name].taps = this.hooks[name].taps.slice();
      }
    }
  }
  // ...设置一些属性
  // 读取编译记录,不存在就设置为空数组(因为可能存在多个编译)
  const relativeCompilerName = makePathsRelative(this.context, compilerName);
  if (!this.records[relativeCompilerName]) {
    this.records[relativeCompilerName] = [];
  }
  if (this.records[relativeCompilerName][compilerIndex]) {
    childCompiler.records = this.records[relativeCompilerName][compilerIndex];
  } else {
    this.records[relativeCompilerName].push((childCompiler.records = {}));
  }
  // 设置编译配置项
  childCompiler.options = Object.create(this.options);
  childCompiler.options.output = Object.create(childCompiler.options.output);
  for (const name in outputOptions) {
    childCompiler.options.output[name] = outputOptions[name];
  }
  // 设置父编译
  childCompiler.parentCompilation = compilation;
  // 执行父编译childCompiler钩子上注册的任务
  compilation.hooks.childCompiler.call(
    childCompiler,
    compilerName,
    compilerIndex
  );
  return childCompiler;
}

子编译也是一个新的 Compiler 实例,它拷贝了父编译除了 makecompileemitafterEmitinvaliddonethisCompilation 之外的所有 hooks。

在执行子编译的时候触发了注册在make上的另外一个钩子 SingleEntryPlugin

// node_modules/webpack/lib/SingleEntryPlugin.js

compiler.hooks.make.tapAsync("SingleEntryPlugin", (compilation, callback) => {
  // 这里的entry是htmlWebpackPlugin的load path + html path
  // '/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/user.html'

  // name 就是htmlWebpackPlugin
  const { entry, name, context } = this;
  
  const dep = SingleEntryPlugin.createDependency(entry, name);
  compilation.addEntry(context, dep, name, callback);
});

这是一个动态添加入口文件的插件(在webpack5中移除了,改为EntryPlugin),将 html 模板作为一个入口文件,最终的编译后 js 和 css 文件都会以标签的形式插入。

2) 第二个注册的钩子函数 SingleEntryPlugin 就是加载入口文件的:

// node_modules/webpack/lib/SingleEntryPlugin.js

compiler.hooks.make.tapAsync("SingleEntryPlugin", (compilation, callback) => {
  // 这里的entry就是 ./src/index.js
  // name 就是 main
  const { entry, name, context } = this;

  const dep = SingleEntryPlugin.createDependency(entry, name);
  compilation.addEntry(context, dep, name, callback);
});

dep 是创建的入口文件依赖信息,在addEntry时作为entry实参传入:

SingleEntryDependency {module: null, weak: false, optional: false, loc: {…}, request: './src/index.js', …}
▶ loc:{name: 'main'}
  module:null
  optional:false
  request:'./src/index.js'type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
  userRequest:'./src/index.js'
  weak:false
▶ __proto__:ModuleDependency

我们看看 addEntry 做了哪些事情:

// node_modules/webpack/lib/Compilation.js

/**
  *
  * @param {string} context 当前编译环境的路径,一般就是项目根路径
  * @param {Dependency} entry 入口文件依赖信息,经过SingleEntryPlugin.createDependency处理过的dep
  * @param {string} name 入口文件名称
  * @param {ModuleCallback} callback 回调(make钩子执行栈中的函数)
  * @returns {void} returns
  */
addEntry(context, entry, name, callback) {
  // 执行addEntry钩子上注册的任务
  this.hooks.addEntry.call(entry, name);
  // 声明一个入口
  const slot = {
    name: name,
    // TODO webpack 5 remove `request`
    request: null,
    module: null
  };
  // 如果这个入口文件是一个模块依赖,关于Dependency可以查看node_modules/webpack/lib/Dependency.js
  // 设置请求路径
  if (entry instanceof ModuleDependency) {
    slot.request = entry.request;
  }
  // TODO webpack 5: 当支持多个入口模块时,改为合并模块
  // 如果是准备好了的入口点,那么就覆盖它,否则就添加进去(多入口打包)
  const idx = this._preparedEntrypoints.findIndex(slot => slot.name === name);
  if (idx >= 0) {
    // 覆盖现有入口点
    this._preparedEntrypoints[idx] = slot;
  } else {
    this._preparedEntrypoints.push(slot);
  }
  // 通过入口文件和上下文环境添加模块链(解析模块依赖关系)
  this._addModuleChain(
    context,
    entry,
    module => {
      this.entries.push(module);
    },
    (err, module) => {
      // 如果失败则抛出错误信息,并执行回调,返回异常
      if (err) {
        this.hooks.failedEntry.call(entry, name, err);
        return callback(err);
      }
      // 如果成功返回模块则将模块绑定到入口信息slot上
      if (module) {
        slot.module = module;
      } else {
        // 否则的话从_preparedEntrypoints上移除slot
        const idx = this._preparedEntrypoints.indexOf(slot);
        if (idx >= 0) {
          this._preparedEntrypoints.splice(idx, 1);
        }
      }
      // 执行入口文件添加成功的钩子
      this.hooks.succeedEntry.call(entry, name, module);
      return callback(null, module);
    }
  );
}

/**
  *
  * @param {string} context context string path
  * @param {Dependency} dependency dependency used to create Module chain
  * @param {OnModuleCallback} onModule function invoked on modules creation
  * @param {ModuleChainCallback} callback callback for when module chain is complete
  * @returns {void} will throw if dependency instance is not a valid Dependency
  */
_addModuleChain(context, dependency, onModule, callback) {
  // 是否生成包含统计数据的json文件
  const start = this.profile && Date.now();
  const currentProfile = this.profile && {};
  // 是否在webpack打包错误时立即退出
  const errorAndCallback = this.bail
    ? err => {
      callback(err);
      }
    : err => {
      err.dependencies = [dependency];
      this.errors.push(err);
      callback();
      };
  // 如果不是标准的依赖模块就抛出错误信息
  if (
    typeof dependency !== "object" || dependency === null || !dependency.constructor
  ) {
    throw new Error("Parameter 'dependency' must be a Dependency");
  }
  // 从依赖工厂中取出模块工厂(创建模块的工厂实例,继承至NormalModuleFactory)
  const Dep = /** @type {DepConstructor} */ (dependency.constructor);
  const moduleFactory = this.dependencyFactories.get(Dep);
  // 不存在就抛出错误信息
  if (!moduleFactory) {
    throw new Error(
      `No dependency factory available for this dependency type: ${dependency.constructor.name}`
    );
  }
  // this.semaphore 这个类是一个编译队列控制,对执行进行了并发控制,默认并发数为 100
  // 超过后存入 semaphore.waiters,根据情况再调用 semaphore.release 去执行存入的事件 semaphore.waiters。
  this.semaphore.acquire(() => {
    moduleFactory.create(
      {
        contextInfo: {
          issuer: "",
          compiler: this.compiler.name
        },
        context: context,
        dependencies: [dependency]
      },
      (err, module) => {
        // 如果创建模块失败,则调出编译队列中等待执行的任务执行掉,目前是入口文件打包,暂时没有待执行的
        // 然后就抛出错误信息,提示没有找到入口模块
        if (err) {
          this.semaphore.release();
          return errorAndCallback(new EntryModuleNotFoundError(err));
        }
        // 创建结束时间
        let afterFactory;
        // 如果当前存在统计stats的文件
        if (currentProfile) {
          // 设置统计时间
          afterFactory = Date.now();
          currentProfile.factory = afterFactory - start;
        }
        // 进行module缓存
        const addModuleResult = this.addModule(module);
        module = addModuleResult.module;
        // 将module添加到入口文件中 entries
        onModule(module);
        dependency.module = module;
        module.addReason(null, dependency);
        // build之后添加模块依赖信息
        const afterBuild = () => {
          if (addModuleResult.dependencies) {
            this.processModuleDependencies(module, err => {
              if (err) return callback(err);
              callback(null, module);
            });
          } else {
            return callback(null, module);
          }
        };
        if (addModuleResult.issuer) {
          if (currentProfile) {
            module.profile = currentProfile;
          }
        }
        if (addModuleResult.build) {
          this.buildModule(module, false, null, null, err => {
            if (err) {
              this.semaphore.release();
              return errorAndCallback(err);
            }
            if (currentProfile) {
              const afterBuilding = Date.now();
              currentProfile.building = afterBuilding - afterFactory;
            }
            this.semaphore.release();
            afterBuild();
          });
        } else {
          this.semaphore.release();
          this.waitForBuildingFinished(module, afterBuild);
        }
      }
    );
  );
}
// node_modules/webpack/lib/NormalModuleFactory.js

// 生成factory函数,里面执行resolver钩子生成resolver函数并执行
this.hooks.factory.tap("NormalModuleFactory", () => (result, callback) => {
  // 调用resolver钩子,返回resolver函数
  let resolver = this.hooks.resolver.call(null);
  // Ignored
  if (!resolver) return callback();
  // 执行resolver函数,第一个参数是beforeResolve执行结果,进行模块解析
  resolver(result, (err, data) => {
    if (err) return callback(err);
    // Ignored
    if (!data) return callback();
    // direct module
    if (typeof data.source === "function") return callback(null, data);
    // 如果有resolve结果,执行afterResolve上注册的钩子函数,执行完毕将结果返回给回调
    this.hooks.afterResolve.callAsync(data, (err, result) => {
      if (err) return callback(err);
      // Ignored
      if (!result) return callback();
      let createdModule = this.hooks.createModule.call(result);
      if (!createdModule) {
        if (!result.request) {
          return callback(new Error("Empty dependency (no request)"));
        }
        createdModule = new NormalModule(result);
      }
      createdModule = this.hooks.module.call(createdModule, result);
      return callback(null, createdModule);
    });
  });
});


// 生成resolver函数
//  参数 data
//  {contextInfo: {…}, resolveOptions: {…}, context: '/Users/---/lagou-edu/webpack-entry', request: './src/index.js', dependencies: Array(1)}
//    context:'/Users/---/lagou-edu/webpack-entry'
//  ▼ contextInfo:{issuer: '', compiler: undefined}
//      compiler:undefined
//      issuer:''
//    ▶ __proto__:Object
//  ▼ dependencies:(1) [SingleEntryDependency]
//    ▼ 0:SingleEntryDependency {module: null, weak: false, optional: false, loc: {…}, request: './src/index.js', …}
//      ▶ loc:{name: 'index'}
//        module:null
//        optional:false
//        request:'./src/index.js'
//      ▶ type (get):ƒ type() {\n\t\treturn "single entry";\n\t}
//        userRequest:'./src/index.js'
//        weak:false
//      ▶ __proto__:ModuleDependency
//      length:1
//    ▶ __proto__:Array(0)
//    request:'./src/index.js'
//  ▶ resolveOptions:{}
//  ▶ __proto__:Object
this.hooks.resolver.tap("NormalModuleFactory", () => (data, callback) => {
  // 该函数作用为解析构建所有 module 所需要的 loaders 的绝对路径及这个 module 的相关构建信息
  // 取出上下文信息,编译环境路径和请求文件路径
  const contextInfo = data.contextInfo;
  const context = data.context;
  const request = data.request;
  // loader 模块
  // Resolver {_pluginCompat: SyncBailHook, fileSystem: CachedInputFileSystem, hooks: {…}, withOptions: ƒ}
  // ▶  _pluginCompat:SyncBailHook {_args: Array(1), taps: Array(4), interceptors: Array(0), call: ƒ, promise: ƒ, …}
  // ▶  fileSystem:CachedInputFileSystem {fileSystem: NodeJsInputFileSystem, _statStorage: Storage, _readdirStorage: Storage,_readFileStorage: Storage, _readJsonStorage: Storage, …}
  // ▶  hooks:{resolveStep: SyncHook, noResolve: SyncHook, resolve: AsyncSeriesBailHook, result: AsyncSeriesHook,parsedResolve: AsyncSeriesBailHook, …}
  // ▶  withOptions:options => {\n\t\t\tconst cacheEntry = childCache.get(options);\n\t\t\tif (cacheEntry !== undefined)return cacheEntry;\n\t\t\tconst mergedOptions = cachedCleverMerge(originalResolveOptions, options);\n\t\t\tconstresolver = this.get(type, mergedOptions);\n\t\t\tchildCache.set(options, resolver);\n\t\t\treturn resolver;\n\t\t}
  // ▶  __proto__:
  // loaderResolver 和 normalResolver 都继承了 Resolver,__proto__上拥有Resolver所有的成员方法
  // 下面可以通过 normalResolver.resolve 解析模块
  const loaderResolver = this.getResolver("loader");
  // 普通模块
  const normalResolver = this.getResolver("normal", data.resolveOptions);
  // Loader 模块request path是 Loader path + module path
  // 这里做一下拆分
  // '/Users/***/lagou-edu/webpack-entry/node_modules/html-webpack-plugin/lib/loader.js!/Users/***/lagou-edu/webpack-entry/src/index.html'
  let matchResource = undefined;
  let requestWithoutMatchResource = request;
  const matchResourceMatch = MATCH_RESOURCE_REGEX.exec(request);
  if (matchResourceMatch) {
    matchResource = matchResourceMatch[1];
    if (/^\.\.?\//.test(matchResource)) {
      matchResource = path.join(context, matchResource);
    }
    requestWithoutMatchResource = request.substr(
      matchResourceMatch[0].length
    );
  }
  // 是否忽略 preLoader 以及 normalLoader
  const noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");
  // 是否忽略 normalLoader
  const noAutoLoaders =
   noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");
  // 忽略所有的 preLoader / normalLoader / postLoader
  const noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");
  // 首先解析出所需要的 loader,这种 loader 为内联的 loader
  let elements = requestWithoutMatchResource
    .replace(/^-?!+/, "")
    .replace(/!!+/g, "!")
    .split("!");
  // 获取资源路径
  let resource = elements.pop();
  // 获取每个loader及对应的options配置(将inline loader的写法变更为module.rule的写法)
  elements = elements.map(identToLoaderRequest);
  // 并行运行第一个参数中的回调,将返回结果以数组的形式传递给第二个参数最终回调
  // 其中第一个是加载Loader模块
  // 第二个是加载普通模块
  asyncLib.parallel(
    [
      // callback =>
      //  this.resolveRequestArray(
      //   contextInfo,
      //   context,
      //   elements,
      //   loaderResolver,
      //   callback
      //  ),
      callback => {
        if (resource === "" || resource[0] === "?") {
          return callback(null, {
            resource
          });
        }
        // 普通的模块编译调用normalResolver的resolve方法
        normalResolver.resolve(
          contextInfo,
          context,
          resource,
          {},
          (err, resource, resourceResolveData) => {
            if (err) return callback(err);
            callback(null, {
              resourceResolveData,
              resource
            });
          }
        );
      }
    ],
    (err, results) => {
      // loader处理
      ...
    }
  );
});

/**
 * 创建一个module
 * @param {*} data
 * {
 *   contextInfo: {
 *     issuer: "",
 *     compiler: this.compiler.name
 *   },
 *   context: context,
 *   dependencies: [dependency]
 * },
 * @param {*} callback
 * @returns
 */
create(data, callback) {
  // 取出模块依赖信息
  const dependencies = data.dependencies;
  // 查看是否存在缓存
  const cacheEntry = dependencyCache.get(dependencies[0]);
  // 存在就返回缓存
  if (cacheEntry) return callback(null, cacheEntry);
  const context = data.context || this.context;
  const resolveOptions = data.resolveOptions || EMPTY_RESOLVE_OPTIONS;
  const request = dependencies[0].request;
  const contextInfo = data.contextInfo || {};
  // 执行beforeResolve钩子上注册的任务
  this.hooks.beforeResolve.callAsync(
    {
      contextInfo,
      resolveOptions,
      context,
      request,
      dependencies
    },
    (err, result) => {
      // 未做处理的话,result 还是传入的第一个参数 { contextInfo: ... }
      if (err) return callback(err);
      // Ignored
      if (!result) return callback();
      // 执行完毕就执行factory钩子生成工厂函数
      // factory 钩子上注册的函数返回一个 factory 函数
      const factory = this.hooks.factory.call(null);
      // Ignored
      if (!factory) return callback();
      // 调用factory函数,第一个参数是beforeResolve钩子的执行结果,第二个参数是回调
      factory(result, (err, module) => {
        if (err) return callback(err);
        if (module && this.cachePredicate(module)) {
          for (const d of dependencies) {
            dependencyCache.set(d, module);
          }
        }
        callback(null, module);
     });
    }
  );
}
// node_modules/enhanced-resolve/lib/Resolver.js

/**
 *
 * @param {上下文信息} context
 * {issuer: '', compiler: undefined}
 *   compiler:undefined
 *   issuer:''
 * ▶ __proto__:Object
 * @param {路径} path '/Users/---/lagou-edu/webpack-entry'
 * @param {请求文件} request './src/index.js'
 * @param {解析环境} resolveContext {}
 * @param {回调} callback
 * @returns
 */
resolve(context, path, request, resolveContext, callback) {
  const obj = {
    context: context,
    path: path,
    request: request
  };
  const message = "resolve '" + request + "' in '" + path + "'";
  // 调用doResolve进行解析
  return this.doResolve(
    this.hooks.resolve,
    obj,
    message,
    {
      missing: resolveContext.missing,
      stack: resolveContext.stack
    },
    (err, result) => {
      if (!err && result) {
        return callback(
          null,
          result.path === false ? false : result.path + (result.query || ""),
          result
        );
      }
      // 没有加载结果的话再加载一遍,打印加载日志
      const localMissing = new Set();
      const log = [];
      return this.doResolve(
        this.hooks.resolve,
        obj,
        message,
        {
          log: msg => {
            if (resolveContext.log) {
              resolveContext.log(msg);
            }
            log.push(msg);
          },
          missing: localMissing,
          stack: resolveContext.stack
        },
        (err, result) => {
          if (err) return callback(err);
          const error = new Error("Can't " + message);
          error.details = log.join("\n");
          error.missing = Array.from(localMissing);
          this.hooks.noResolve.call(obj, error);
          return callback(error);
        }
      );
    }
  );
}

/**
 *
 * @param {resolve钩子} hook
 * @param {请求信息(包含编译环境、路径、文件)} request
 * {context: {…}, path: '/Users/---/lagou-edu/webpack-entry', request: './src/index.js'}
 * ▶ context:{issuer: '', compiler: undefined}
 *   request:'./src/index.js'
 *   path:'/Users/---/lagou-edu/webpack-entry'
 * ▶ __proto__:Object
 * @param {请求的文字描述('resolve './src/index.js' in '/Users/---/lagou-edu/webpack-entry'')} message
 * @param {解析环境} resolveContext
 * {missing: undefined, stack: undefined}
 *   stack:undefined
 *   missing:undefined
 * ▶ __proto__:Object
 * @param {*} callback
 * @returns
 */
doResolve(hook, request, message, resolveContext, callback) {
  if (typeof callback !== "function")
    throw new Error("callback is not a function " + Array.from(arguments));
  if (!resolveContext)
    throw new Error(
      "resolveContext is not an object " + Array.from(arguments)
    );
  const stackLine = // 创建一个堆栈线 'resolve: (/Users/---/lagou-edu/webpack-entry) ./src/index.js'
    hook.name +
    ": (" +
    request.path +
    ") " +
    (request.request || "") +
    (request.query || "") +
    (request.directory ? " directory" : "") +
    (request.module ? " module" : "");
  let newStack;
  if (resolveContext.stack) {
    newStack = new Set(resolveContext.stack);
    if (resolveContext.stack.has(stackLine)) {
      // 防止递归,如果当前resolve环境中存在当前解析,就阻止运行
      // 返回递归错误
      const recursionError = new Error(
        "Recursion in resolving\nStack:\n  " +
         Array.from(newStack).join("\n  ")
      );
      recursionError.recursion = true;
      if (resolveContext.log)
        resolveContext.log("abort resolving because of recursion");
      return callback(recursionError);
    }
    newStack.add(stackLine);
  } else {
    // 否则的话就存入新的堆栈中
    newStack = new Set([stackLine]);
  }
  this.hooks.resolveStep.call(hook, request);
  // 注册的taps 或者 拦截器 不为空
  // {type: 'async', fn: ƒ, name: 'UnsafeCachePlugin'}
  if (hook.isUsed()) {
    const innerContext = createInnerContext(
      {
        log: resolveContext.log,
        missing: resolveContext.missing,
        stack: newStack
      },
      message
    );
    // 返回钩子的执行结果(解析结果)
    return hook.callAsync(request, innerContext, (err, result) => {
      if (err) return callback(err);
      if (result) return callback(null, result);
      callback();
    });
  } else {
    callback();
  }
}

流程:addEntry -> _addModuleChain -> create -> resolve -> doResolve

  1. addEntry 组装入口信息,然后交由 _addModuleChain 处理,顾名思义,添加模块链就是添加模块之间的相互依赖

  2. _addModuleChain 取出模块工厂(normalModuleFactory 或者 contextModuleFactory),调用 create 创建解析工厂 factory

  3. create 执行 factory 钩子上注册的函数 normalModuleFactory 生成 factory 函数并执行

  4. factory 函数里面又执行 resolver 钩子生成 resolve 函数,然后执行这个函数,这个resolver主要就是处理Loader的

  5. resolve 函数里面先取出加载 module 的 factory,也就是 this.resolverFactory(type, resolveOptions),这个 this.resolverFactory 就是 Compiler 中挂载的 resolves (Compiler.js 第 151 行),里面在 resolverFactory 挂载了一堆 plugin;然后进行加载模块路径分析,判断是 loader 模块还是普通文件模块(Loader 模块路径通常解析成 LoaderJsPath + filePath,普通文件模块解析成 filePath);最终通过一个并行任务 asyncLib.parallel 并行执行 Loader 模块和普通模块的解析,最终将解析结果合并后给回调处理。

  6. 在普通模块的解析中直接调用 resolve 方法进行解析(normalResolver.resolve)

  7. resolve 中进行参数拼装,然后又调用 doResolve 进行最终解析