Skip to main content

深入理解 plugins 插件

loader 被用于转换某些类型的模块,而插件则可以用来处理各种各样的任务,例如打包优化和压缩、重新定义环境中的变量等。

插件是 webpack 生态系统的重要组成部分,为社区用户提供了一种强大方式来直接触及 webpack 的编译过程(compilation process)。

插件能够钩入(hook)到每个编译(compilation)中触发的所有关键事件。在编译的每个阶段中,插件都拥有对 compiler 对象的完全访问能力,并且在合适的时机,还可以访问当前的 compilation 对象。

一、Tapable

tapable 这个小型库是 webpack 的一个核心工具,它为 webpack 插件接口提供了核心能力。

webpack 中许多对象扩展自 Tapable 类(例如 compiler 对象)。这个类暴露 taptapAsynctapPromise 等方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。

二、常见的插件

三、插件的使用

这里以 HtmlWebpackPlugin 为例,HtmlWebpackPlugin 用于生成一个 HTML 文件,把所有生产的 JS 文件都引入到该文件中,最终生成到 output 目录。

  • 安装
npm install --save-dev html-webpack-plugin
  • 配置
webpack.config.js
var HtmlWebpackPlugin = require('html-webpack-plugin');
var path = require('path');

var webpackConfig = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index_bundle.js'
},
plugins: [new HtmlWebpackPlugin()]
};

输出结果:

dist/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>webpack App</title>
</head>
<body>
<script src="index_bundle.js"></script>
</body>
</html>

如果需要多个页面的配置,需要实例化多个 HtmlWebpackPlugin 对象,也可以对其进行参数配置:

const htmlWebPackConfig = {
title: 'Hello Webpack', // 配置模板 title
template: '', // 模板来源 html 文件
filename: 'index.html', // 生成的模板文件名
favicon: '', // 指定页面的图标
hash: true, // 是否生成 hash 添加在引入文件地址的末尾 默认为 true
inject: '', // 引入模板的注入位置 取值有(true/false/body/head)
minify: { // 对生成的 html 文件进行压缩,默认是 false
collapseWhitespace: true, // 是否去除空格
removeAttributeQuotes: true, // 去掉属性引用
caseSensitive: false, // 是否大小写敏感
removeComments: true // 去掉注释
},
cache: true, // 表示内容变化的时候生成一个新的文件,默认 true
showErrors: true, // 是否将错误信息写在页面,默认 true
chunks: ['index'] // 引入模块,即 entry 中设置的多个 js,这里是执行 js, 否则引入全部
}

点击查看更多配置项

四、loader 与 plugin 的区别

  • loader 是一个转换器,用于将 A 文件编译成 B 文件,对文件进行操作,在打包文件之前运行。例如将 A.scss 转换为 B.css

  • plugin 是一个扩展器,用于增强 webpack 的功能,它不直接操作文件,而是基于事件机制工作,监听 webpack 打包过程中的某些节点,执行相应的任务,在整个编译周期都起作用。例如打包优化、资源管理、环境变量注入等,其目的是解决 loader 无法实现的其他事。

五、如何实现一个插件

由于 webpack 基于发布订阅模式,在运行的生命周期中会广播出许多事件,插件通过监听这些事件,就可以在特定的阶段执行自己的插件任务。

1、插件的组成

webpack 插件由以下组成:

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

2、创建插件

2-1、老版的写法 webpack <v5

插件是由「具有 apply 方法的 prototype 对象」所实例化出来的。这个 apply 方法在安装插件时,会被 webpack compiler 调用一次。apply 方法可以接收一个 webpack compiler 对象的引用,从而可以在回调函数中访问到 compiler 对象。一个简单的插件结构如下:

// 一个 JavaScript 命名函数
function HelloWorldPlugin() {}

// 在插件函数的 prototype 上定义一个 apply 方法
HelloWorldPlugin.prototype.apply = function (compiler) {
// 指定一个挂载到 webpack 自身的事件钩子
compiler.plugin(
'done',
function (compilation /* 当前打包构建流程的上下文 */, callback) {
console.log('Hello World!')

// 功能完成后可以调用 webpack 提供的回调
callback()
}
)
}

