细说 webpack 插件之 Compiler 对象的创建

1、前文回顾

前文末尾调用 this.runWebpack 方法把前面的加载所得的 webpack 模块执行:代码如下

js 复制代码
class WebpackCLI {
  // ....
  
  async run () {
    // 加载 webpack
    this.webpack = await this.loadWebpack();
  }

  async loadWebpack(handleError = true) {
    // 等效于 require('webpack')
    return this.tryRequireThenImport(WEBPACK_PACKAGE, handleError);
  }
  
  async runWebpack(options, isWatchCommand) {
    let compiler;
    const callback = () => {}
    compiler = await this.createCompiler(options, callback);
  }

  async createCompiler(options, callback) {
    let config = await this.loadConfig(options);
    config = await this.buildConfig(config, options);
    let compiler;
    try {
      compiler = this.webpack(config.options, callback)
    } catch (e) {}

    return compiler;
  }
}
  1. 命令行: $ npm run dev
  2. 解析上面命令所得 webpack 命令执行
  3. webpack 命令解析到 webpack/bin/webpack.js 脚本
  4. webpack/bin/webpack.js 加载 webpack-cli/lib/index.js 创建 WebpackCLI 实例并调用 .run 方法
  5. WebpackCLI.prototype.run 加载加载 webpack/lib/webpack.js 得到 this.webpack
  6. 最后执行 this.runWebpack() 方法,进而执行 webpack/lib/webpack.js 导出的 webpack 方法创建 compiler 对象;

本文接上文继续讨论 webpack 启动后的的第一个环节------ 创建 compiler 实例对象。

2、创建 compiler

创建 compiler 的工作是由 webpack/lib/webpack.js 模块完成的;

以下为模块基础信息:

  • 路径:webpack/lib/webpack.js
  • 导出:函数 webpack
  • 参数:
    • options: 创建 compiler 所需要的选项对象
    • callback: 创建 compiler 对象处理编译结果的回调函数,内部用 callback 决定是否调用 compiler.run 自动启动编译

下面我们讨论该方法内部实现的细节问题:

2.1 整体结构

js 复制代码
const webpack = (options, callback) => {
  const create = () => {};

  if (callback) {
    // 传递 callback 的情况
    try {
      const { compiler, watch, watchOptions } = create();
      
      if (watch) {
        compiler.watch(watchOptions, callback);
      } else {
        compiler.run((err, stats) => {});
      }
      
      return compiler;
    } catch (err) {}
  } else {
    // 未传递 callback 的情况
    const { compiler, watch } = create();
    return compiler;
  }
}

module.exports = webpack;

形参:

  • options: 用户传入的创建编译器实例的配置项
  • callback: 处理 webpack 编译工作的回调函数

实参:

  • options: webpack.config.js 配置文件导出的配置选项
  • callback: WebpackCLI.prototype.runWebpack 中创建的 callback 方法(见上文 前文回顾 下有关 WebpackCLI 代码)

具体工作: webpack 方法做了以下工作

  1. 声明具体创建 compiler 的 create 内部方法,该方法内部处理创建 compiler 逻辑;
  2. 判断是否传入了 callback 决定是否启用编译(调用 compiler.run 方法)
    • 2.1 传入 callback,则调用私有的 create 方法创建 compiler 对象;接着判断是否是 watch 模式,若是则调用 compiler.watch 否则调用 compiler.run 启动编译;
    • 2.2 如果没有传入 callback 说明不需要自动启用编译,则在创建 compiler 对象后直接返回;

这里我们属于第一种情况,这里默认的情况中,webpack-cli 是创建了 callback 这个回调的,需要自动开启编译,这个也符合我们的直觉; 另一种 需要外界传入 callback 的场景是给自定义编译的情况准备的,比如很多的跨端框架,包括封装了构建流程的脚手架比如 React 的 script 都是这个特性的典型应用场景;

2.2 create 方法

下面我们看下 create 方法:

