作用
- 压缩html文件
const HtmlMinimizerPlugin = require("html-minimizer-webpack-plugin");
new HtmlMinimizerPlugin()
涉及 webpack API
-
compilation.getCache:获取缓存
- 具体查看 copy-webpack-plugin 解析文章
-
compiler.webpack.ModuleFilenameHelpers.matchObject:匹配文件方法
- 具体查看 copy-webpack-plugin 解析文章
实现 html-minimizer-webpack-plugin
constructor
constructor(options) {
validate(
schema, options || {}, {
name: "Html Minimizer Plugin",
baseDataPath: "options"
});
const {
minify = htmlMinifierTerser, // 默认使用 html-minifier-terser 库进行压缩
minimizerOptions, // 压缩参数
parallel = true,
test = /\.html(\?.*)?$/i, // 匹配 html 文件
include,
exclude
} = options || {};
let minimizer;
if (Array.isArray(minify)) {
minimizer = minify.map((item, i) => {
return {
implementation: item,
options: Array.isArray(minimizerOptions) ? minimizerOptions[i] : minimizerOptions
};
});
} else {
minimizer =
{
implementation: minify,
options: minimizerOptions
};
}
this.options = {
test,
parallel,
include,
exclude,
minimizer
};
}
apply
apply(compiler) {
const pluginName = this.constructor.name;
// 获取 cpu 多核数量,用于创建 worker
const availableNumberOfCores = HtmlMinimizerPlugin.getAvailableNumberOfCores(this.options.parallel);
compiler.hooks.compilation.tap(pluginName, compilation => {
compilation.hooks.processAssets.tapPromise({
name: pluginName,
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_SIZE, // 优化现有 asset 大小,例如进行压缩或者删除空格阶段
additionalAssets: true
}, assets =>
this.optimize(compiler, compilation, assets, {
availableNumberOfCores
}));
compilation.hooks.statsPrinter.tap(pluginName, stats => {
stats.hooks.print.for("asset.info.minimized").tap("html-minimizer-webpack-plugin", (minimized, {
green,
formatFlag
}) => minimized ?
/** @type {Function} */
green(
/** @type {Function} */
formatFlag("minimized")) : "");
});
});
}
getAvailableNumberOfCores
static getAvailableNumberOfCores(parallel) {
// In some cases cpus() returns undefined
// https://github.com/nodejs/node/issues/19022
const cpus = os.cpus() || {
length: 1
};
return parallel === true ? cpus.length - 1 : Math.min(Number(parallel) || 0, cpus.length - 1);
}
optimize 压缩过程
获取 html 文件资源
async optimize(compiler, compilation, assets, optimizeOptions) {
const cache = compilation.getCache("HtmlMinimizerWebpackPlugin");
let numberOfAssets = 0;
// 筛选 html 文件,获取源文件信息以及设置对应的缓存
const assetsForMinify = await Promise.all(Object.keys(assets).filter(name => {
const {
info
} =
compilation.getAsset(name); // Skip double minimize assets from child compilation
if (info.minimized) { // 通过插件压缩后会设置 minimized 参数,避免重复压缩
return false;
}
// webpack 提供的匹配文件名的方法
if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind( // eslint-disable-next-line no-undefined
undefined, this.options)(name)) {
return false;
}
return true;
}).map(async name => {
const {
info,
source // 这里的 source 是源文件字符串,非 buffer
} =
compilation.getAsset(name);
// 根据文件内容生成 hash
const eTag = cache.getLazyHashedEtag(source);
// 返回|新建缓存
const cacheItem = cache.getItemCache(name, eTag);
const output = await cacheItem.getPromise();
if (!output) {
numberOfAssets += 1; // 获取需要压缩的 html 资源数量
}
return {
name,
info,
inputSource: source,
output,
cacheItem
};
}));
if (assetsForMinify.length === 0) { // 不需要压缩退出
return;
}
// 通过 Worker 进行压缩
// ...
}
创建 Worker
- 通过 jest-worker 创建 Worker,通过 Worker 线程执行压缩任务,避免在主线程中进行耗时的计算操作
async optimize(compiler, compilation, assets, optimizeOptions) {
// 筛选 html 资源等操作
let getWorker;
let initializedWorker;
let numberOfWorkers;
if (optimizeOptions.availableNumberOfCores > 0) { // 获取到的系统 cup 数量 - 1
// 如果 html 文件数量小于系统 cup 数量,那么根据 html 文件数量创建 worker 数量
numberOfWorkers = Math.min(numberOfAssets, optimizeOptions.availableNumberOfCores);
getWorker = () => { // 创建 worker 任务
if (initializedWorker) { // 避免重复创建
return initializedWorker;
}
// 通过 worker 线程执行对应的方法
initializedWorker = new Worker(require.resolve("./minify"), {
numWorkers: numberOfWorkers,
enableWorkerThreads: true
}); // https://github.com/facebook/jest/issues/8872#issuecomment-524822081
// 在开发过程中更加方便地调试和排查问题
const workerStdout = initializedWorker.getStdout();
if (workerStdout) {
workerStdout.on("data", chunk => process.stdout.write(chunk)); // 监测主线程输出
}
const workerStderr = initializedWorker.getStderr();
if (workerStderr) {
workerStderr.on("data", chunk => process.stderr.write(chunk)); // 监测主线程错误输出
}
return initializedWorker;
};
}
// 执行压缩任务
// ...
}
主线程派发压缩任务
async optimize(compiler, compilation, assets, optimizeOptions) {
// ...
const {
RawSource //创建 webpack 格式文件
} = compiler.webpack.sources;
const scheduledTasks = [];
// 筛选出的每个 html 文件都是一个任务
for (const asset of assetsForMinify) {
scheduledTasks.push(async () => {
const {
name,
inputSource,
cacheItem
} = asset;
let {
output
} = asset;
let input;
const sourceFromInputSource = inputSource.source(); // 源文件字符串内容,非 buffer
if (!output) {
input = sourceFromInputSource;
if (Buffer.isBuffer(input)) { // 将 buffer 内容转换成字符串
input = input.toString();
}
const options = {
name,
input,
minimizer: this.options.minimizer
};
try {
// serialize 通过 serialize-javascript 库将对象序列化成字符串,因为是跨线程传递参数
// 通过 jest-wokrer 库在创建的worker线程中执行 minify.js 文件导出的 transform 方法
output = await (getWorker ? getWorker().transform(serialize(options)) : minifyInternal(options));
} catch (error) {
compilation.errors.push(
/** @type {WebpackError} */
HtmlMinimizerPlugin.buildError(error, name));
return;
}
// 获取压缩结果
output.source = new RawSource(output.code);
// 将结果进行缓存
await cacheItem.storePromise({
source: output.source,
errors: output.errors,
warnings: output.warnings
});
}
// 表示文件已压缩过了,后续跳过
const newInfo = {
minimized: true
};
if (output.warnings && output.warnings.length > 0) {
for (const warning of output.warnings) {
compilation.warnings.push(HtmlMinimizerPlugin.buildWarning(warning, name));
}
}
if (output.errors && output.errors.length > 0) {
for (const error of output.errors) {
compilation.errors.push(HtmlMinimizerPlugin.buildError(error, name));
}
}
// 更新源文件内容为压缩后内容、添加文件信息info
compilation.updateAsset(name, output.source, newInfo);
});
}
// 设置一次执行的任务数量
const limit = getWorker && numberOfAssets > 0 ? numberOfWorkers : scheduledTasks.length;
await throttleAll(limit, scheduledTasks); // 一次执行的任务数量,具体实现查看 copy-webpack-pulgin
if (initializedWorker) {
await initializedWorker.end(); // 压缩完成后关闭 worker 线程
}
}
Worker 线程执行压缩任务
getWorker().transform
async optimize(compiler, compilation, assets, optimizeOptions) {
// ...
output = await (getWorker ? getWorker().transform(serialize(options)) : minifyInternal(options));
// ...
}
async function transform(options) { // option 被 serialize-javascript 序列化成了字符串
// 'use strict' => this === undefined (Clean Scope)
// Safer for possible security issues, albeit not critical at all here
// eslint-disable-next-line no-new-func, no-param-reassign
// 通过 new Function 将传递的字符串对象,反序列化还原成对象
const evaluatedOptions = new Function("exports", "require", "module", "__filename", "__dirname", `'use strict'\nreturn ${options}`)(exports, require, module, __filename, __dirname);
return minify(evaluatedOptions);
}
module.exports = {
minify,
transform
};
minify
const minify = async options => {
const result = {
code: options.input,
warnings: [],
errors: []
};
const transformers = Array.isArray(options.minimizer) ? options.minimizer : [options.minimizer];
for (let i = 0; i <= transformers.length - 1; i++) {
const {
implementation
} = transformers[i];
// 通过 html-minifier-terser 压缩
const minifyResult = await implementation({
[options.name]: result.code
}, transformers[i].options);
// 是否压缩成功,压缩成功返回对象
if (Object.prototype.toString.call(minifyResult) === "[object Object]" && minifyResult !== null && "code" in minifyResult) {
result.code = minifyResult.code;
result.warnings = result.warnings.concat(minifyResult.warnings || []);
result.errors = result.errors.concat(minifyResult.errors || []);
} else {
// @ts-ignore
result.code = minifyResult;
}
}
return result;
};
json-minimizer-webpack-plugin
- 代码和 html-minimizer-webpack-plugin 逻辑一致,区别在于压缩方式
// ...
output = await minify(options);
// ...
通过 JSON.stringify 的第三个参数进行压缩
const result = JSON.stringify(JSON.parse(input), replacer, space); // 第三个参数 space 控制缩进和插入,当为 undefiend 时,没有缩进,实现压缩