webpack5 打包流程源码剖析(1)

开篇

webpack 相信大家都不陌生,近些年来被广泛应用于打包资源前端项目工程之中。

通常,我们只要掌握了 webpack 一些常用配置,足以满足项目构建的大多数应用场景。

然,当你想去站在一个更高的角度去看待和使用 webpack 时,如做优化、自定义 plugin 和 loader,理解 webpack 的打包编译流程尤为重要。掌握打包流程线上的各个阶段所做的工作,能够更准确的帮助我们实现高标准的定制化功能。

本篇,我们将以 Webpack 5 作为材料,调试源码来熟悉 webpack 打包流程线,有了源码流程上的熟悉,今后想深入那部分配置的原理将变得容易。

一、前置知识

「1. Webpack」
webpack 是 JavaScript 应用程序静态模块打包器。它的源码设计采用了插件架构,由 Tapable 提供注册和调用插件的能力。因此,webpack 中应用了很多内置插件,来完成对每一个 config 配置项功能的实现。

「2. Compiler」
compiler 理解为 编译器,仅在 webpack 初始化时创建一次实例,是 webpack 打包流程上的支柱引擎。

在 compiler 实例上提供了大量 hooks 来面向用户实现自定义插件,在 run 启动打包之后会创建 compilation 实例处理模块的编译工作。

「3. Compilation」
compilation 理解为 编译,是由 compiler 编译器创建而成,一个编译器可能会创建一次或多次编译,则 compilation 可能会被创建多次。(watch

compilation 会从入口模块开始,编译模块以及它的依赖子模块。所有的模块的编译都会经过:加载(loaded)、封存(sealed)、优化(optimized)、分块(chunked)、哈希(hashed) 和 重新创建(restored)。

「4. Dependence」
webpack 用于它来记录模块间依赖关系。在模块中引用其它模块,会将引用关系表述为 Dependency 子类并关联 module 对象,等到当前 module 内容都解析完毕之后,启动下次循环开始将 Dependency 对象转换为新的 Module 子类。

「5. Module」
webpack 处理每一个资源文件时,都会以 module 对象形式存在,包含了资源的路径、上下文、依赖、内容等信息。所有关于资源的构建、转译、合并也都是以 module 为基本单位进行。

「6. Chunk」
编译完成准备输出时,webpack 会将 module 按特定的规则组织成一个一个的 chunk,这些 chunk 某种程度上跟最终输出的文件一一对应。

「7. Assets」
asset 代表了最终要输出写入磁盘的文件内容,它与 chunk 一一对应。

「8. Tapable」
Tapable 提供注册和调用插件的能力,来串联 webapck 整个打包流程的插件工作。

它能够在特定时机触发钩子,并附带上足够的上下文信息,去通知插件注册的钩子回调,去产生 side effect 影响编译状态和后续流程。

Tapable 的具体介绍和使用可以查阅这篇文章 Webpack 插件架构 - Tapable

流程概览

webpack 打包流程整体可划分为四大块,后续的源码调试也将按照以下划分进行分析。

  1. 初始化阶段
  2. 构建阶段(make)
  3. 生成阶段(seal)
  4. 写入阶段(emit)

由于内容和篇幅过长,将分成两篇文章来介绍流程,本篇主要介绍「初始化阶段」和 「构建阶段」;「生成阶段」和「写入阶段」请移步到 webpack5 打包流程源码剖析(2)

二、调试环境

我们先来初始化一个调试环境:

mkdir webpack-debugger && cd webpack-debugger && npm init -y && npm install webpack webpack-cli -D
复制代码

webpack-debugger 目录下创建 src/index.js 作为入口模块,文件中添加一行打印代码:

// src/index.js
conspole.log('webpack-debugger');
复制代码

新建 webpack.config.js,我们使用最简单的打包配置:

// webpack.config.js
const path = require('path');

module.exports = () => ({
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'static/js/[name].[contenthash:8].js',
  }
})
复制代码

最后,创建 build.js 作为调用 webpack 打包的执行文件:

