手把手教你学习webapck插件plugin的开发


theme: smartblue

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

准备知识

作用

通过插件我们可以拓展webpack,加入自定义的构建行为,使webpack可以执行更广泛的任务,拥有更强的构建能力。

工作原理

webpack在编译过程中,会触发一些列Tapable钩子事件,插件所做的,就是找到相应的钩子,往上面挂上自己的任务,也就是注册事件。这样,当webpack构建的时候,插件注册的事件就会随着钩子的触发而执行了。

webpack内部的钩子

什么是钩子

钩子的本质就是:事件。为了方便我们直接介入和控制编译过程,webpack把编译过程中触发的各类关键事件封装成了事件接口暴露了出来。这些接口被很形象的称作:hooks(钩子)。开发插件,离不开这些钩子。

Tapble

Tapble为webpack提供了统一的接口(钩子)类型定义,他是webpack核心功能库,webpack中目前有很多中hooks,在Tapble源码中可以看到,他们是:

https://github.com/webpack/tapable/blob/master/lib/index.js

exports.__esModule = true;
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesLoopHook = require("./AsyncSeriesLoopHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

Tapble还统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:

  • tap:可以注册同步和异步钩子
  • tapAsync:回调方式注册异步钩子
  • tapPromise:Promise方式注册异步钩子

Plugin构建对象

Compiler

compiler对象中保存着完整的webpack环境配置,每次启动webpack构建时它都是一个独一无二,仅仅创建一次的对象。这个对象会在首次启动webpack时构建,我们可以通过compiler对象访问到webpack的主环境配置,如loader、plugin等配置信息。

compiler主要有以下属性:

属性名
options 可以访问本次启动的webpack时的所有配置文件,如loaders、entry、output、plugin等等配置信息。
inputFileSystem 可以进行文件操作,功能如Node.js内的fs
outputFileSystem 可以进行文件操作,功能如Node.js内的fs
hooks 可以注册Tabable的不同种类Hook,从而可以在compiler生命周期中植入不同的逻辑

Compilation

compilation对象代表一次资源的构建,compilation实例能够访问所有的模块和它们的依赖

一个compilation对象会对构建依赖图中所有模块,进行编译。在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、分块(chunk)、哈希(hash)和重新创建(restore)

它主要有以下属性

属性名
modules 可以访问所有模块,打包的每一个文件都是一个模块。
chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
assets 可以访问本次打包生成所有文件的结果。
hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。

生命周期简图

插件开发基础示例

插件基础结构示例

一个插件由以下构成

  • 一个具名 JavaScript 函数。
  • 在它的原型上定义 apply 方法。
  • 指定一个触及到 webpack 本身的 事件钩子
  • 操作 webpack 内部的实例特定数据。
  • 在实现功能后调用 webpack 提供的 callback。
// 一个 JavaScript class
class MyExampleWebpackPlugin {
  // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
  apply(compiler) {
    // 指定要附加到的事件钩子函数
    compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',
      (compilation, callback) => {
        ......
        callback();
      }
    );
  }
}

手写一个最简单的插件

创建一个test-plugin.js的文件

class TestPlugin {
  constructor() {
    console.log("我的第一个插件");
  }
  apply(){
    
  }
}

module.exports = TestPlugin;

webpack.config.js中引入并使用

const path = require("path");
const TestPlugin = require("./plugins/test-plugin");

module.exports = {
  entry: "./src/main.js",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "js/[name].js",
  },
  module: {
    rules: [],
  },
  plugins: [
    new TestPlugin(),
  ],
  mode: "production",
};

命令行执行 webpack,可以看到插件正常执行了。

image.png
webpack执行插件的流程:

  1. webpack创建compiler对象

  2. 遍历所有plugins中插件,调用插件的apply方法

  3. 执行剩下编译流程(触发各个hooks事件)

注:如果没有写apply函数,webpack会报错:Invalid configuration object. Webpack has been initialized using a configuration object that does not match the API schema.

注册hooks

钩子函数:https://v4.webpack.docschina.org/api/compiler-hooks/#相关钩子

钩子函数的调用,采用下面方法。

这个方法,接受两个参数,第一个参数是当前定义的类名,第二个参数是回调函数,如

// 一个 JavaScript class
class MyExampleWebpackPlugin {
  // 将 `apply` 定义为其原型方法,此方法以 compiler 作为参数
  apply(compiler) {
    // 指定要附加到的事件钩子函数
    compiler.hooks.emit.tapAsync('MyExampleWebpackPlugin',
      (compilation, callback) => {
        ......
        callback();
      }
    );
  }
}

注,回调函数的参数需要查看对应的钩子函数需不需要参数,如果没有写,代表没有参数。如:

如果有,回调函数的参数就是文档上写的,如:

environment钩子

根据environment钩子,我们来在插件内增加一些逻辑

