扩展插件

与 loader 相比,插件是一种更灵活的扩展 Webpack 的方法。您可以访问 Webpack 的编译器编译过程。可以运行子编译器,插件可以与 loader 一起工作,就像 MiniCssExtractPlugin 插件那样。

插件允许您通过钩子拦截 Webpack 的执行。Webpack 本身也是以插件集合的形式实现的。它的底层依赖于 tapable 插件接口,允许Webpack 以不同的方式应用插件。

接下来你将学会开发几个小插件。与 loader 不同,没有单独的环境可以运行插件,因此您必须使用 Webpack 本身运行它们。但是,可以将较小的逻辑在 Webpack 之外运行,因为这允许您单独对其进行单元测试。

Webpack 插件执行的基本流程

每个 Webpack 插件都需要向外暴露一个 apply(compiler) 方法。JavaScript 中有多种方式可以实现此操作。您可以使用一个函数,然后将该方法附加到它的 prototype 上。在最新的语法中,您可以使用 class 来达到同样的目的。

无论您采用何种方法,都应捕获用户在构造函数中传递的可能选项。声明某种模式并将它们传达给用户是个好主意。schema-utils 允许您验证选项,并且还可以与 loader 一起使用。

当插件连接到 Webpack 配置时,Webpack 将运行其构造函数,并把编译器对象作为参数调用 apply 函数。该对象包含了 Webpack 的插件 API,并允许您使用官方编译器参考中列出的钩子。

webpack-defaults 是开发 Webpack 插件的起点。它包含官方用于开发 loader 和插件的基础结构。

搭建开发环境

由于插件必须在 Webpack 中运行,因此您必须设置一个环境,以运行一个可进一步开发的 demo 插件:

webpack.plugin.js

const path = require("path");
const DemoPlugin = require("./plugins/demo-plugin.js");

const PATHS = {
  lib: path.join(__dirname, "app", "shake.js"),
  build: path.join(__dirname, "build"),
};

module.exports = {
  entry: {
    lib: PATHS.lib,
  },
  output: {
    path: PATHS.build,
    filename: "[name].js",
  },
  plugins: [new DemoPlugin()],
};

如果您还没有 lib 入口文件,那就新建一个。可以不必管其中的内容,只要它是 Webpack 可以解析的 JavaScript 文件就行。

为方便运行,设置一个 NPM 脚本:

package.json

"scripts":  {

  "build:plugin": "webpack --config webpack.plugin.js",
  ...  
},

执行它应该导致 Error: Cannot find module 失败,因为我们还有开发出 demo-plugin。

如果您需要交互式开发环境,请考虑在构件中启用 nodemon。Webpack 的观察模式在这种情况下不起作用。

实现一个基本插件

对于一个插件来说,至少要做两件事:捕获选项,并且提供 apply 方法:

plugins/demo-plugin.js

module.exports = class DemoPlugin {
  apply() {
    console.log("applying");
  }
};

如果您运行插件(npm run build:plugin),您应该在控制台上看到 applying 消息。鉴于大多数插件都接受选项,捕获并把它们传递 apply 函数是个好主意。

捕获选项

可以通过 constructor 捕获选项:

plugins/demo-plugin.js

module.exports = class DemoPlugin {
  constructor(options) {
    this.options = options;
  }
  apply() {
    console.log("apply", this.options);
  }
};

现在运行插件,控制台会打印出 apply undefined,因为没有传递任何选项。

调整配置以传递选项:

webpack.plugin.js

module.exports = {
  ...

  // plugins: [new DemoPlugin()],

  plugins: [new DemoPlugin({ name: "demo" })],

};

运行后,你应该可以看到 apply { name: 'demo' }

理解编译器和编译

apply 接收 webpack 的编译器作为参数。调整如下:

plugins/demo-plugin.js

module.exports = class DemoPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {
    console.log(compiler);
  }
};

运行后,您应该会看到很多数据。特别是 options 应该看起来很熟悉,因为它包含了 Webpack 配置。你还可以看到其他熟悉的名字,比如:records