// build.js
const path = require('path');
const webpack = require('webpack');
const configFactory = require('./webpack.config.js');
const config = configFactory();

debugger;
const compiler = webpack(config);

debugger;
compiler.run();
复制代码

通常我们都会在 package.json scripts 字段中使用 webpack bin 命令执行去 webpack-cli 进行打包,而进入 webpack-cli 内部调用打包与上面大同小异。

上述代码可知:webpack 默认导出一个函数方法,接收 config 配置对象作为参数,返回 compiler 对象;通过 compiler.run() 开启打包流程。

如果你使用的 VSCode,新建一个 JavaScript Debug Terminal 并执行 node build.js 开启调试。

接下来我们分析初始化阶段 webpack 做了哪些事情。

三、初始化阶段

我们把在真正构建入口模块之前的这一阶段划分为初始化阶段,主要步骤如下:

  1. 初始化参数:将用户传入配置与默认配置结合得到最终配置参数;
  2. 创建编译器对象:根据配置参数创建 Compiler 实例对象;
  3. 初始化编译环境:注册用户配置插件及内置插件;
  4. 运行编译:执行 compiler.run 方法;
  5. 确定入口:根据配置 entry 找寻所有入口文件,并转换为 dependence 对象,等待执行 compilition.addEntry 编译工作。

3.1、webpack()

webpack 依赖包默认导出一个方法,这个方法的定义在 webpack/lib/webpack.js 之中:

// webpack/lib/webpack.js
const webpack = (options, callback) => {
  const create = () => {
    const webpackOptions = options;
    const compiler = createCompiler(webpackOptions);
  }

  if (callback) {
    const { compiler } = create();
    compiler.run((err, stats) => {
      compiler.close(err2 => {
        callback(err || err2, stats);
      });
    });
    return compiler;
  } else {
    const { compiler } = create();
    return compiler;
  }
}
复制代码

这个方法允许传递两个参数,若传递了 callback,创建 compiler 实例后会自动调用 run 方法启动打包,否则交由外部手动调用来启动打包。

3.2、createCompiler

参数合并、compiler 编译器实例创建、注册外部和内部插件都在这里来完成:

// webpack/lib/webpack.js
const createCompiler = rawOptions => {
  // 规范 webpack config 配置项(创建 config 配置项)
  const options = getNormalizedWebpackOptions(rawOptions);
  // 设置默认的 config.context
  applyWebpackOptionsBaseDefaults(options);
  // 创建 compiler 编译器实例
  const compiler = new Compiler(options.context, options);
  // 应用 Node 环境插件,如为 compiler 提供 fs 文件操作 API(fs 模块二次封装)。
  new NodeEnvironmentPlugin({
    infrastructureLogging: options.infrastructureLogging
  }).apply(compiler);
  // 注册用户传入的插件配置
  if (Array.isArray(options.plugins)) {
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        plugin.call(compiler, compiler);
      } else {
        plugin.apply(compiler);
      }
    }
  }
  // 应用配置项默认值
  applyWebpackOptionsDefaults(options);
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  // 关键,注册 Webpack 打包流程的内置插件
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  return compiler;
};
复制代码

这里我们重点提及两处:

  1. new Compiler(options.context, options) Compiler 是一个 ES6 class 构造函数,到这里我们只认识到了一个 run 方法,它的基础结构如下:
// webpack/lib/Compiler.js
class Compiler {
  constructor(context, options = {}) {
    this.hooks = Object.freeze({
      initialize: new SyncHook([]),
      run: new AsyncSeriesHook(["compiler"]),
      done: new AsyncSeriesHook(["stats"]),
      emit: new AsyncSeriesHook(["compilation"]),
      make: new AsyncParallelHook(["compilation"]),
      ... 很多很多
    });
    this.options = options;
		this.context = context;
  }
  run(callback) {}
}
复制代码
  1. new WebpackOptionsApply().process(options, compiler) 上面「前置知识」中我们了解到:webpack 是一个插件结构化的设计架构,即每一个功能的实现都是由一个插件来完成的,比如模块的编译入口 entry 是由 EntryPlugin 来管理和执行。