module.exports = HelloWorldPlugin

上面代码中,

  • compiler 扩展自 Tapable 类,包含了 webpack 环境所有的配置信息,在启动 webpack 时被实例化,可理解为 webpack 实例,用来注册和调用插件。

  • compilation 也扩展自 Tapable 类,作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件等,当 webpack 以开发模式运行时,每检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。

Compiler 和 Compilation 的区别:

Compiler 代表了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只是代表了一次新的编译。

2-2、新写法 webpack >=v5

class HelloWorldPlugin {
// webpack 会调用 HelloWorldPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler) {
// 指定一个挂载到 webpack 自身的事件钩子,实现插件功能
compiler.hooks.done.tap('HelloWorldPlugin', (compilation, callback) => {
// compilation 为当前打包构建流程的上下文
console.log('Hello World!')
// 功能完成后可以调用 webpack 提供的回调
callback()
})
}
}

module.exports = HelloWorldPlugin

新的 webpack 需要使用 compiler.hooks 的写法,点击查看 compiler 钩子。

3、使用插件

webpack.config.js
var HelloWorldPlugin = require('hello-world')

var webpackConfig = {
// ... 这里是其他配置 ...
plugins: [
new HelloWorldPlugin({options: true})
]
}

4、插件实现示例

这里写一个名为 EndWebpackPlugin 的插件,作用是在 Webpack 即将退出时再附加一些额外的操作,例如 Webpack 成功编译和输出了文件后执行发布操作把输出的文件上传到服务器。同时该插件还能区分 Webpack 构建是否执行成功。

  • 创建插件:

老版的写法:

src/plugin1.js
function Plugin1(options) {}

// 在插件函数的 prototype 上定义一个 apply 方法
Plugin1.prototype.apply = function (compiler) {
// 所有文件资源经过不同的 loader 处理后触发这个事件
compiler.plugin('emit', function (compilation, callback) {
// 获取打包后的 JS 文件名
const filename = compiler.options.output.filename
// 生成一个 index.html 并引入打包后的 JS 文件
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="${filename}"></script>
</head>
<body>

</body>
</html>`
// 所有处理后的资源都放在 compilation.assets 中
// 添加一个 index.html 文件
compilation.assets['index.html'] = {
source: function () {
return html
},
size: function () {
return html.length
}
}

// 功能完成后调用 webpack 提供的回调
callback()
})
}

module.exports = Plugin1

新版的写法:

src/plugin1.js
class Plugin1 {
apply(compiler) {
compiler.hooks.emit.tap('Plugin1', (compilation) => {
// 获取打包后的 JS 文件名
const filename = compiler.options.output.filename
// 生成一个 index.html 并引入打包后的 JS 文件
const html = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="${filename}"></script>
</head>
<body>

</body>
</html>`
// 所有处理后的资源都放在 compilation.assets 中
// 添加一个 index.html 文件
compilation.assets['index.html'] = {
source: function () {
return html
},
size: function () {
return html.length
}
}

// 功能完成后调用 webpack 提供的回调
// callback()
})
}
}

module.exports = Plugin1
  • 使用插件:
webpack.config.js
const Plugin1 = require('./src/plugin1')

module.exports = {
// ...
plugins: [
new Plugin1(),
// 也可以这么写 ↓
// 在初始化 EndWebpackPlugin 时传入了两个参数,分别是在成功时的回调函数和失败时的回调函数
new Plugin1(
() => {
// Webpack 构建成功,并且文件输出了后会执行到这里,在这里可以做发布文件操作
},
(err) => {
// Webpack 构建失败,err 是导致错误的原因
console.error(err)
}
)
]
}
  • 运行结果:

目录文件:

|- /dist
|- bundle.js
|- index.html

生成一个 index.html 并引入打包后的 JS 文件:

dist/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="bundle.js"></script>
</head>
<body>

</body>
</html>