作用
- 压缩打包后的文件,可以配置是否删除源文件
javascript">const ***pressionPlugin = require("***pression-webpack-plugin");
new ***pressionPlugin()
涉及 webpack API
-
处理 asset 钩子***pilation.hooks.processAssets
- PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER:优化已有 asset 的转换操作阶段,例如对 asset 进行压缩,并作为独立的 asset
- additionalAssets: true 会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时,这里为 ***pressionWebpackPlugin 添加的压缩文件后触发
***piler.hooks.this***pilation.tap(pluginName, ***pilation => {
***pilation.hooks.processAssets.tapPromise({
name: pluginName,
// 优化已有 asset 的转换操作,例如对 asset 进行压缩,并作为独立的 asset
stage: ***piler.webpack.***pilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER,
additionalAssets: true // true会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时
}, assets =>
this.***press(***piler, ***pilation, assets));
});
-
返回或新建缓存:***pilation.getCache
- 具体查看 copy-webpack-plugin 解析文章
-
返回 asset 文件信息:***pilation.getAsset
const {
info,
source
} =***pilation.getAsset(name); // name:"main.js" 打包后输出文件的 name
-
文件名匹配函数:***piler.webpack.ModuleFilenameHelpers.matchObject
- 具体查看 copy-webpack-plugin 解析文章
-
模版字符串替换:***pilation.getPath
- 具体查看 copy-webpack-plugin 解析文章
实现
constructor
- 初始化选项和压缩配置,以及默认使用 zlib 库进行压缩
class ***pressionPlugin {
constructor(options) {
validate(
/** @type {Schema} */
schema, options || {}, {
name: "***pression Plugin",
baseDataPath: "options"
});
const {
test,
include,
exclude,
algorithm = "gzip",
***pressionOptions ={},
filename = (options || {}).algorithm === "brotli***press" ? "[path][base].br" : "[path][base].gz",
threshold = 0,
minRatio = 0.8,
deleteOriginalAssets = false
} = options || {};
this.options = {
test,
include,
exclude,
algorithm,
***pressionOptions,
filename,
threshold,
minRatio,
deleteOriginalAssets
};
/**
{
test: undefined,
include: undefined,
exclude: undefined,
algorithm: "gzip",
***pressionOptions: {
level: 9,
},
filename: "[path][base].gz",
threshold: 0,
minRatio: 0.8,
deleteOriginalAssets: false,
}
*/
this.algorithm = this.options.algorithm;
if (typeof this.algorithm === "string") {
const zlib = require("zlib"); // 默认使用 zlib 压缩
this.algorithm = zlib[this.algorithm];
if (!this.algorithm) {
throw new Error(`Algorithm "${this.options.algorithm}" is not found in "zlib"`);
}
const default***pressionOptions = {
gzip: {
level: zlib.constants.Z_BEST_***PRESSION // 9
},
deflate: {
level: zlib.constants.Z_BEST_***PRESSION
},
deflateRaw: {
level: zlib.constants.Z_BEST_***PRESSION
},
brotli***press: {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY
}
}
}[algorithm] || {};
this.options.***pressionOptions ={ // 传递给 zlib 的压缩参数
...default***pressionOptions,
...this.options.***pressionOptions
};
}
}
}
apply
- 通过 processAssets 钩子的 PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER 阶段进行 assets 压缩
apply(***piler) {
const pluginName = this.constructor.name;
***piler.hooks.this***pilation.tap(pluginName, ***pilation => {
***pilation.hooks.processAssets.tapPromise({
name: pluginName,
// 优化已有 asset 的转换操作,例如对 asset 进行压缩,并作为独立的 asset
stage: ***piler.webpack.***pilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER,
additionalAssets: true // true会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时,这里为 ***pressionWebpackPlugin 添加的压缩文件后触发
}, assets =>
this.***press(***piler, ***pilation, assets));
***pilation.hooks.statsPrinter.tap(pluginName, stats => {
stats.hooks.print.for("asset.info.***pressed").tap("***pression-webpack-plugin", (***pressed, {
green,
formatFlag
}) => ***pressed ?
green(formatFlag("***pressed")) : "");
});
});
}
***press
- 遍历源 asset 进行压缩,会通过缓存已压缩文件来优化性能
asset 数据结构
async ***press(***piler, ***pilation, assets) {
const cache = ***pilation.getCache("***pressionWebpackPlugin");
// 遍历文件
const assetsForMinify = (await Promise.all(Object.keys(assets).map(async name => {
// 获取文件信息
const {
info,
source
} =***pilation.getAsset(name);
})
if (info.***pressed) { // 当插件第一次添加压缩文件后,因为 additionalAssets:true 会第二次触发插件回调,如果第一次被压缩了 info.***pressed 为 true
return false;
}
// 通过开发者传递的 test、exclude、include 匹配文件
if (!***piler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {
return false;
}
// 获取压缩相关 name
let relatedName; // "gzipped"
if (typeof this.options.algorithm === "function") {
if (typeof this.options.filename === "function") {
relatedName = `***pression-function-${crypto.createHash("md5").update(serialize(this.options.filename)).digest("hex")}`;
} else {
/**
* @type {string}
*/
let filenameForRelatedName = this.options.filename;
const index = filenameForRelatedName.indexOf("?");
if (index >= 0) {
filenameForRelatedName = filenameForRelatedName.slice(0, index);
}
relatedName = `${path.extname(filenameForRelatedName).slice(1)}ed`;
}
} else if (this.options.algorithm === "gzip") {
relatedName = "gzipped";
} else {
relatedName = `${this.options.algorithm}ed`;
}
if (info.related && info.related[relatedName]) {
return false;
}
// 缓存文件相关
const cacheItem = cache.getItemCache(serialize({ // 第一个参数key:序列化成字符串,通过 serialize-javascript 库序列化成字符串
name,
algorithm: this.options.algorithm,
***pressionOptions: this.options.***pressionOptions
}), cache.getLazyHashedEtag(source)); // 第二个参数 etag: 根据资源文件内容生成 hash
// 返回缓存内容
const output = (await cacheItem.getPromise()) || {};
// 返回文件 buffer
let buffer; // No need original buffer for cached files
if (!output.source) {
if (typeof source.buffer === "function") {
buffer = source.buffer();
} // ***patibility with webpack plugins which don't use `webpack-sources`
// See https://github.***/webpack-contrib/***pression-webpack-plugin/issues/236
else {
buffer = source.source();
if (!Buffer.isBuffer(buffer)) {
// eslint-disable-next-line no-param-reassign
buffer = Buffer.from(buffer);
}
}
if (buffer.length < this.options.threshold) { // 小于开发者传入的要压缩的阈值退出
return false;
}
}
return {
name,
source,
info,
buffer,
output,
cacheItem,
relatedName
};
}))).filter(assetForMinify => Boolean(assetForMinify));
// webpack 格式文件,用于生成输出文件
const {
RawSource
} = ***piler.webpack.sources;
const scheduledTasks = [];
// 压缩操作
for (const asset of assetsForMinify) {
scheduledTasks.push((async () => {
// ...
})
}
await Promise.all(scheduledTasks);
}
生成输出压缩文件
// 压缩操作
for (const asset of assetsForMinify) {
scheduledTasks.push((async () => {
const {
name,
source,
buffer,
output,
cacheItem,
info,
relatedName
} = asset;
// 优先将压缩相关内容存入缓存
if (!output.source) {
if (!output.***pressed) {
try {
// 文件内容压缩
output.***pressed = await this.run***pressionAlgorithm(buffer);
} catch (error) {
***pilation.errors.push(error);
return;
}
}
// 压缩效果相关阈值,> 开发者传入的值跳过
if (output.***pressed.length / buffer.length > this.options.minRatio) {
await cacheItem.storePromise({
***pressed: output.***pressed
});
return;
}
// 根据压缩后的内容生成文件
output.source = new RawSource(output.***pressed);
await cacheItem.storePromise(output); // 存入 source、***pressed
}
// this.options.filename:"[path][base].gz" , filename:"main.css"
// newFilename:'main.css.gz'
const newFilename = ***pilation.getPath(this.options.filename, {
filename: name // name:"main.css"
});
const newInfo = {
***pressed: true
};
// 是否删除源文件,通过 ***pilation.updateAsset 更新源文件信息
if (this.options.deleteOriginalAssets) {
if (this.options.deleteOriginalAssets === "keep-source-map") {
***pilation.updateAsset(name, source, {
// @ts-ignore
related: {
sourceMap: null
}
});
}
***pilation.deleteAsset(name);
} else {
***pilation.updateAsset(name, source, {
related: {
[relatedName]: newFilename
}
});
}
// 生成压缩文件
***pilation.emitAsset(newFilename, output.source, newInfo);
})
}
run***pressionAlgorithm
- 通过 zlib 进行压缩
const zlib = require("zlib");
this.algorithm = zlib['gzip'];
run***pressionAlgorithm(input) {
return new Promise((resolve, reject) => {
this.algorithm(input, this.options.***pressionOptions, (error, result) => {
if (error) {
reject(error);
return;
}
if (!Buffer.isBuffer(result)) {
resolve(Buffer.from(result));
} else {
resolve(result);
}
});
});
}