WebpackOptionsApply().process 中注册的内置插件很多,这里我们只关心本篇会涉及到的插件配置。

// webpack/lib/WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
  constructor() {
    super();
  }
  process(options, compiler) {
    new JavascriptModulesPlugin().apply(compiler);
    // entry 插件
    new EntryOptionPlugin().apply(compiler);
    compiler.hooks.entryOption.call(options.context, options.entry);
    ...
  }
}
复制代码
  • EntryOptionPlugin 会为 config.entry 中的每个配置应用 EntryPlugin 来注册编译入口,等后续 hooks.make 时机开始入口模块的编译。
  • JavascriptModulesPlugin 提供了 parse AST 的核心实现,后续收集模块内的所引入的 deps 依赖模块时会用到。

到这里,compiler 实例创建完成,并且将相关插件注册成功。

接下来会执行 compiler.run() 开启打包。由于还没有走到真正的编译,将这部分内容放在「初始化阶段」一并介绍。

3.3、compiler.run

// webpack/lib/Compiler.js
class Compiler {
  ...
  run(callback) {
    const finalCallback = (err, stats) => {} // 所有工作都完成后的最终执行函数
    const onCompiled = (err, compilation) => {} // 编译完成后的执行函数
    this.hooks.beforeRun.callAsync(this, err => {
      if (err) return finalCallback(err);
      this.hooks.run.callAsync(this, err => {
        if (err) return finalCallback(err);
        this.compile(onCompiled);
      });
    });
  }
}
复制代码

首先执行了 beforeRunrun 两个 hook 钩子,如果有插件中注册了这两类钩子,注册的回调函数就会立刻执行。下面我们移步到 this.compile 之中。

// webpack/lib/Compiler.js
class Compiler {
  ...
  compile(callback) {
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      this.hooks.compile.call(params);
      const compilation = this.newCompilation(params);
      this.hooks.make.callAsync(compilation, err => {
        compilation.finish(err => {
          compilation.seal(err => {
            return callback(null, compilation);
          });
        });
      });
    });
  }
}
复制代码

compile() 是启动编译的关键,编译实例 compilation 的参数定义、实例创建以及编译完成后的收尾工作都在这里实现。

  1. 首先是 this.newCompilationParams 创建 compilation 编译模块所需的参数:
// webpack/lib/Compiler.js
newCompilationParams() {
  const params = {
    normalModuleFactory: this.createNormalModuleFactory(),
    contextModuleFactory: this.createContextModuleFactory()
  };
  return params;
}
复制代码

这里,我们需要留意一下 createNormalModuleFactory,在 webpack 中,每一个依赖模块都可以看作是一个 Module 对象,通常会有很多模块要处理,这里创建一个模块工厂 Factory。

// webpack/lib/Compiler.js
createNormalModuleFactory() {
  const normalModuleFactory = new NormalModuleFactory({
    context: this.options.context,
    fs: this.inputFileSystem,
    resolverFactory: this.resolverFactory, // resolve 模块时使用
    options: this.options.module,
    associatedObjectForCache: this.root,
    layers: this.options.experiments.layers
  });
  this._lastNormalModuleFactory = normalModuleFactory;
  this.hooks.normalModuleFactory.call(normalModuleFactory);
  return normalModuleFactory;
}
复制代码
  1. 创建 compilation 实例对象:
// webpack/lib/Compiler.js
newCompilation(params) {
  this._cleanupLastCompilation(); // 清除上次 compilation
  const compilation = this._lastCompilation = new Compilation(this, params);
  this.hooks.thisCompilation.call(compilation, params);
  this.hooks.compilation.call(compilation, params);
  return compilation;
}
复制代码

CompilationCompiler 都是一个 class 构造函数,实例上也包含了非常多的属性和方法。