js 复制代码
const create = () => {
  if (!asArray(options).every(webpackOptionsSchemaCheck)) {
    getValidateSchema()(webpackOptionsSchema, options);
  }
  let compiler;
  let watch = false;
  let watchOptions;
  if (Array.isArray(options)) {
    // opitons 是数组,创建多编译器
    compiler = createMultiCompiler(
      options,
    );
    watch = options.some(options => options.watch);
    watchOptions = options.map(options => options.watchOptions || {});
  } else {
    // options 不是数组,创建一个编译器实例就好了
    const webpackOptions = options;
    compiler = createCompiler(webpackOptions);
    watch = webpackOptions.watch;
    watchOptions = webpackOptions.watchOptions || {};
  }
  
  // 返回结果:
  // compiler: Compiler 编译器实例
  // watch: 是否是 watch 模式表示
  // watchOptions: watchOptions watch 模式的选项对象
  return { compiler, watch, watchOptions };
};

create 方法做了以下工作:

  1. 校验用户传递的 options 是否符合 webpack 内部约定好的 schema,如果不符合就会立刻跑出异常信息;这一点还挺好用的,业务中可以校验一波;
  2. 根据传入的 options 类型分类讨论:
    • 2.1 如果传入的 options 是数组,说明本项目需要多编译器进行编译,则调用 createMultiCompiler 方法创建多编译器;然后判断这些 options 中是否存在需要 watch 模式的,最后再取得各个配置项中 watchOption 配置;
    • 2.2 不是数组的情况,则说明是一个编译器的情况,则调用 createCompiler 方法创建编译器实例对象,然后获取是否 watch 模式表示以及 watchOptions 配置项目;
  3. 返回前面两步创建的 compiler,watch,watchOptions
    • 3.1 compiler: Compiler 编译器实例
    • 3.2 watch: 是否是 watch 模式表示
    • 3.3 watchOptions: watchOptions watch 模式的选项对象

我们这里讨论的是创建单个 Compiler 实例对象的情况,所以我们暂时不展开 createMultiCompiler 方法,只表 createCompiler 方法;

2.3 createCompiler 方法

createCompiler 函数创建了一个编译器对象,并根据传入的 rawOptions 配置对 compiler 进行初始化。接着设置了一些默认选项、初始化插件,并调用了一些钩子函数,最后返回 compiler 对象。

js 复制代码
const createCompiler = rawOptions => {
    const options = getNormalizedWebpackOptions(rawOptions);
    applyWebpackOptionsBaseDefaults(options);
    const compiler = new Compiler(options.context, options);
    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();
    new WebpackOptionsApply().process(options, compiler);
    compiler.hooks.initialize.call();
    return compiler;
};

下面我们来看这个方法的具体工作:

  1. 调用 getNormalizedWebpackOptions 格式化 rawOptions,返回经过处理的 webpack 标准配置对象,用以抹平各种语法的配置对象;
  2. 经过上一步拿到标准的 webpack 配置选项对象后,调用 applyWebpackOptionsBaseDefaults 进行基础配置如 context/target/mode 等选项的初始化,比如 context 的值被设置为当前的工作目录;
  3. 创建 Compiler 对象实例,传入经过处理的 options 对象;
  4. 初始化 NodeEnvironmentPlugin 插件,NodeEnvironmentPlugin 是 Compiler 工作中相当依赖的一个模块,主要负责文件 I/O、监听文件内容改变。由四个基本的文件系统组成:inputFileSystem、outputFileSystem 处理文件i/o,watchFileSystem 监听文件改动,intermediateFileSystem则处理所有不被看做是输入或输出文件系统操作,比如写记录,缓存或者分析输出等。
  5. 初始化插件调用,webpack 内部插件以及我们自定义的插件上定义的 apply 方法就是在此时调用的,调用这些插件的 apply 方法时会传入 compiler 对象,通过 compiler 对象则可以注册全生命周期的钩子;
  6. 调用 applyWebpackOptionsDefaults 方法对 webpack 设置默认选项,根据传入的配置对象 options,对设置webpack配置的默认值,根据不同的配置项进行赋值。
  7. 调用 new WebpackOptionsApply().process(options, compiler) 方法,利用 WebpackOptionsApply 类型完成 compiler 特性的应用工作。webpack 高度封装了内部特性,外部配置中的每个 key 对应的特性都是由一个或者组合多个插件实现,而 WebpackOptionsApply 就是根据 options 注册对应的插件,这个过程像不像中医照方子抓药的过程,所谓"方子"就是 "options" 配置,而"插件"就是各式各样的"中药材";
  8. 触发 compiler.hooks.environment、compiler.hooks.afterEnvironment、compiler.hooks.initialize 钩子;
  9. 返回 compiler 对象

