![59593c2f99c65e701bd7a86d6af9bfd0.png](https://i-blog.csdnimg.cn/blog_migrate/18857d9a4e2132a26dcb59ef1a112563.jpeg)
本文只会针对 Webpack 插件展开,没有 Webpack 基础的同学慎入,讨论的版本是 Webpack4。下面我将通过 4 个部分拆解 Webpack 插件。
- What,什么是 Webpack 插件?
- How,怎么编写一个 Webpack 插件
- Why,为什么要编写 Webpack 插件
- Where,Webpack 插件实战,离线包
什么是 Webpack 插件
一个标准的 Webpack4 配置应该是下面这样的。
module.exports = {
mode: "development",
entry: {},
output: {},
optimization: {},
module: {},
plugins: [],
};
我们知道在 Webpack 中,每一个资源(asset)就是一个模块,所以对资源的操作都可以交由module
完成,但是打包相关的或者热更新等,module
都无法完成,而 Webpack 插件支持在程序各个阶段执行,这主要基于 Tapable,Webpack 基于它暴露各个阶段的钩子,这些钩子可以通过 Compiler Hooks 这个链接查看,它支持同步和异步。
怎么编写一个 Webpack 插件
我们观察到 Webpack 配置中 plugins 是一个数组,数组中每一项都是 Webpack 插件的实例。Webpack 遍历执行每一个插件,然后调用插件的apply
方法,并传递compiler
对象,Webpack 插件必须实现apply
方法。
下面我们看一下最简单的 Webpack 插件
class SimpleWebpackPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
// 一般约定tab里的第一个字符串与插件名相同
compiler.hooks.done.tap("SimpleWebpackPlugin", (stat) => {
console.log(this.options.name + " compiler done");
});
}
}
在 Webpack 配置中新增一项
const SimpleWebpackPlugin = require("./SimpleWebpackPlugin");
module.exports = {
plugins: [
new SimpleWebpackPlugin({
name: "webpack",
}),
],
};
为什么要编写 Webpack 插件
有人可能会好奇,编写 Webpack 插件还需要问为什么吗?那是没见过那些不懂 Webpack 机制的,比如一个 cra(create-react-app)搭建的项目,直接一手yarn run eject
,然后就开始在项目之间魔改。
下面的例子不是本人!比如想在项目打完包之后将文件压缩生成 zip 文件,然后上传 zip 到服务器,直接找到项目中Webpack compiler的build函数,在 build 函数的回调里生成 zip 文件,这也是典型的here, it works
,又不是不能跑。这有什么缺点?不够优雅?还有其他的吗,难道不够优雅还不够吗。如果谁都在构建配置中随意添加功能,性能是一方面,维护和扩展性也很差。而通过 Webpack 插件的形式简单,我们甚至不需要执行操蛋的yarn run eject
。
Webpack 插件实战,离线包
下面我们来实战写一个离线包插件。介绍一下离线包的使用场景,一个 App 里很多页面是 h5 的,如果每次都要通过网络读取远程静态资源,会造成页面白屏时间较长,且造成流量浪费,我们可以通过一个离线的 zip 包提前将资源打包好,这样 App 每次只需要从本地下载好的离线包读取数据就行。再优化一点,对新旧资源做差分检查,每次只将代码改变的部分打成 zip 包,并更新新旧资源的映射表(需要服务端配合)。反正离线包就是为了优化资源访问。
话不多说,直接上代码
// OfflineZipWebpackPlugin.js
const fs = require("fs-extra");
const path = require("path");
const JSZip = require("jszip");
const { RawSource } = require("webpack-sources");
const zip = new JSZip();
// 必须放在htmlWebpackPlugin之后,否则html不再编译阶段
/**
* 生成zip离线包脚本,通过callback回调上传离线包
* @param filename
* @param outputPath
* @param callback
* @param excludeDir
*/
module.exports = class OfflineZipWebpackPlugin {
constructor(options) {
this.options = options;
}
excludeFiles(files) {
const { excludeDir } = this.options;
const newFiles = {};
if (Array.isArray(excludeDir)) {
excludeDir.forEach((dirName) => {
for (let path in files) {
if (!path.includes(dirName)) {
newFiles[path] = files[path];
}
}
});
return newFiles;
}
return files;
}
apply(compiler) {
const { filename, callback, outputPath } = this.options;
let newOutputPath = "";
compiler.hooks.emit.tapAsync(
"OfflineZip",
(compilation, pluginCallback) => {
const folder = zip.folder(filename);
for (let filename in compilation.assets) {
const source = compilation.assets[filename].source();
folder.file(filename, source);
}
zip.files = this.excludeFiles(zip.files);
zip
.generateAsync({
type: "nodebuffer",
})
.then((content) => {
if (!fs.existsSync(outputPath)) {
fs.mkdirSync(outputPath);
} else {
fs.emptyDirSync(outputPath);
}
newOutputPath = path.join(outputPath, filename + ".zip");
const relativePath = path.relative(
compilation.options.output.path,
newOutputPath
);
// 需要相对路径
compilation.assets[relativePath] = new RawSource(content);
console.log("newOutputPath", newOutputPath);
pluginCallback();
});
}
);
compiler.hooks.done.tap("OfflineZip", () => {
if (callback) callback(newOutputPath);
});
}
};
webpack配置
// webpack.config.js
module.exports = {
plugins: [
new OfflineZipWebpackPlugin({
filename: paths.zipFileName,
outputPath: paths.zipDir,
excludeDir: ["/static/media"],
callback: (zipPath) => {
uploadFile(zipPath);
},
}),
],
};
解释一下OfflineZipWebpackPlugin
构造函数里传递的参数
- filename zip文件名
- outputPath zip包文件输出到哪个目录
- excludeDir 可忽略某一路径下的文件
- callback 打包成功之后的回调
说一下OfflineZipWebpackPlugin
类的apply方法里都干了哪些事。
- 通过
compiler.hooks.emit.tapAsync
监听文件即将要构建(emit)的钩子。 - 将一个个资源对象都加到
jszip
的实例里。folder.file(filename, source);
- 再用
jszip
的generateAsync
方法将内容(content)以nodebuffer
的方式输出 - 用
webpack-sources
的RawSource
封装对象,再赋值到compilation.assets
上 - 调用
pluginCallback
,通知Webpack,钩子回调已经支持完毕。 - 最后监听
compiler.hooks.done.tap
,当编译完成时,触发回调,将zip包上传到远程服务器。
这里有一个小矛盾,callback函数到底要不要放到OfflineZipWebpackPlugin
里,这个看个人喜好吧。如果说你崇尚高内聚松耦合,那放在一起更容易维护。如果你是Kiss(Keep it simple and stupid)原则的拥簇者,那可以新写一个AfterOfflineZipWebpackPlugin
,单独处理离线包的上传。
最后的最后
其实编写一个Webpack插件很简单,但是要理解Webpack插件的机制就需要理解Tapable机制,它利用了发布订阅模式,或者说观察者模式,很好的解藕了分布者和订阅者之间的联系,他们不用互相关心,交给中间方就行。
文章中用到了几个模式的术语,如果想了解设计模式,可以看这篇文章。
成楠Peter:设计模式之美-前端zhuanlan.zhihu.com![ac8c2a934dda6c4760dfdf66aab88cb4.png](https://i-blog.csdnimg.cn/blog_migrate/50c2cf6d0de63f8bb7dd0f2aa28ad016.jpeg)
Webpack的学习推荐极客专栏,玩转Webpack。它不仅非常全面的介绍了Webpack相关的知识,还介绍了工程管理相关的内容,相信如果你认真看完这个专栏,你一定会成为一个Webpack配置工程师加工程管理工程师。
![0165081abce44b28dd34d5846740f8d1.png](https://i-blog.csdnimg.cn/blog_migrate/952bd66c069cda7c5e8cee9de48b0d53.jpeg)