// webpack/lib/Compilation.js
class Compilation {
  constructor(compiler, params) {
    this.hooks = Object.freeze({ ... });
    this.compiler = compiler;
    this.params = params;
    this.options = compiler.options;
    this.entries = new Map(); // 存储 entry module
    this.modules = new Set();
    this._modules = new Map(); // 存储所有 module
    ...
  }
}
复制代码
  1. 调用 hooks.make 这一步很关键,在上面创建得到 compilation 之后,就可以进入编译阶段,编译会从入口模块开始进行。

而入口模块的准备工作是在注册的 EntryOptionPlugin 之中:

// webpack/lib/EntryOptionsPlugin.js
class EntryOptionPlugin {
  apply(compiler) {
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
      EntryOptionPlugin.applyEntryOption(compiler, context, entry);
      return true;
    });
  }
  static applyEntryOption(compiler, context, entry) {
    const EntryPlugin = require("./EntryPlugin");
    for (const name of Object.keys(entry)) {
      const desc = entry[name];
      const options = EntryOptionPlugin.entryDescriptionToOptions(compiler, name, desc);
      for (const entry of desc.import) {
        new EntryPlugin(context, entry, options).apply(compiler); // 注册 entry 插件
      }
    }
  }
}
复制代码

其中关键部分是为每个 entry 注册 EntryPlugin,在 EntryPlugin 中就会看到与 hook.make 相关的逻辑:

// webpack/lib/EntryPlugin.js
class EntryPlugin {
  apply(compiler) {
    // 1、记录 entry 模块解析时使用 normalModuleFactory
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency, // key
          normalModuleFactory // value
        );
      }
    );
    const { entry, options, context } = this;
    // 2、为 entry 创建 Dependency 对象
    const dep = EntryPlugin.createDependency(entry, options);
    // 3、监听 hook.make,执行 compilation.addEntry
    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      compilation.addEntry(context, dep, options, err => {
        callback(err);
      });
    });
  }
}
复制代码

现在我们清楚了:当执行 hooks.make.callAsync 时,其实就是执行 compilation.addEntry 开始入口模块的编译构建阶段。

这也是很关键的一步:找到入口进行构建。

四、构建阶段

hooks.make 是触发入口模块编译的开始,在 webpack 的构建阶段,流程如下:

  1. entry 入口模块创建 entryData 存储在 compilation.entries,用于后续为每个入口输出 chunk
  2. 拿到处理 entry 模块的工厂方法 moduleFactory,开始 ModuleTree 的创建,后面每个文件模块都会先生成一个 Module
  3. 执行 handleModuleCreation 开始处理入口模块的构建,当然,入口模块中所引入的依赖模块,构建也都是从这里开始;
  4. 构建过程会经历 feactorize(创建 module)addModulebuildModule 三个阶段,build 阶段涉及到 loader 代码转换和依赖收集;
  5. 模块构建完成后,若存在子依赖(module.dependencies),回到第三步开始子依赖的构建。

下面,我们看看代码上的流程线。

4.1、构建 EntryModuleTree

compilation.addEntry 开始进入 entry 模块的编译,调用 _addEntryItem 创建 entryData 加入到 this.entries 集合中。

// webpack/lib/Compilation.js
class Compilation {
  constructor(compiler, params) {
    this.entries = new Map();
    ...
  }
  
  addEntry(context, entry, options, callback) {
    this._addEntryItem(context, entry, "dependencies", options, callback);
  }
  
  _addEntryItem(context, entry, target, options, callback) {
    const { name } = options;
    let entryData = name !== undefined ? this.entries.get(name) : this.globalEntry;
    if (entryData === undefined) {
      entryData = {
        dependencies: [],
        includeDependencies: [],
        options: {
          name: undefined,
          ...options
        }
      };
      entryData[target].push(entry);
      this.entries.set(name, entryData);
    }
    this.hooks.addEntry.call(entry, options);

    this.addModuleTree({ context, dependency: entry, contextInfo: undefined }, (err, module) => {
      this.hooks.succeedEntry.call(entry, options, module);
      return callback(null, module);
    });
  }
}
复制代码

