likes
comments
collection
share

Webpack5源码解读系列5 - 高可拓展Plugin机制

作者站长头像
站长
· 阅读数 14

Plugin介绍 & 使用

Plugin介绍

插件(Plugin)的概念

插件,又称做 Plug-in 或者 addin、addon 等,是一种遵循一定规范的应用程序接口编写出来的程序,从而可以为系统扩展原本不存在的特性。同时,如果一个系统支持了插件体系,也就拥有了可以实现用户自定义化的功能。

Plugin(插件)是Webpack实现的基础,Webpack运行时在各个阶段都会抛出事件,Plugin通过捕获这些事件完成功能。

Webpack内部,除构建主流程之外其他能力均通过Plugin机制完成,初始化Webpack时会根据传入配置项注入相对应Plugin,从而完成配置项工作。除此之外,Webpack允许用户使用第三方Plugin拓展功能。整体上来说Plugin机制让Webpack具有高拓展特性。

Webpack5源码解读系列5 - 高可拓展Plugin机制

Webpack提供了Plugin规范的接口协议,插件既可以以类形式编写,也可以以对象形式编写:

interface PluginInterface {
  apply: (compiler: Compiler) => void;
}

Plugin可在apply方法内部往传入的compiler实例注册事件,从而拓展编译器能力。

Plugin使用

Webpack配置项中提供plugins字段,用户可通过该字段注册自定义插件,如使用html-webpack-plugin的配置项如下:

const path = require('path');
const webpackHtmlPlugin = require('html-webpack-plugin');

module.exports = {
  plugins: [
    new webpackHtmlPlugin()
  ],
}

Plugin实现原理

Plugin执行时行为的本质是一个事件订阅,Webpack在运行各个阶段会做事件发布,从而完成插件能力,该过程中事件能力由Tapable库提供,了解Plugin之前我们先了解Tapable

Tapable机制

想更全面了解Tapable机制的同学可以查阅理解Webpack不得不提的Tapable

Tapable是专门为插件机制提供事件钩子注册/发布的库,为同步或异步场景提供了10种事件发布模型:

Webpack5源码解读系列5 - 高可拓展Plugin机制

按照执行过程可以将这10种模型分为4种类型钩子,分别是普通钩子、保险(bail)钩子、循环钩子、瀑布流钩子。同步和异步串行都有这四种类型钩子,而异步并行则因为执行特点则仅有普通、保险钩子。

  • 普通钩子

普通钩子类型是按照注册顺序执行钩子函数,并不关心返回值如何:

Webpack5源码解读系列5 - 高可拓展Plugin机制

  • 保险钩子

保险钩子是在同步钩子的基础上新增判断钩子返回值能力,一旦返回值不为undefined则会跳过所有钩子执行。

Webpack5源码解读系列5 - 高可拓展Plugin机制

  • 循环钩子

循环钩子机制有点类似于保险钩子,但是有一点不同:当钩子返回内容不为undefined时,会重新开始钩子执行而非中断执行。

Webpack5源码解读系列5 - 高可拓展Plugin机制

  • 瀑布流钩子

瀑布钩子是在同步钩子的基础上将前面钩子的返回值作为下一个钩子执行结果:

Webpack5源码解读系列5 - 高可拓展Plugin机制

TapableWebpack应用非常广泛,CompilerCompilation以及其他能力内随处可见使用Tapable管理事件,现在有了Tapable概念之后,我们看一下Plugin机制的实现。

Plugin实现

前置名词解释:CompilerCompilation分别代表什么?它们有什么关系?

  • Compiler:在Webpack运行期间常驻对象,内部可创建一个或者多个Compilation对象完成构建任务(watch模式下可能产生多次构建)。
  • CompilationCompiler产生构建任务时会新建一个Compilation对象并完成构建任务

基于前面Tapable的介绍,我们已经对插件运行机制有了一定的了解,Webpack分别在CompilerCompilation对象上提供事件钩子注册对象实现能力拓展,这里浅看一下代码即可:

  • Compiler 对象

Webpack5源码解读系列5 - 高可拓展Plugin机制

  • Compilation 对象

Webpack5源码解读系列5 - 高可拓展Plugin机制

前面提到Plugin需要遵循插件规范才能够被Webpack处理,在apply方法中插件会传入Compiler实例,可以在Compiler实例上注册钩子:

class CustomPlugin {
  apply(compiler: Compiler) {
    // do something
    compiler.hooks.xxxx.tap(xxx)
  }
}

Compiler上有个叫做compilation的钩子,可用该钩子往Compilation上注册事件,事件回调中第一个参数是Compilation实例,如下面例子:

compiler.hooks.compilation.tap(
class CustomPlugin {
  apply(compiler: Compiler) {
    compiler.hooks.compilation.tap('CustomHook', (compilation) => {});
  }
}

Plugin机制整体处理流程图如下:

Webpack5源码解读系列5 - 高可拓展Plugin机制

实例讲解

EntryOptionPlugin

EntryOptionPlugin提供处理入口配置项能力,Webpack核心流程并没有处理入口配置项逻辑能力,而是将其“外包”出去,使用Plugin承接处理入口配置项任务,假设有如下配置项:

module.exports = {
  entry: {
    index: "./src/js/index.js",
    home: './src/js/Home.js',
  },
}

经过入口配置项格式化之后会处理为如下:

module.exports = {
  entry: {
    index: {
      import: "./src/js/index.js",
    },
    home: {
      import: './src/js/Home.js',
    },
  },
}

之后便交给EntryOptionPlugin处理配置项内容:


class EntryOptionPlugin {
  /**
   * @param {Compiler} compiler the compiler instance one is tapping into
   * @returns {void}
   */
  apply(compiler) {
    compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {
      EntryOptionPlugin.applyEntryOption(compiler, context, entry);
      return true;
    });
  }

  static applyEntryOption(compiler, context, entry) {
    if (typeof entry === "function") {
      const DynamicEntryPlugin = require("./DynamicEntryPlugin");
      new DynamicEntryPlugin(context, entry).apply(compiler);
    } else {
      // 以对象形式配置,所以走此分支
      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);
        }
      }
    }
  }

  static entryDescriptionToOptions(compiler, name, desc) {
    // 无需关注
  }
}

EntryOptionPlugin内部判断配置项为非函数类型,判断为同步入口,交由EntryPlugin处理:

class EntryPlugin {
  constructor(context, entry, options) {
    this.context = context;
    this.entry = entry;
    this.options = options || "";
  }

  apply(compiler) {
    compiler.hooks.compilation.tap(
      "EntryPlugin",
      (compilation, { normalModuleFactory }) => {
        compilation.dependencyFactories.set(
          EntryDependency,
          normalModuleFactory
        );
      }
    );

    const { entry, options, context } = this;
    const dep = EntryPlugin.createDependency(entry, options);

    compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
      compilation.addEntry(context, dep, options, err => {
        callback(err);
      });
    });
  }

  static createDependency(entry, options) {
    const dep = new EntryDependency(entry);
    dep.loc = { name: typeof options === "object" ? options.name : options };
    return dep;
  }
}

重点关注两段代码:

  • dependencyFactories注入EntryDependency对应的Module工厂,简单来说是当Compilation解析到入口文件时,会实例化一个描述入口文件的Module模型。
compiler.hooks.compilation.tap(
  "EntryPlugin",
  (compilation, { normalModuleFactory }) => {
    compilation.dependencyFactories.set(
      EntryDependency,
      normalModuleFactory
    );
  }
);
  • 创建EntryDependency实例,并将其加入到Compilation入口,作为应用解析起点,这样Compilationmake阶段就可以根据起点递归解析整个应用,并将所有文件都处理为Module
const { entry, options, context } = this;
const dep = EntryPlugin.createDependency(entry, options);

compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
  compilation.addEntry(context, dep, options, err => {
    callback(err);
  });
});

Plugin机制在Webpack中应用特别广泛,Webpack除了打包过程中的模块读取、Chunk分析、代码生成、资源输出等关于流程上的代码由CompilerCompilation实现以外,其他具体的能力均通过Plugin机制完成,所以Webpack具有很强的可拓展性。

小结

Plugin减少了Webpack功能模块对核心模块的侵入性,给予了Webpack很强的自定义能力,在Webpack内部除了主流程之外其他能力均由Plugin完成,甚至连配置项最终都会转为Plugin,同时Webpack还提供自定义注册插件,用户可通过config.plugin字段配置第三方插件。