浅谈Webpack Plugin

1,227 阅读6分钟

你想深入学习webpack吗?你想自己开发一个webpack plugin吗?我就问问,不想就算了吧!

了解一下什么是Plugin?

官方文档定义:插件是 webpack 的 支柱 功能。webpack 自身也是构建于你在 webpack 配置中用到的相同的插件系统之上!

插件目的在于解决 loader 无法实现的其他事

Webpack Plugin其实就是一个类!!!

创建一个 Plugin

先学会一个简易版的plugin

// myPlugin.js
class MyPlugin {
	constructor() {
    console.log('Plugin 被创建了');
  }
  apply(compiler) {}
}

module.exports = MyPlugin;

// webpack.config.js
// 引入
const MyPlugin = require('myPlugin.js');
module.exports = {
	plugins: [
  	new MyPlugin({title: 'MyPlugin'}),
  ]
};

我们先来看一下 Webpack ****官方给的案例:

const pluginName = 'ConsoleLogOnBuildWebpackPlugin';

class ConsoleLogOnBuildWebpackPlugin {
    apply(compiler) {
        // 代表开始读取 records 之前执行
        compiler.hooks.run.tap(pluginName, compilation => {
            console.log("webpack 构建过程开始!");
        });
    }
}

简单说明

  • Plugin 其实就是一个类。
  • 类需要一个 apply 方法,执行具体的插件方法。
  • 插件方法做了一件事情就是在 run 这个 Hook 上注册了一个同步的打印日志的方法。
  • apply 方法的入参注入了一个 compiler 实例,compiler 实例是 Webpack 的支柱引擎,代表了 CLI 和 Node API 传递的所有配置项。
  • Hook 回调方法注入了 compilation 实例,compilation 能够访问当前构建时的模块和相应的依赖。

就这?就这?就这?


接下来讲解下基础知识点

什么是Hook?

Webpack 在编译的过程中会触发一系列流程,而在这样一连串的流程中,Webpack 把一些关键的流程节点暴露出来供开发者使用,这就是 Hook,可以类比 React / Vue的生命周期钩子。

Plugin 就是在这些 Hook 上暴露出方法供开发者做一些额外操作,因此在写 Plugin 的时候,也需要先了解我们应该在哪个 Hook 上做操作。

Webpack 共提供了以下十种 Hooks,代码中所有具体的 Hook 都是以下这 10 种中的一种。

// 源码取自:lib/index.js
"use strict";

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");

// 以下 2 个是 hook 工具类,分别用于 hooks 映射以及 hooks 重定向
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

Hook 的类型可以通过官方 API 查询,地址传送门:www.webpackjs.com/api/compile…

核心工具库Tapable

Tapable是什么?webpack就是一个事件流,这个事件流是一个个插件组成的,而保证这些插件有序的运行就是Tapable的作用,它是采用发布订阅的架构模式。tapable中有很多钩子方法,用来搜集注册的plugin。简单来说Tapable就是webpack用来创建钩子的库。

Tapable 是 Webpack 核心工具库,它提供了所有 Hook 的抽象类定义,Webpack 许多对象都是继承自 Tapable 类。比如: tap,tapAsync和tapPrimise。

// 第二节 “创建一个 Plugin” 中说的 10 种 Hooks 都是继承了这两个类
// 源码取自:tapable.d.ts
declare class Hook<T, R, AdditionalOptions = UnsetAdditionalOptions> {
  tap(options: string | Tap & IfSet<AdditionalOptions>, fn: (...args: AsArray<T>) => R): void;
}

declare class AsyncHook<T, R, AdditionalOptions = UnsetAdditionalOptions> extends Hook<T, R, AdditionalOptions> {
  tapAsync(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: Append<AsArray<T>, InnerCallback<Error, R>>) => void
  ): void;
  tapPromise(
    options: string | Tap & IfSet<AdditionalOptions>,
    fn: (...args: AsArray<T>) => Promise<R>
  ): void;
}

什么是compiler?

