美文网首页
webpack 之 Compiler 、Compilation

webpack 之 Compiler 、Compilation

作者: AizawaSayo | 来源:发表于2021-05-11 19:12 被阅读0次

官方解释:【Compiler and Compilation】以及 【Writing a Plugin】,翻译并展开如下:

开发插件时最重要的两个概念是 compiler 编译器和 compilation 编译对象。理解它们的角色是扩展 webpack 引擎重要的第一步。

一个 webpack 插件需要具备:

  • 是一个JavaScript命名函数或者JavaScript类
  • 定义一个名为 apply 的原型方法(插件最核心的部分
  • 指定要钩入/监听的事件 hook
  • 处理webpack内部实例特定的数据
  • 功能完成后,调用webpack提供的回调

传入的 compiler 是 webpack 初始化时创建的 Compiler 实例,它上面的 hooks 属性,用于将插件注册到 compiler 生命周期中的各种钩子事件上。

// 一个插件类基本结构示例:
class HelloCompilationPlugin {
  constructor(options = {}) {
    // 省略构造器部分
  }
  // 调用原型方法 apply 并传入 compiler 对象
  apply(compiler) {
    // 给 compilation 钩子注册 'HelloCompilationPlugin',回调会在 compilation 对象创建之后触发
    compiler.hooks.compilation.tap('HelloCompilationPlugin', (compilation) => {
      // 回调参数是 compilation 对象,因此这里可以使用各种可用的 compilation hooks(钩子)
      compilation.hooks.optimize.tap('HelloCompilationPlugin', () => {
        // 优化阶段开始时触发
        console.log('资源正在优化');
      });
    });
    // 在 emit 阶段,即输出 asset 到 output 目录之前触发回调函数
    compiler.hooks.emit.tapAsync(
      'HelloCompilationPlugin',
      (compilation, callback) => {
        console.log('This is an example plugin!');
        console.log(
          '`compilation`对象,即资源的一个单一版本的构建',
          compilation
        );
        // 使用webpack提供的 plugin API 操作构建
        compilation.addModule(/* ... */);
        callback();
      }
    );
  }
}

module.exports = HelloCompilationPlugin;

在 webpack 中以new XxxPlugin的方式配置插件,用于在 Compiler 实例对象上注册

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

module.exports = {
  plugins: [
    //... 其他插件
    // 创建插件实例
    new HelloCompilationPlugin({...options})
  ]
}

Compiler 模块是 webpack 的主要引擎,它扩展自 Tapable 类。在执行 webpack 构建的准备阶段,会创建一个Compiler的实例,然后配置项传递的实例化插件以及webpack内置插件都会在该 compiler 对象上注册。具体就是依次调用插件的 apply 方法,并将 compiler 对象 (包含webpack的各种配置信息) 传进去供 plugin 使用,compiler 包含整个构建流程的全部钩子,通过它可以把控整个 webpack 构建周期。在运行期间 compiler 会根据 webpack 不同阶段触发的各种事件钩子,执行插件附加/绑定在 hook 上的函数。 compiler 只是负责维持生命周期运行的功能,所有的加载、打包和写入工作,都被委托到注册过的插件上了。webpack 使用 WebpackOptionsDefaulterWebpackOptionsApply 来配置 Compiler 实例以及所有内置插件。
Compiler 类实例化并注册 plugins 后,若 webpack 函数接收了回调callback,会执行compiler.run()方法,webpack即刻开启编译之旅。如果未指定callback回调,则需要用户自己调用run方法来启动编译。

上面说的 Compiler 实例化、插件注册及启动编译 (run or watch方法),详见下面/lib/webpack.js源码[简化版]

// /lib/webpack.js
const webpack = (options, callback) => {
// 创建 Compiler 类的实例
  const compiler = new Compiler(options.context);
  compiler.options = options;
  // 注册所有自定义插件
  if (Array.isArray(options.plugins)) {
    // 遍历传入的 webpack 配置中的实例化插件数组
    for (const plugin of options.plugins) {
      if (typeof plugin === "function") {
        // 在compiler对象的作用域下调用plugin构造函数,即this指向compiler;同时把compiler对象当作参数传过去。并且compiler对象会继承plugin的所有属性、方法
        plugin.call(compiler, compiler);
      } else {
        // 如果 plugin 是其他类型,就执行plugin对象的apply方法。
        // plugins 数组的内容一般都是一个个插件实例化对象,也就是 object。
        // 
        plugin.apply(compiler);
      }
     }
  }
  applyWebpackOptionsDefaults(options);
  // 触发 compiler 的 两个 hook: environment,afterEnvironment
  compiler.hooks.environment.call();
  compiler.hooks.afterEnvironment.call();
  // 根据 options 的配置不同,注册激活一些默认自带的插件和 resolverFactory.hooks
  // 大部分插件的作用是往 compiler.hooks:compilation,thisCompilation 里注册一些事件
  new WebpackOptionsApply().process(options, compiler);
  compiler.hooks.initialize.call();
  // 获取是否以watch监听模式启动的 webpack 以及 监听相关配置
  let watch = options.watch || false;
  let watchOptions = options.watchOptions || {};
  if (callback) { // 如果传递了回调
    if (watch) { // 配置传了 watch 则调用监听模式启动 webpack
      compiler.watch(watchOptions, callback);
    } else { // 启动 compiler.run,即开启编译工作, webpack 的核心构建流程
      compiler.run((err, stats) => { 
      // stats 对象是编译过程中的有用信息, 包括:
      //*   错误和警告(如果有的话)
      //*   计时信息
      //*   module 和 chunk 信息
      // webpack CLI 正是基于这些信息在控制台 展示友好的格式输出。
        compiler.close(err2 => {
          callback(err || err2, stats);
        });
      });
    }
    return compiler;
  } else {
    if (watch) {
      util.deprecate(() => {}, "watch模式必须提供callback回调函数!", "DEP_WEBPACK_WATCH_WITHOUT_CALLBACK")();
    }
    return compiler;
  }
}

module.exports = webpack;

Tapable 是什么?GitHub文档
它是一个管理钩子事件监听与触发的小型库,export 了许多Hook类(hook 构造函数),如 SyncHook、SyncBailHook 等,可以用来为插件创建 hooks。除此之外,还暴露了 tap,tapAsynctapPromise 这些绑定 hook 的方法(除此之外还有 intercept -钩子拦截器),用来向 webpack 注入自定义构建的步骤。整体类似发布订阅模式

tap: (name: string | Tap, fn: (context?, ...args) => Result) => void
tapAsync: (name: string | Tap, fn: (context?, ...args, callback: (err, result: Result) => void) => void) => void
tapPromise: (name: string | Tap, fn: (context?, ...args) => Promise<Result>) => void
第一个参数 name 或 Tap 对象:name是绑定事件名,通常用来识别插件,因此一般就传插件名称;Tab对象可以添加一些具体的信息。
第二个参数是回调函数,即该事件被触发时,需要做的事情。回调的 context参数是可选值,表示上下文对象,且需要第一个参数传Tab对象:{ name: 'myPluginxxx', context: true, ... }
整体和事件侦听器的机制类似。

其中Tab参数的接口:

interface Tap {
    name: string,
    type: string
    fn: Function,
    stage: number,
    context: boolean,
    before?: string | Array
}

同步 hook 只能使用 tap 方法;而异步 hook 除了 tapAsync 和 tapPromise 这些异步方法,也支持用 tap 方法让 hook 以同步方式运行。

当使用 tapAsync method来访问插件时,需要调用作为函数的最后一个参数提供的回调函数。

class HelloAsyncPlugin {
    compiler.hooks.emit.tapAsync(
      'HelloAsyncPlugin',
      (compilation, callback) => {
        // Do something async...
        console.log('以异步方式触及运行钩子');
        setTimeout(function () {
          console.log('Done with async work...');
          callback();
        }, 1000);
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

当我们使用 tapPromise method来访问插件时,则需要返回一个promise,它可以在异步任务完成时解决。

class HelloAsyncPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapPromise('HelloAsyncPlugin', (compilation) => {
      // return a Promise that resolves when we are done...
      return new Promise((resolve, reject) => {
        setTimeout(function () {
          console.log('以异步的方式触发具有延迟操作的钩子。');
          resolve();
        }, 1000);
      });
    });
    // 在回调用上 async await
    compiler.hooks.run.tapPromise('MyPlugin', async (source, target, routesList) => {
        await new Promise((resolve) => setTimeout(resolve, 1000));
        console.log('以异步的方式触发具有延迟操作的钩子。');
      }
    );
  }
}

module.exports = HelloAsyncPlugin;

我们可以有多种方式 hook 到 compiler 中,hook将编译一个方法,可以让各种插件都以最合适有效的方式去运行。

创建钩子的方法:const hook = new SyncHook(["arg1", "arg2", "arg3"])
在类的 constructor 的 this.hooks 内用生成 Hook 类实例的方式定义 hooks,表明它们的类型并传入参数名称数组;
在 webpack 运行特定阶段(比如 compiler.run 和 compile)通过callcallAsyncpromise调用这些 hook,声明注册在这个 hook 上的插件 (内的方法) 触发的时机。类实似于发布-订阅模式中的发布事件
插件通过taptapAsync等方法订阅 Compiler 或 Compilation 实例的 hook,方法内可以调用 api 进行一些处理。webpack 在运行过程中,当这些特定的 hook 事件被广播时,订阅了该 hook 的插件在监听到后就会执行绑定的逻辑。
掌握 webpack 流程中 compiler、compilation 对象事件钩子触发的时机,就是掌握如何写一个插件的关键。

具体让我们看Compiler类源码:

// /lib/Compiler.js
const {
    SyncHook,
    SyncBailHook,
    AsyncParallelHook,
    AsyncSeriesHook
} = require("tapable");

class Compiler {
  constructor(context) {
    // 定义一堆hook,done,beforeRun,run,emit等等
    this.hooks = Object.freeze({
      /** @type {SyncBailHook<[Compilation], boolean>} */       
      run: new AsyncSeriesHook(["compiler"]), // 在开始读取records之前调用
      /** @type {SyncHook<[Compilation, CompilationParams]>} */
      thisCompilation: new SyncHook(["compilation", "params"]), // 初始化 compilation 时调用,在触发 compilation 事件之前调用
      /** @type {AsyncSeriesHook<[Compilation]>} */
      emit: new AsyncSeriesHook(["compilation"]), // 输出 asset 到 output 目录之前执行
      /** @type {AsyncSeriesHook<[Compilation]>} */
      afterEmit: new AsyncSeriesHook(["compilation"]), // 输出 asset 到 output 目录之后执行
      /** @type {AsyncSeriesHook<[Stats]>} */
      done: new AsyncSeriesHook(["stats"]), // 在 compilation 完成时执行
    })
  }
  watch(watchOptions, handler) {} // 以监听模式执行 webpack 打包的方法
  run(callback) { // run 即为执行 webpack 打包的主流程函数
    const onCompiled = (err, compilation) => {})
    const run = () => { 
      this.hooks.beforeRun.callAsync(this, err => {
        this.hooks.run.callAsync(this, err => {
          if (err) return finalCallback(err);
          this.readRecords(err => { // 读取之前的 records
            if (err) return finalCallback(err);
            this.compile(onCompiled); //  在 compile 过程后调用 onCompiled,主要用于输出构建资源
          });
        });
      });
    };
  } 
  compile(callback) { // compile 是真正进行编译的方法,最终会把所有原始资源编译为目标资源。
    const params = this.newCompilationParams();
    this.hooks.beforeCompile.callAsync(params, err => {
      if (err) return callback(err);
      this.hooks.compile.call(params);
      // createCompilation方法主要就是清除之前的compilation,重新实例化一个Compilation
      const compilation = this.createCompilation();
      compilation.name = this.name;
      compilation.records = this.records;
      // 触发compiler.hooks:thisCompilation 和 compilation
      // 注册plugins阶段在这两个钩子注册的事件在拿到compilation对象后开始执行
      this.hooks.thisCompilation.call(compilation, params);
      this.hooks.compilation.call(compilation, params);
      return compilation;
    }
  }
}

Compiler 有几个重要的方法,watchrun是启动/执行 webpack 构建的函数,而compile是负责编译的。 它触发 compile 钩子并实例化了一个 compilation,再触发自己的 make 钩子把 compilation 对象作为参数传过去。在后续编译过程中会触发 Compilation 的一系列海量 hooks:如buildModulesucceedModulesucceedEntrysealchunk,实现模块的加载、封闭、优化、分块,哈希和重建等等,并将当前模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态等相关信息保存到 compilation 对象。Compilation.js 类的源码很长,具体编译流程也会涉及到 webpack 的许许多多其他模块,就不在这篇说了。

compiler 钩子触发时机和 webpack 执行流程对照

总结:

  • Compiler类(./lib/Compiler.js):webpack的主要引擎,扩展自Tapable。webpack 从执行到结束,Compiler只会实例化一次。生成的 compiler 对象记录了 webpack 当前运行环境的完整的信息,该对象是全局唯一的,插件可以通过它获取到 webpack config 信息,如entry、output、loaders等配置。

  • Compilation类(./lib/Compilation.js):扩展自Tapable,也提供了很多关键点回调供插件做自定义处理时选择使用拓展。一个 compilation 对象代表了一次单一的版本构建和生成资源,它储存了当前的模块资源、编译生成的资源、变化的文件、以及被跟踪依赖的状态信息。简单来说,Compilation的职责就是对所有 require 图(graph)中对象的字面上的编译,构建 module 和 chunk,并利用插件优化构建过程,同时把本次打包编译的内容全存到内存里。compilation 编译可以多次执行,如在watch模式下启动 webpack,每次监测到源文件发生变化,都会重新实例化一个compilation对象,从而生成一组新的编译资源。这个对象可以访问所有的模块和它们的依赖(大部分是循环依赖)。

Compiler 和 Compilation 的区别

compiler 对象代表的是构建过程中不变的 webpack 环境,整个 webpack 从启动到关闭的生命周期。针对的是webpack。
compilation 对象只代表一次新的编译,只要项目文件有改动,compilation 就会被重新创建。针对的是随时可变的项目文件。

关于 compiler hookscompilation hooks 以及其他重要对象上可用的 hooks 列表,请参阅 plugins API 文档。
参考:webpack插件编写及原理解析

相关文章

网友评论

      本文标题:webpack 之 Compiler 、Compilation

      本文链接:https://www.haomeiwen.com/subject/pcdudltx.html