接着,执行 addModuleTree 获取 moduleFactory 即上文存储的 normalModuleFactory

// webpack/lib/Compilation.js
addModuleTree({ context, dependency, contextInfo }, callback) {
  const Dep = dependency.constructor; // EntryDependency
  // dependencyFactories.get(EntryDependency) = normalModuleFactory
  const moduleFactory = this.dependencyFactories.get(Dep); // 用于后续执行 moduleFactory.create()
  this.handleModuleCreation({ 
    factory: moduleFactory, 
    dependencies: [dependency], 
    originModule: null, contextInfo, context 
  }, (err, result) => {
    callback(null, result);
  });
}
复制代码

然后,执行 handleModuleCreation

// webpack/lib/Compilation.js
handleModuleCreation(
  {
    factory, // moduleFactory
    dependencies, // [dep]
    ...
  },
  callback
) {
  const moduleGraph = this.moduleGraph;
  this.factorizeModule(
    {
      currentProfile: false,
      factory,
      dependencies,
      factoryResult: true,
      originModule,
      contextInfo,
      context
    },
    (err, factoryResult) => {
      const newModule = factoryResult.module;
      this.addModule(newModule, (err, module) => {
        ...
      });
    }
  );
}
复制代码

factorizeModule 有分解模块的意思,可以理解为:为 entry 创建一个 Module。它的函数体逻辑十分简单:

// webpack/lib/Compilation.js
Compilation.prototype.factorizeModule = function (options, callback) {
  this.factorizeQueue.add(options, callback);
}
复制代码

4.2、模块编译所经历的阶段

看到这里会不会感到困惑。从代码来看,将 options 加入到 factorizeQueue 中流程就结束了。

其实不然,这是一个 AsyncQueue 异步队列,你可以理解为每个模块的 factorize 分解都是一个任务加入在队列中,在排到它时便会执行。

factorizeQueue 相似的功能队列还有 addModuleQueuebuildQueue,它们在初始化 compilation 实例时的定义如下:

// webpack/lib/Compilation.js
class Compilation {
  constructor(compiler, params) {
    this.processDependenciesQueue = new AsyncQueue({
      name: "processDependencies",
      parallelism: options.parallelism || 100,
      processor: this._processModuleDependencies.bind(this)
    });
    this.addModuleQueue = new AsyncQueue({
      name: "addModule",
      parent: this.processDependenciesQueue,
      getKey: module => module.identifier(),
      processor: this._addModule.bind(this)
    });
    this.factorizeQueue = new AsyncQueue({
      name: "factorize",
      parent: this.addModuleQueue,
      processor: this._factorizeModule.bind(this)
    });
    this.buildQueue = new AsyncQueue({
      name: "build",
      parent: this.factorizeQueue,
      processor: this._buildModule.bind(this)
    });
  }
  _processModuleDependencies(module, callback) { }
  _addModule(module, callback) { }
  _factorizeModule(params, callback) { }
  _buildModule(module, callback) { }
}
复制代码

一个模块的编译会经过 factorize 创建模块addModule 添加模块buildQueue 构建模块processDependencies 递归处理子依赖模块(如果有) 几个阶段。

而每个阶段的真正执行函数绑定在 Queue.processor 处理器上。

4.3、factorize 创建模块

_factorizeModule 下的链路比较长,先后经过:factory.create --> hooks.factorize --> hooks.resolve --> new NormalModule 得到 module 对象。

// webpack/lib/Compilation.js
_factorizeModule(
  {
    currentProfile,
    factory,
    dependencies,
    originModule,
    factoryResult,
    contextInfo,
    context
  },
  callback
) {
  factory.create({ context, dependencies, ... }, (err, result) => {
    callback(null, factoryResult ? result : result.module);
  });
}
复制代码

factory.create 是创建模块 module 的开始,这里的 factory 就是创建 compilation.params 时传入的 normalModuleFactory

create() 中执行 hooks.factorize.callSync,而注册 hooks.factorize.tapAsync 发生在初始化 NormalModuleFactory 实例时。