2.4 WebpackOptionsApply

  1. 注册 JavascriptModulesPlugin、JsonModulesPlugin、WebAssemblyModulesPlugin 插件,为不同类型的模块注册 Parser 和 Generator;
  2. 为 entry 声明 EntryPlugin;
  3. 注册性能优化相关 ModuleConcatenationPlugin/SplitChunkPlugin,当然还有 treeshaking 相关的,这里没有列出;
  4. 处理 if (options.cache && ... 开启持久化缓存;

3、总结

本文主要讨论了 webpack/lib/webpack.js 导出的方法 webpack 及其创建 Compiler 实例(编译器)对象的主要过程,主要集中在 createCompiler 方法中,这篇你需要了解的点有以下这些:

  1. 创建编译器会根据 options 是否为数组决定创建多编译器或者是单独编译器,当然后面我们的主要精力都在单编译器实例的情境中;
  2. applyWebpackOptionsBaseDefaults 方法进行基础配置如 context/target/mode 等选项的初始化,比如 context 的值被设置为当前的工作目录;
  3. 通过 Compiler 这个类型实例化一个 compiler 实例对象;
  4. 用户定义的插件在 Compiler 实例化之后被应用;
  5. 调用 applyWebpackOptionsDefaults 方法对 webpack 设置默认选项;
  6. 调用 new WebpackOptionsApply().process(options, compiler) 方法,利用 WebpackOptionsApply 类型依据前面得出的 options 进行默认插件引用以及一些内建特性的应用;
  7. compiler.resolverFactory.hooks.resolveOptions.for() 为 normal/loader/context 声明创建 Resolver 路径解析器所需的配置项;
相关推荐
残花月伴5 分钟前
axios
javascript
m0_7482345227 分钟前
2025最新版Java面试八股文大全
java·开发语言·面试
van叶~34 分钟前
仓颉语言实战——2.名字、作用域、变量、修饰符
android·java·javascript·仓颉
邂逅岁月1 小时前
滑不动窗口的秘密—— “滑动窗口“算法 (Java版)
算法·面试·求职招聘·创业创新·刷题
sunny-ll1 小时前
【C++】explicit关键字详解(explicit关键字是什么? 为什么需要explicit关键字? 如何使用explicit 关键字)
c语言·开发语言·c++·算法·面试
泯泷1 小时前
JS代码混淆器:JavaScript obfuscator 让你的代码看起来让人痛苦
开发语言·javascript·ecmascript
高兴蛋炒饭8 小时前
RouYi-Vue框架,环境搭建以及使用
前端·javascript·vue.js
ᥬ 小月亮9 小时前
Vue中接入萤石等直播视频(更新中ing)
前端·javascript·vue.js
夜斗(dou)9 小时前
node.js文件压缩包解析,反馈解析进度,解析后的文件字节正常
开发语言·javascript·node.js
恩爸编程10 小时前
纯 HTML+CSS+JS 实现一个炫酷的圣诞树动画特效
javascript·css·html·圣诞树·圣诞树特效·圣诞树js实现·纯js实现圣诞树