class TestPlugin {
  constructor() {
    console.log("---------------------------------------------------------------");
  }
  apply(compiler) {
    // 由文档可知,environment是同步钩子,所以需要使用tap注册
    compiler.hooks.environment.tap("TestPlugin", () => {
      console.log("我是environment钩子");
    });
  }
}

module.exports = TestPlugin;

执行打包命令

image.png

可以发现,这个钩子会自动执行。

我们在学习几个常用的钩子

emit钩子

钩子类型 执行时机 参数
AsyncSeriesHook(异步串行钩子) 生成资源到 output 目录之前。 compilation

根据这个钩子,我们体验下三种调用方式的异同。

class TestPlugin {
  constructor() {
    console.log("-------------------------------------------------------------------------------");
  }
  apply(compiler) {
    // 由文档可知,environment是同步钩子,所以需要使用tap注册
    compiler.hooks.environment.tap("TestPlugin", () => {
      console.log("我是environment钩子");
    });
    // emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHook
    compiler.hooks.emit.tap("TestPlugin", (compilation) => {
      console.log("emit AAAAAAAAA");
    });
    // emitB:
    compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("emit BBBBBBBBB");
        // 使用tapAsync的调用方式,必须返回callback()代表结束
        callback();
      }, 2000);
    });
    // emitC:
    compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          console.log("emit CCCCCCCCC");
          // tapPromise的方式通过  resolve() 或 reject() 结束函数
          resolve();
        }, 1000);
      });
    });
  }
}

module.exports = TestPlugin;

通过示例,我们可以发现三种函数的写法是不一样,这是我们要注意的。然后,我们可以思考一下,代码执行后的打印顺序。

你可能有一个地方比较疑惑,emit是异步钩子,为什么还可以通过tap来注册我们的插件?

异步钩子可以通过tap方法来注册插件,但是回调函数内部只能写同步代码,不能写异步代码。

上述代码执行的顺序是:

image.png

通过这个示例,我们可以得出这样的结论

  • apply内的钩子函数是从上往下依次执行的
  • 执行过程中,遇到异步任务,会等异步任务执行完毕继续向下执行
  • 三种函数写法是不一样的

我们在看看异步并行钩子的执行顺序

make钩子

钩子类型 执行时机 参数
AsyncParallelHook(异步串行钩子) compilation
/*
  1. webpack加载webpack.config.js中所有配置,此时就会new TestPlugin(), 执行插件的constructor
  2. webpack创建compiler对象
  3. 遍历所有plugins中插件,调用插件的apply方法
  4. 执行剩下编译流程(触发各个hooks事件)
*/
class TestPlugin {
  constructor() {
    console.log("-------------------------------------------------------------------------------");
  }
  apply(compiler) {
    ......
    // 由文档可知,make是异步并行钩子 AsyncParallelHook
    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
      // 需要在compilation hooks触发前注册才能使用
      setTimeout(() => {
        console.log("TestPlugin make 111");
        callback();
      }, 3000);
    });

    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("TestPlugin make 222");
        callback();
      }, 1000);
    });

    compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
      setTimeout(() => {
        console.log("TestPlugin make 333");
        callback();
      }, 2000);
    });
  }
}

module.exports = TestPlugin;

根据执行结果,我们可以看出,对于异步并行钩子,是谁快谁先结束。

image.png

注:每个钩子也有自己优先级,比如make的优先级是高于emit的

compilation 钩子

compilation作为compiler钩子的回调函数参数,它也是有钩子函数的,用法是compiler钩子一直的。

使用node调试代码

我们如果想看compilation或compiler里面具体有什么内容,在命令行打印基本上是看不来东西的,实在太乱太多了!!!!我们可以借助node调试的方式进行查看。

首先,我们在项目内打个断点

class TestPlugin {
  constructor() {
    console.log("-------------------------------------------------------------------------------");
  }
  apply(compiler) {
    debugger
    console.log("compiler");
    // emitA:由文档可知,emit是异步串行钩子 AsyncSeriesHook
    compiler.hooks.emit.tap("TestPlugin", (compilation) => {
      console.log("compilation");
    });
  }
}

module.exports = TestPlugin;

我们在package.json中增加一段node代码

"scripts": {
  "debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
}
  • — insepct 是node内置的调试指令(借助浏览器控制台实现调试)
  • -brk 指在代码的首个位置打一个断点
  • ./node_modules/webpack-cli/bin/cli.js 指webpack打包命令一旦执行时就进入调试状态

我们在命令行执行 npm run debug

然后打开谷歌浏览器任意一个界面,进入控制台,稍等片刻,会发现出现一个node标志

点进去,就可以调试了!第一个断点是-brk生效了,这是webpack cli.js 的第一句代码

我们点击继续执行脚本,然后就可以调试了

现在,可以方便的看compiler里面到底是什么东西了!

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容