此外,在初始化时还注册了 hooks.resolve.tapAsync,它的执行时机更好在 hooks.factorize.tapAsync 之中。代码如下:

// webpack/lib/NormalModuleFactory.js
class NormalModuleFactory extends ModuleFactory {
  constructor() {
    this.hooks.factorize.tapAsync({}, (resolveData, callback) => {
      this.hooks.resolve.callAsync(resolveData, (err, result) => {
        ...
      }
    })
    this.hooks.resolve.tapAsync({}, (data, callback) => {
      ...
    })
  }
  create(data, callback) {
    const resolveData = {
      contextInfo,
      resolveOptions,
      context,
      request,
      dependencies,
      dependencyType,
      createData: {},
      cacheable: true
      ...
    };
    this.hooks.factorize.callAsync(resolveData, (err, module) => {
      const factoryResult = {
        module,
        fileDependencies,
        missingDependencies,
        contextDependencies,
        cacheable: resolveData.cacheable
      };
      callback(null, factoryResult);
    }
  }
}
复制代码

首先第一步是在 hooks.resolve.tapAsync 中:

  1. 调用 enhanced-resolve 第三方库得到资源基于 context 的绝对路径;
  2. 根据资源后缀(文件类型)收集 webpack.config.js 中配置的 loader,得到最终的 loaders 集合,这里涉及到 loader 先后顺序以及内联方式和配置方式的处理;
  3. 根据上述信息得到一个创建 module 时所需的数据 --> createData
// webpack/lib/NormalModuleFactory.js
this.hooks.resolve.tapAsync({}, (data, callback) => {
  // 创建一个 normal Resolve 实例
  const normalResolver = this.getResolver("normal", resolveOptions);
  let resourceData, loaders;

  const continueCallback = () => {
    ... 一系列 loader 规则处理
    // 生成 create module 相关数据集合
    Object.assign(data.createData, {
      layer:
        layer === undefined ? contextInfo.issuerLayer || null : layer,
      request: stringifyLoadersAndResource(
        allLoaders,
        resourceData.resource
      ),
      userRequest,
      rawRequest: request,
      loaders: allLoaders,
      resource: resourceData.resource, // 资源的完整绝对路径
      context:
        resourceData.context || getContext(resourceData.resource),
      matchResource: matchResourceData
        ? matchResourceData.resource
        : undefined,
      resourceResolveData: resourceData.data,
      settings,
      type,
      parser: this.getParser(type, settings.parser), // module 的 parse 依赖收集解析器
      parserOptions: settings.parser,
      generator: this.getGenerator(type, settings.generator), // module 的代码生成器
      generatorOptions: settings.generator,
      resolveOptions
    });
    callback();
  }

  // 执行 enhanced-resolve 第三方库解析模块路径,得到 resolvedResource
  this.resolveResource(
    contextInfo,
    context,
    unresolvedResource,
    normalResolver,
    resolveContext,
    (err, resolvedResource, resolvedResourceResolveData) => {
      if (resolvedResource !== false) {
        resourceData = {
          resource: resolvedResource,
          data: resolvedResourceResolveData,
          ...cacheParseResource(resolvedResource)
        };
      }
      continueCallback();
    }
  );
})
复制代码

有了 module createData,接下来就是创建 NormalModule 实例得到 module

this.hooks.factorize.tapAsync({}, (resolveData, callback) => {
  this.hooks.resolve.callAsync(resolveData, (err, result) => {
    const createData = resolveData.createData;
    this.hooks.createModule.callAsync(createData, resolveData, (err, createdModule) => {
      // 创建 module 实例
      if (!createdModule) createdModule = new NormalModule(createData);
      createdModule = this.hooks.module.call(createdModule, createData, resolveData);
      return callback(null, createdModule);
    })
  }
})
复制代码

一个 module 实例上,记录了 parse 以及 loader 相关信息,包含常用的属性和方法:

// webpack/lib/NormalModule.js
class NormalModule extends Module {
  constructor({ ...createData }) {
    this.request = request;
    this.parser = parser;
    this.generator = generator;
    this.resource = resource;
    this.loaders = loaders;
    this._source = null; // module 文件内容
  }
  createSource() {},
  _doBuild(options, compilation, resolver, fs, hooks, callback) {}
  build(options, compilation, resolver, fs, callback) {}
  codeGeneration() {}
  ...
}
复制代码

create module 完成后,将 result 回传给 callback 即回到了 this.factorizeModule 的回调中执行 addModule

// webpack/lib/Compilation.js
handleModuleCreation({ ... }, callback) {
  this.factorizeModule({ ... }, (err, factoryResult) => {
    const newModule = factoryResult.module;
    this.addModule(newModule, (err, module) => {
      ...
    });
  });
}
复制代码

4.4、addModule 存储模块

addModule AsyncQueue 的处理器是 _addModule,将 module 添加到 modules 集合中,后续在 seal 「生成阶段」会遍历 modules 读取模块代码。

// webpack/lib/Compilation.js
_addModule(module, callback) {
  const identifier = module.identifier();
  const alreadyAddedModule = this._modules.get(identifier);
  if (alreadyAddedModule) {
    return callback(null, alreadyAddedModule);
  }

  this._modulesCache.get(identifier, null, (err, cacheModule) => {
    if (cacheModule) {
      cacheModule.updateCacheModule(module);
      module = cacheModule;
    }
    this._modules.set(identifier, module);
    this.modules.add(module);
    callback(null, module);
  });
}
复制代码

module 添加完成后,执行 callback 回到 addModule 的回调中:

// webpack/lib/Compilation.js
handleModuleCreation({ ... }, callback) {
  this.factorizeModule({ ... }, (err, factoryResult) => {
    const newModule = factoryResult.module;
    this.addModule(newModule, (err, module) => {
      for (let i = 0; i < dependencies.length; i++) {
        const dependency = dependencies[i]; // entry dep
        moduleGraph.setResolvedModule(
          connectOrigin ? originModule : null,
          dependency,
          module
        );
      }
      this._handleModuleBuildAndDependencies(
        originModule,
        module,
        recursive,
        callback
      );
    });
  });
}
复制代码

4.5、buildModule 构建模块

接下来执行 _handleModuleBuildAndDependencies 进入 build 阶段。

build 阶段做了以下几件事情:

  1. 创建 loader context 上下文;
  2. 调用 loader-runner 第三方库提供的 runLoaders() 执行 loader 进行代码转换,这里会传入 resourceloadersloaderContext,得到一个转换后的源代码 result; 3、执行 createSource 创建 RawSource 实例到 module._source 上,通过 _source.source() 拿到文件转换后的源代码;
  3. 执行 parse(JavascriptParser) 对 source 进行 ast 解析,收集模块的依赖集合到 module.dependencies 中。

下面我们看看代码上的实现。

// webpack/lib/Compilation.js
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
  this.buildModule(module, err => {
    ...
  })
}
复制代码

首先判断是否需要 build,初次打包都会需要,接着执行 module.buildNormalModule.build,并执行 _doBuild 进行打包。

// webpack/lib/Compilation.js
_buildModule(module, callback) {
  module.needBuild({ ... }, (err, needBuild) => {
    this.hooks.buildModule.call(module);
    this.builtModules.add(module);
    module.build(
      this.options,
      this,
      this.resolverFactory.get("normal", module.resolveOptions),
      this.inputFileSystem,
      err => { 
        ... 
      }
    )
  })
}

// webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
  return this._doBuild(options, compilation, resolver, fs, hooks, err => {
    ...
  })
}
复制代码

_doBuild 中创建 loader 执行上下文,通过 runLoaders 执行 loader 转换代码,最后得到 this._source 对象。

// webpack/lib/NormalModule.js
_doBuild(options, compilation, resolver, fs, hooks, callback) {
  // 创建 loader 上下文
  const loaderContext = this._createLoaderContext(resolver, options, compilation, fs, hooks);
  // 生成 _source 对象
  const processResult = (err, result) => {
    const source = result[0];
    const sourceMap = result.length >= 1 ? result[1] : null;
    this._source = this.createSource(
      options.context,
      this.binary ? asBuffer(source) : asString(source),
      sourceMap,
      compilation.compiler.root
    );
    return callback();
  }
  // 调用 loader 进行代码转换
  runLoaders({
    resource: this.resource,
    loaders: this.loaders, // 配置的 loader
    context: loaderContext,
  }, (err, result) => {
    processResult(err, result.result);
  })
}
复制代码

_doBuild 执行完毕后回到 build 作用域下,对经过 loader 转换后的源代码进行 parse ast 解析,收集依赖模块,并生成 build module hash。

// webpack/lib/NormalModule.js
build(options, compilation, resolver, fs, callback) {
  return this._doBuild(options, compilation, resolver, fs, hooks, err => {
    const handleParseResult = result => {
      this._initBuildHash(compilation);
      return handleBuildDone();
    }

    const handleBuildDone = () => {
      // 创建快照
      compilation.fileSystemInfo.createSnapshot(..., (err, snapshot) => {
        return callback();
      })
    }

    const source = this._source.source();
    // 这里的 parse 是由 JavascriptParser.js 提供
    result = this.parser.parse(this._ast || source, {
      source,
      current: this,
      module: this,
      compilation: compilation,
      options: options
    });
    handleParseResult(result);
  })
}
复制代码

最后回到 build 时传递的 callback,将 module 存储在 _modulesCache 中:

// webpack/lib/Compilation.js
module.build(
  this.options,
  this,
  this.resolverFactory.get("normal", module.resolveOptions),
  this.inputFileSystem,
  err => { 
    this._modulesCache.store(module.identifier(), null, module, err => {
      this.hooks.succeedModule.call(module);
      return callback();
    });
  }
)
复制代码

至此,module 阶段就已完成,回到 this.buildModule 中执行 processModuleDependencies 处理依赖模块。

// webpack/lib/Compilation.js
this.buildModule(module, err => {
  this.processModuleDependencies(module, err => {
    callback(null, module);
  });
})
复制代码

4.6、processModuleDependencies

如果模块存在 dependencies 依赖,则会对子模块调用 handleModuleCreation() 进行上述构建步骤,否则执行 callback 模块编译结束。

// webpack/lib/Compilation.js
_processModuleDependencies(module, callback) {
  // 没有要处理的依赖
  if (sortedDependencies.length === 0 && inProgressTransitive === 1) {
    return callback();
  }
  // 处理子依赖
  for (const item of sortedDependencies) {
    this.handleModuleCreation(item, err => {
      ...
    });
  }
}
复制代码

现在,所有的依赖处理完成,依次完成 this.handleModuleCreation ---> this.addModuleTree ---> compilation.addEntry ---> compiler.hooks.make

4.7、compilation.finish

进入了 compilation.finish 意味着模块的 make 打包制作阶段完成。在这里,调用 hooks.finishModules 并收集模块构建过程中产生的 errorswarnings

// webpack/lib/Compilation.js
class Compilation {
  constructor(compiler, params) {
    this.errors = [];
    this.warnings = [];
  }
  finish(callback) {
    this.factorizeQueue.clear();
    const { modules } = this;
    this.hooks.finishModules.callAsync(modules, err => {
      for (const module of modules) {
        // 收集 error
        const errors = module.getErrors();
        if (errors !== undefined) {
          for (const error of errors) {
            this.errors.push(error);
          }
        }
        // 收集 warning
        const warnings = module.getWarnings();
        if (warnings !== undefined) {
          for (const warning of warnings) {
            this.warnings.push(warning);
          }
        }
      }
      this.moduleGraph.unfreeze();
      callback();
    });
  }
}
复制代码

最后

感谢阅读。

第二篇介绍了「生成阶段」和「写入阶段」,可以移步到这里查看 webpack5 打包流程源码剖析(2)

猜你喜欢

转载自juejin.im/post/7128705370335215630