compiler包含了webpack环境所有的配置信息。 这个对象在启动webpack时被一次性建立,并配置好所有可操作的设置,包括options,loader和plugin。当在webpack环境中应用一个插件时,插件将收集到此compiler对象的引用。可以使用它来访问webpack的主环境。

什么是compilation?

compilation对象包含了当前模块资源、编译生成资源、变化的文件等。当运行webpack开发环境中间件时,没检测到一个文件变化,就会创建资源。

compilation对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。

compiler和compilation的区别?

Compiler 对象包含了 Webpack 环境所有的的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;

Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
                                              —— 摘自「深入浅出 Webpack」
  1. 生成时间不同,周期长度不同。compiler在webpack开始到关闭整个生命周期存在,而compilation只是代表了一次新的编译过程。
  2. compiler和compilation暴露出的钩子不同。

compiler常用的钩子

Hooktype调用
runAsyncSeriesHook开始读取 records 之前
compileSyncHook一个新的编译(compilation)创建之后
emitAsyncSeriesHook生成资源到 output 目录之前
doneSyncHook编译(compilation)完成
initializeSyncHook当编译器对象被初始化时调用(html-webpack-plugin中有用到)

compilation常用的钩子

Hooktype调用
buildModuleSyncHook在模块构建开始之前触发。
finishModulesSyncHook所有模块都完成构建。
optimizeSyncHook优化阶段开始时触发。

其他的就不做过多介绍了,附上官网的链接webpack.docschina.org/api/compile…


完成一个完整的Webpack Plugin

Webpack 插件的名称都是叫xxx-webpack-plugin,Plugin demo就这样开始了。

Copyright-webpack-plugin

这是一个简单的demo,通过plugin生成一个markdown文件,文件中输出'written by zhulm'

// plugin class
class CopyrightWebpackPlugin {
  constructor () {}
  apply(compiler) {
    // console.log('compiler: ', compiler);
    compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', 
      (compilation,cb) => {
        // console.log('compilation: ', compilation);
        compilation.assets['copyright.md'] = {
          source: function() {
            return 'written by zhulm';
          },
          size: function() {
            return 15;
          },
        }
        cb()
      }
    )
  }
}
module.exports = CopyrightWebpackPlugin;

代码讲解:

  1. 主要是运用单compiler的钩子的emit方法,使用这个方法创建一个copyright.md文件,写入文字。
  2. tapAsync方法来调用钩子,需要转入两个参数,第一个参数是本身插件的名称,第二个参数是一个函数。
  1. source是源码文件,而size是生成文件的大小,返回 15 表示生成的文件大小是 15 byte。
  2. assets: { [pathname: string]: Source } — 普通对象,其中 key 是 asset 的路径名,value 是 asset 的数据

详情见我的github地址:github.com/dakoujia/we…

OutputCurrentBuildInfoPlugin

这个插件是获取输出信息的一个插件,获取git的提交信息及时间

new OutputCurrentBuildInfoPlugin({
      outputName: 'build-log.json',
      dateFormatType: 'YYYY-MM-DD HH:mm:ss',
      buildType: 'local'
    }),

生成的build-log.json

// local
{"build_time":"2021-07-04 20:28:20","build_type":"local","git":{"currentBranch":"","build_user":{"name":"zhulm","email":"liangming.zhu@foxmail.com"}}}
// git
{"build_time":"2021-07-04 21:26:59","build_type":"git","git":{"last_commit":{"hash":"f94253ba4af5727f1b9bd48f9b0b225723d597a4","date":"2021-07-04T21:19:44+08:00","message":"docs(read me): README.md","refs":"HEAD -> main, origin/main, origin/HEAD","body":"提交人:朱良明\n","author_name":"zhulm","author_email":"liangming.zhu@foxmail.com"},"currentBranch":"main","build_user":{"name":"zhulm","email":"liangming.zhu@foxmail.com"}}}

详情见我的github地址:github.com/dakoujia/we…

遇到的问题

1.构建的时候报错

webpack版本问题导致的,webpack5.0以上的版本会有这个问题,将版本降到4.16.5可以解决;

参考文献

揭秘webpack plugin: juejin.cn/post/684490…

官网的plugin: webpack.docschina.org/plugins/