# 如何编写一个 Plugin
这一节我们会编写一个简单的 Plugin。
loader 就是一个函数,拿到源代码做一些处理,返回新的代码;
而 plugin 我们则要编写一个类,这个类中在 webpack 的具体生命周期中做一些事情。 原理是通过事件驱动的时候
# loader 和 plugin 区别
我们先来回顾一下 loader 和 plugin 的区别。
loader 能够帮助我们处理模块,当我们引入一些 js 或者其他文件的模块的时候,需要进行一些特定处理的时候,我们可以借助 loader。
plugin 帮助我们在 webpack 打包的一些具体时刻上帮我们做一些事情,比如 html-webpack-plugin 能帮我们在打包结束的时候,帮我们生成一个 html 文件;clean-webpack-plugin 能帮助我们在打包开始之前帮助我们清除相应目录的文件
# 创建插件
webpack 插件由以下组成:
- 一个
JavaScript命名函数。 - 在插件函数的
prototype上定义一个apply方法。 - 指定一个绑定到
webpack自身的 事件钩子。 - 处理
webpack内部实例的特定数据。 - 功能完成后调用
webpack提供的回调。
class MyPlugin {
apply(compiler) {
compiler.hooks.done.tap(' MyPlugin', (stats) => {
console.log('Hello World!');
})
}
}
module.exports = MyPlugin;
# 一个简单的 plugin
我们写一个简单的 plugin,在代码生成的时候生成一个 copyright.txt 文件
# 基本格式
我们在根目录新建一个 plugins 目录,新建一个 copyright-webpack-plugin.js,我们在文件中写一个类:
class CopyrightWebpackPlugin {
constructor() {
console.log('插件使用了'); // 构造函数
}
apply(compiler) { // 调用这个插件的时候,会自动调用这个函数,compiler 是 webpack 的一个实例对象
}
}
module.exports = CopyrightWebpackPlugin;
如果我们想要接受参数的话,可以在 constructor 函数中接受通过 options 来接受使用就 ok 了。
上面是 webpack 插件的一个基本格式。
# 使用
我们在 webpack.common.js 中使用此插件:
// ...
const CopyRightWebpackPlugin = require('../plugins/copyright-webpack-plugin');
const commonConfig = {
// ...
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: 'src/index.html',
}),
new CopyRightWebpackPlugin()
],
// ...
}
// ...
我们打包一下文件 npm run dev,可以看到控制台输出 插件使用了 文字:

# 完善插件内容
我们可以在 webpack 官网找到相关的 compiler-hooks 这块文档,hooks 其实就是 webpack 的生命周期函数,代表在某些时刻就会执行的钩子函数。
我们想要在打包结束之后生成一个 copyright.txt,查文档可以发现我们需要使用 emit 这个 hooks,代表生成资源到 output 目录之前。那么我们继续完善我们的插件:
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
// 生成一个一个新的文件资源,插入到 webpack 构建中:
compilation.assets['copyright.txt']= {
source: function() {
return 'copyright by darrell' // 文件内容
},
size: function() {
return 21; // 文件的长度
}
};
cb();
})
}
}
module.exports = CopyrightWebpackPlugin;
hooks的调用通过compiler.hooks.someHook.tap(...)进行调用,不过这个取决于钩子的类型,如果是同步的钩子使用tab,异步的钩子使用tapAsync,emit这个钩子是异步的,所以我们使用tabAsync- 钩子函数接受两个参数,一个是插件名,一个是函数,函数接受两个参数,一个是
compilation代表这次打包相关的内容,cb代表回调函数。 compilation存放了这次打包的所有东西,compilation.assets存放了这次打包出来的所有文件信息,我们想要在打包完成之后在插入一个txt文件,实际上就是往assets中再加入一个copyright.txt文件,其中source属性是这个文件的内容,size是这个文件的长度。

我们打包一下,发现 dist 目录生成了一个 copyright.txt 文件,长度是 21字节:

到这里我们一个简单的在打包完成之后生成一个 copyright.txt 文件就完成了。
# 更多 hooks
# entryOption
在 entry 配置项处理过之后,执行插件,这是一个同步的钩子,我们使用的时候就直接使用 tap 就行,并且第二个参数,只要传入一个参数 compilation 就 ok 了,因为同步就不要回调方法了。
compiler.hooks.compile.tap('CopyrightWebpackPlugin', (context, entry) => {
console.log('context\n', context);
console.log('entry\n', entry);
})
参数是context,entry,代表所在目录,和入口文件:
# afterEmit
生成资源到 output 目录之后,异步钩子。
# done
编译(compilation)完成。同步钩子
compiler.hooks.done.tap('CopyrightWebpackPlugin', (stats) => {
console.log('stats\n', stats);
})
参数是 stats,代表生成的文件内容

# failed
编译(compilation)失败。同步钩子
compiler.hooks.beforeCompile.tap('CopyrightWebpackPlugin', (compilationParams) => {
throw 'too big'; // 编译(compilation)参数创建之后,执行插件,我们抛出一个错误
})
compiler.hooks.failed.tap('CopyrightWebpackPlugin', (error) => {
console.log('error\n', error);
})
参数是 error,代表错误信息:

更多的钩子参大家可以在用到的时候再去参考 webpack 官网 Compiler Hooks。
# 使用 Node 查看参数
有的时候我们想要知道 emit 钩子的 compilation 下有哪一些参数,我们需要通过 console.log 来进行打印,但是这样会显得不够直观,控制台一下子看不过来。

所以我们可以借助 Node 帮我们打断点的方式去浏览器中查看。
# 修改 package.json
我们在 scripts 下在增加一个 debug 的命令,这个命令其实和 build 的命令是一样的,但是我们能借助 node 在运行 webpack 的时候传递一些参数进去:
...
"scripts": {
...
"debug": "node --inspect --inspect-brk node_modules/webpack/bin/webpack.js --config ./config/webpack.common.js",
},
...
--inspect此参数的意思是开启webpack调试工具--inspect-brk此参数的意思是在webpack打包的第一行打一个断点。
我们使用 npm run debug 开启 node 的调试,在命令行中提示我们 http://127.0.0.1:9229/a5667edf-aea8-4b91-b2e5-2925dcea7dab 在这个地址刚我们开启了调试。
打开 chorme 浏览器,输入网址,右键打开检查窗口,我们可以看到一个 node 的按钮,点一下便进到了 webpack 源码的页面,因为第一行我们打了一个断点,所以代码会停在第一行。


如果现在我们想要查看 emit 下的 compilation,我们可以在函数里打一个 debugger:
class CopyrightWebpackPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('CopyrightWebpackPlugin', (compilation, cb) => {
debugger;
// 生成一个一个新的文件资源,插入到 webpack 构建中:
compilation.assets['copyright.txt']= {
source: function() {
return 'copyright by darrell' // 文件内容
},
size: function() {
return 21; // 文件的长度
}
};
cb();
})
}
}
我们重新运行 npm run debug,我们点击右侧的蓝色三角形,可以直接进到我们打的 debugger 这里,我们可以直接将鼠标放到 compilation 上查看其属性,也可以在右侧 watch 中新增 compilation 来查看相关属性:



如何对代码进行调试,对每一个程序员都是一项必备的技能,大家还是要掌握起来。
# 更多插件
# 一个压缩资源为 zip 包的插件
这个插件的作用是生成一个 构建的文件 的 zip 包,通过 Compilation 进行文件写入,可以将生成好的 zip 资源包设置到 compilation.assets 对象上。
# 前置知识
我们需要 jszip 这个库,他能帮我们将文件压缩为 zip 包,使用例子如下:
var zip = new JSZip();
// 添加 txt 文件
zip.file("Hello.txt", "Hello World\n");
// 添加 img 文件夹
var img = zip.folder("images");
img.file("smile.gif", imgData, {base64: true});
// 生成 zip 包
zip.generateAsync({type:"blob"}).then(function(content) {
// see FileSaver.js
saveAs(content, "example.zip");
});
/*
Results in a zip containing
Hello.txt
images/
smile.gif
*/
# 写点代码
我们这里面要用到的 hooks 是 emit,就是生成文件的阶段,我们可以读取 compilation.assets 对象的值,并将我们生成的 zip 资源包设置到这个对象上。
同时在文件写入的时候我们需要使用 webpack-sources,它是一个 Webpack 的源代码处理类。更多可参考 webpack-sources, web-service的源代码处理类
// zip-plugin.js
// 引入 jszip
const JSZip = require('jszip');
const path = require('path');
// 引入 RawSource,帮助我们写入文件
const RawSource = require('webpack-sources').RawSource;
// 实例化一个 zip
const zip = new JSZip();
module.exports = class ZipPlugin {
// 接收插件配置参数
constructor(options) {
this.options = options;
}
// 定义 apply 方法
apply(compiler) {
// 在生成的 emit hooks 上 注册相应的时间
compiler.hooks.emit.tapAsync('ZipPlugin', (compilation, callback) => {
// 根据 配置名,给 zip 命名
const folder = zip.folder(this.options.filename);
// 循环遍历 assets 上的文件,
// 并添加到 zip 中去
for (let filename in compilation.assets) {
const source = compilation.assets[filename].source();
folder.file(filename, source);
}
// 开始生成 zip
zip.generateAsync({
type: 'nodebuffer'
}).then((content) => {
// 得到 zip 包的绝对路径
const outputPath = path.join(
compilation.options.output.path,
this.options.filename + '.zip'
);
// 得到 zip 包的相对路径
const outputRelativePath = path.relative(
compilation.options.output.path,
outputPath
);
// 开始往 compilation.assets 文件上写入文件
compilation.assets[outputRelativePath] = new RawSource(content);
// 执行回调
callback();
});
});
}
}
接着在 webpack.common.js 中配置插件:
// ...
const CopyRightWebpackPlugin = require('../plugins/copyright-webpack-plugin');
const ZipPlugin = require('../plugins/zip-plugins');
const commonConfig = {
// ...
plugins: [
// ...
new CopyRightWebpackPlugin(),
new ZipPlugin({
filename: 'offline'
}),
],
// ...
}
// ...
最后我们运行 npm run bundle,可以看到在 dist 目录下生成了相应的 offline.zip 包:

解压后可以发现,offline.zip 包中就是此次打包生成的结果文件:

# 相关链接
# 示例代码
示例代码可以看这里:
← 如何编写一个 Loader 总览 →