webpack-plugin
插件作用: 扩展 webpack
文档:https://webpack.docschina.org/api/plugins
示例代码:https://github.com/mouday/webpack-loader-plugins
钩子
钩子的本质就是:事件
Tapable
Tapable 为 webpack 提供了统一的插件接口(钩子)类型定义
统一暴露了三个方法给插件,用于注入不同类型的自定义构建行为:
- tap:可以注册同步钩子和异步钩子。
- tapAsync:回调方式注册异步钩子。
- tapPromise:Promise 方式注册异步钩子。
compiler
compiler 对象中保存着完整的 Webpack 环境配置,每次启动 webpack 构建时它都是一个独一无二,仅仅会创建一次的对象。
主要属性:
- compiler.options 可以访问本次启动 webpack 时候所有的配置文件,包括但不限于 loaders 、 entry 、 output 、 plugin 等等完整配置信息。
- compiler.inputFileSystem 和 compiler.outputFileSystem 可以进行文件操作,相当于 Nodejs 中 fs。
- compiler.hooks 可以注册 tapable 的不同种类 Hook,从而可以在 compiler 生命周期中植入不同的逻辑。
Compilation
compilation 对象代表一次资源的构建,compilation 实例能够访问所有的模块和它们的依赖。
主要属性:
- compilation.modules 可以访问所有模块,打包的每一个文件都是一个模块。
- compilation.chunks chunk 即是多个 modules 组成而来的一个代码块。入口文件引入的资源组成一个 chunk,通过代码分割的模块又是另外的 chunk。
- compilation.assets 可以访问本次打包生成所有文件的结果。
- compilation.hooks 可以注册 tapable 的不同种类 Hook,用于在 compilation 编译模块阶段进行逻辑添加以及修改。
生命周期简图
第一个插件
执行流程:
- webpack 加载 webpack.config.js 中的所有配置,此时就会
new TestPlugin()
,执行插件的constructor
- webpack 创建 compiler 对象
- 遍历所有 plugins 中的插件,调用插件的 apply 方法
- 执行剩下的编译流程,触发各个 hooks 时间
js
// plugins/test-plugin.js
class TestPlugin {
constructor() {
console.log("TestPlugin constructor");
}
apply(compiler) {
console.log("TestPlugin apply");
}
}
module.exports = TestPlugin;
使用插件
js
// webpack.config.js
const path = require("path");
const TestPlugin = require("./plugins/test-plugin.js");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true,
},
module: {},
plugins: [new TestPlugin()],
mode: "development",
};
compiler 钩子: https://webpack.docschina.org/api/compiler-hooks/
注册钩子
js
// plugins/test-plugin.js
class TestPlugin {
constructor() {
console.log("TestPlugin constructor");
}
apply(compiler) {
console.log("TestPlugin apply");
// 从文档可知, environment hook 是 SyncHook,
// 也就是同步钩子, 只能用tap注册
compiler.hooks.environment.tap("TestPlugin", (compilationParams) => {
console.log("compiler.environment()");
});
// 从文档可知, emit 是 AsyncSeriesHook,
// 也就是异步串行钩子,特点就是异步任务顺序执行
compiler.hooks.emit.tap("TestPlugin", (compilation) => {
console.log("compiler.emit() 111");
});
compiler.hooks.emit.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.emit() 222");
callback();
}, 2000);
});
compiler.hooks.emit.tapPromise("TestPlugin", (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log("compiler.emit() 333");
resolve();
}, 1000);
});
});
// 从文档可知, make 是 AsyncParallelHook,
// 也就是异步并行钩子, 特点就是异步任务同时执行
// 可以使用 tap、tapAsync、tapPromise 注册。
// 如果使用tap注册的话,进行异步操作是不会等待异步操作执行完成的。
compiler.hooks.make.tap("TestPlugin", (compilation) => {
setTimeout(() => {
console.log("compiler.make() 111");
}, 2000);
});
// 使用tapAsync、tapPromise注册,进行异步操作会等异步操作做完再继续往下执行
compiler.hooks.make.tapAsync("TestPlugin", (compilation, callback) => {
setTimeout(() => {
console.log("compiler.make() 222");
// 必须调用
callback();
}, 1000);
});
compiler.hooks.make.tapPromise("TestPlugin", (compilation) => {
// 必须返回promise
return new Promise((resolve) => {
setTimeout(() => {
console.log("compiler.make() 333");
resolve();
}, 1000);
});
});
}
}
module.exports = TestPlugin;
Node.js 调试 debugger
package.json
json
{
"scripts": {
"debug": "node --inspect-brk ./node_modules/webpack-cli/bin/cli.js"
}
}
参数说明
--inspect-brk 启动调试,首行打断点
运行指令
bash
npm run debug
打开 Chrome 浏览器任意页面,打开调试面板,找到 Node 的绿色图标
可以在代码任意位置打断点
js
debugger;
BannerWebpackPlugin
给打包输出文件添加注释
js
// plugins/banner-webpack-plugin.js
class BannerWebpackPlugin {
constructor(options = {}) {
this.options = options;
}
apply(compiler) {
// 需要处理文件
const extensions = ["js", "css"];
// 前缀注释
const prefix = `/*
* Author: ${this.options.author}
*/\n`;
// emit是异步串行钩子
compiler.hooks.emit.tapAsync(
"BannerWebpackPlugin",
(compilation, callback) => {
// compilation.assets包含所有即将输出的资源
// 通过过滤只保留需要处理的文件
const assetPaths = Object.keys(compilation.assets).filter(
(assetPath) => {
const splitted = assetPath.split(".");
return extensions.includes(splitted[splitted.length - 1]);
}
);
// 遍历需要处理的资源,添加注释
assetPaths.forEach((assetPath) => {
// 获取文件内容
const source = compilation.assets[assetPath].source();
// 添加注释
const content = prefix + source;
// 覆盖资源
compilation.assets[assetPath] = {
// 资源内容
source() {
return content;
},
// 资源大小
size() {
return content.length;
},
};
});
callback();
}
);
}
}
module.exports = BannerWebpackPlugin;
使用
js
// webpack.config.js
const path = require("path");
const BannerWebpackPlugin = require("./plugins/banner-webpack-plugin.js");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true,
},
module: {},
plugins: [
new BannerWebpackPlugin({
author: "老王",
}),
],
// mode: "development",
mode: "production",
};
CleanWebpackPlugin
在 webpack 打包输出前将上次打包内容清空。
js
// plugins/clean-webpack-plugin.js
class CleanWebpackPlugin {
apply(compiler) {
// 获取操作文件的对象
const fs = compiler.outputFileSystem;
// emit是异步串行钩子
compiler.hooks.emit.tapAsync(
"CleanWebpackPlugin",
(compilation, callback) => {
// 获取输出文件目录
const outputPath = compiler.options.output.path;
// 删除目录所有文件
const err = this.removeFiles(fs, outputPath);
// 执行成功err为undefined,执行失败err就是错误原因
callback(err);
}
);
}
removeFiles(fs, path) {
try {
// 读取当前目录下所有文件
const files = fs.readdirSync(path);
// 遍历文件,删除
files.forEach((file) => {
// 获取文件完整路径
const filePath = `${path}/${file}`;
// 分析文件
const fileStat = fs.statSync(filePath);
// 判断是否是文件夹
if (fileStat.isDirectory()) {
// 是文件夹需要递归遍历删除下面所有文件
this.removeFiles(fs, filePath);
} else {
// 不是文件夹就是文件,直接删除
fs.unlinkSync(filePath);
}
});
// 最后删除当前目录
fs.rmdirSync(path);
} catch (e) {
// 将产生的错误返回出去
return e;
}
}
}
module.exports = CleanWebpackPlugin;
使用
js
// webpack.config.js
const path = require("path");
const CleanWebpackPlugin = require("./plugins/clean-webpack-plugin.js");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true,
},
module: {},
plugins: [new CleanWebpackPlugin()],
// mode: "development",
mode: "production",
};
AnalyzeWebpackPlugin
分析 webpack 打包资源大小,并输出分析文件。
js
// plugins/analyze-webpack-plugin.js
class AnalyzeWebpackPlugin {
apply(compiler) {
// emit是异步串行钩子
compiler.hooks.emit.tap("AnalyzeWebpackPlugin", (compilation) => {
// Object.entries将对象变成二维数组。二维数组中第一项值是key,第二项值是value
let list = ["# 分析打包资源大小", "", "| 名称 | 大小 |", "| --- | --- |"];
for (let [filename, file] of Object.entries(compilation.assets)) {
list.push(`| ${filename} | ${Math.ceil(file.size() / 1024)}KB |`);
}
let source = list.join("\n");
// 生成一个md文件
compilation.assets["analyze.md"] = {
source() {
return source;
},
size() {
return source.length;
},
};
});
}
}
module.exports = AnalyzeWebpackPlugin;
使用
js
// webpack.config.js
const path = require("path");
const AnalyzeWebpackPlugin = require("./plugins/analyze-webpack-plugin.js");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
clean: true,
},
module: {},
plugins: [new AnalyzeWebpackPlugin()],
// mode: "development",
mode: "production",
};
InlineChunkWebpackPlugin
webpack 打包生成的 runtime 文件太小了,额外发送请求性能不好,所以需要将其内联到 js 中,从而减少请求数量。
html-webpack-plugin 执行流程
pnpm i safe-require -D
js
// plugins/inline-chunk-webpack-plugin.js
const HtmlWebpackPlugin = require("safe-require")("html-webpack-plugin");
class InlineChunkWebpackPlugin {
constructor(tests) {
this.tests = tests;
}
apply(compiler) {
compiler.hooks.compilation.tap(
"InlineChunkWebpackPlugin",
(compilation) => {
// 获取html-webpack-plugin插件实例
const hooks = HtmlWebpackPlugin.getHooks(compilation);
// 注册一个钩子,在html-webpack-plugin插件生成html文件时调用
hooks.alterAssetTagGroups.tap("InlineChunkWebpackPlugin", (assets) => {
assets.headTags = this.getInlineTag(
assets.headTags,
compilation.assets
);
assets.bodyTags = this.getInlineTag(
assets.bodyTags,
compilation.assets
);
});
// 删除runtime文件
hooks.afterEmit.tap("InlineChunkHtmlPlugin", () => {
Object.keys(compilation.assets).forEach((assetName) => {
if (this.tests.some((test) => assetName.match(test))) {
delete compilation.assets[assetName];
}
});
});
}
);
}
// 将html-webpack-plugin生成的html文件中的link和script标签提取出来
getInlineTag(tags, assets) {
/**
*
* 输入
* [
{
tagName: 'script',
voidTag: false,
meta: { plugin: 'html-webpack-plugin' },
attributes: { defer: true, type: undefined, src: 'runtime-main.js' }
}
]
输出:
[
{
tagName: 'script',
innerHTML: 'runtime的文件内容',
closeTag: true,
}
]
*/
return tags.map((tag) => {
if (tag.tagName !== "script") return tag;
// 文件路径
const scriptName = tag.attributes.src;
if (!this.tests.some((test) => scriptName.match(test))) return tag;
return {
tagName: "script",
innerHTML: assets[scriptName].source(),
closeTag: true,
};
});
}
}
module.exports = InlineChunkWebpackPlugin;
使用
js
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const InlineChunkWebpackPlugin = require("./plugins/inline-chunk-webpack-plugin.js");
module.exports = {
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js",
clean: true,
},
module: {
},
optimization: {
splitChunks: {
chunks: "all",
},
runtimeChunk: {
name: (entrypoint) => `runtime-${entrypoint.name}`,
},
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
new InlineChunkWebpackPlugin([/runtime(.*)\.js$/g]),
],
// mode: "development",
mode: "production",
};
https://www.bilibili.com/video/BV14T4y1z7sw?p=84&spm_id_from=pageDriver