如果你浏览 Webpack 的插件开发文档,你会看到编译器提供了大量的钩子。每个钩子对应一个特定的阶段。例如,要发出文件,您可以监听 emit 事件然后写入文件。

将实现更改为侦听和捕获 compilation

plugins/demo-plugin.js

module.exports = class DemoPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {

    // console.log(compiler);

    compiler.plugin("emit", (compilation, cb) => {
      console.log(compilation);

      cb();
    });

  }
};

如果插件中漏掉了回调函数,Webpack 会运行失败,并且不会有任何反馈信息!

运行构建应显示比以前更多的信息,因为编译对象包含 Webpack 遍历的整个依赖关系图。您可以访问与此相关的所有内容,包括入口,块,模块,资源等。

许多可用的钩子都可作用于编译结果,但有时它们基于非常具体的结构,并且需要更具体的研究来理解它们。

loader 可以通过下划线形式(this._compiler/ this._compilation)脏访问compilercompilation

通过 compilation 编写文件

compilation 中的 assets 对象,可用于编写新的文件。您还可以捕获已创建的资源,对其进行修改。

要编写资源,您必须使用 webpack-sources 来抽象文件。首先安装它:

npm install webpack-sources --save-dev

调整代码,通过 RawSource 进行文件写入:

plugins/demo-plugin.js

const { RawSource } = require("webpack-sources");

module.exports = class DemoPlugin {
  constructor(options) {
    this.options = options;
  }
  apply(compiler) {

    const { name } = this.options;

    compiler.plugin("emit", (compilation, cb) => {

      // console.log(compilation);

      compilation.assets[name] = new RawSource("demo");

      cb();
    });
  }
};

构建之后,您应该看到输出:

Hash: d698e1dab6472ba42525
Version: webpack 3.8.1
Time: 51ms
 Asset     Size  Chunks             Chunk Names
lib.js   2.9 kB       0  [emitted]  lib
  demo  4 bytes          [emitted]
   [0] ./app/shake.js 107 bytes {0} [built]

如果您检查 build/demo 文件,您将看到它包含上面代码中的单词 demo

compilation 有一组自己的钩子,请查看官方编译参考

管理警告和错误

可以通过抛出错误(throw new Error("Message"))终端插件的执行。如果验证选项,则可以使用此方法。

如果您想在编译期间向用户发出警告或错误消息,您应该使用compilation.warningscompilation.errors。例:

compilation.warnings.push("warning");
compilation.errors.push("error");

尽管有一个日志议案,但是没有其他办法可以将消息传递给 Webpack。如果您想将 console.log 用于此目的,请将添加在 verbose 标记后面。问题是 console.log 将打印到 stdout,结果将最终出现在 webpack 的 --json 输出中。用户可以使用该标记解决此问题。

插件还可以有插件

插件可以提供自己的钩子。html-webpack-plugin 使用插件来扩展自身,如“起步”一章中所述。

插件可以运行自己的编译器

在特殊情况下,比如 offline-plugin,运行子编译器是有意义的,它完全控制相关的入口和输出。该插件的作者 Arthur Stolyar 在 Stack Overflow 上解释了子编译器的想法

总结

当您开始设计插件时,可以花时间研究类似的现有插件。每次只开发插件的一小块,然后一一验证它。研究 Webpack 源代码可以提供更多的洞察力,因为它本身就是插件的集合。

回顾一下:

  • 插件可以拦截 Webpack 的执行并能够扩展 Webpack 的功能,使它们比 loader 更灵活。
  • 插件可以与 loader 结合使用。MiniCssExtractPlugin 就是这样工作的,随附的 loader 程序用于标记要提取的资源。
  • 插件可以访问 Webpack 的编译器编译过程。两者都为 Webpack 执行流程的不同阶段提供了钩子,并允许您操作它。Webpack 本身就是这样运作的。
  • 插件可以发出新资源并塑造现有资源。
  • 插件可以实现自己的插件系统。HtmlWebpackPlugin 是这种插件的一个例子。
  • 插件可以自己运行编译器。隔离提供了更多控制,并允许编写出像 offline-plugin 这样的插件。
用户头像
登录后发表评论