文章目录
- 作用
- 涉及 webpack API
- 处理 asset 钩子compilation.hooks.processAssets
- 返回或新建缓存:compilation.getCache
- 返回 asset 文件信息:compilation.getAsset
- 文件名匹配函数:compiler.webpack.ModuleFilenameHelpers.matchObject
- 模版字符串替换:compilation.getPath
- 实现
- constructor
- apply
- 生成输出压缩文件
作用
- 压缩打包后的文件,可以配置是否删除源文件
const CompressionPlugin = require("compression-webpack-plugin");new CompressionPlugin()
涉及 webpack API
-
处理 asset 钩子compilation.hooks.processAssets
- PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER:优化已有 asset 的转换操作阶段,例如对 asset 进行压缩,并作为独立的 asset
- additionalAssets: true 会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时,这里为 CompressionWebpackPlugin 添加的压缩文件后触发
compiler.hooks.thisCompilation.tap(pluginName, compilation => {compilation.hooks.processAssets.tapPromise({name: pluginName,// 优化已有 asset 的转换操作,例如对 asset 进行压缩,并作为独立的 assetstage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, additionalAssets: true // true会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时}, assets => this.compress(compiler, compilation, assets));
});
const {info,source} =compilation.getAsset(name); // name:"main.js" 打包后输出文件的 name
-
文件名匹配函数:compiler.webpack.ModuleFilenameHelpers.matchObject
- 具体查看 copy-webpack-plugin 解析文章
-
模版字符串替换:compilation.getPath
- 具体查看 copy-webpack-plugin 解析文章
实现
constructor
- 初始化选项和压缩配置,以及默认使用 zlib 库进行压缩
class CompressionPlugin {constructor(options) {validate(/** @type {Schema} */schema, options || {}, {name: "Compression Plugin",baseDataPath: "options"});const {test,include,exclude,algorithm = "gzip",compressionOptions ={},filename = (options || {}).algorithm === "brotliCompress" ? "[path][base].br" : "[path][base].gz",threshold = 0,minRatio = 0.8,deleteOriginalAssets = false} = options || {};this.options = {test,include,exclude,algorithm,compressionOptions,filename,threshold,minRatio,deleteOriginalAssets};/**{test: undefined,include: undefined,exclude: undefined,algorithm: "gzip",compressionOptions: {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 defaultCompressionOptions = {gzip: {level: zlib.constants.Z_BEST_COMPRESSION // 9},deflate: {level: zlib.constants.Z_BEST_COMPRESSION},deflateRaw: {level: zlib.constants.Z_BEST_COMPRESSION},brotliCompress: {params: {[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MAX_QUALITY}}}[algorithm] || {};this.options.compressionOptions ={ // 传递给 zlib 的压缩参数...defaultCompressionOptions,...this.options.compressionOptions};}}
}
apply
- 通过 processAssets 钩子的 PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER 阶段进行 assets 压缩
apply(compiler) {const pluginName = this.constructor.name;compiler.hooks.thisCompilation.tap(pluginName, compilation => {compilation.hooks.processAssets.tapPromise({name: pluginName,// 优化已有 asset 的转换操作,例如对 asset 进行压缩,并作为独立的 assetstage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_TRANSFER, additionalAssets: true // true会多次调用回调,一次是在指定 stage 添加资产时触发回调,另一次是后来由插件添加资产时,这里为 CompressionWebpackPlugin 添加的压缩文件后触发}, assets => this.compress(compiler, compilation, assets));compilation.hooks.statsPrinter.tap(pluginName, stats => {stats.hooks.print.for("asset.info.compressed").tap("compression-webpack-plugin", (compressed, {green,formatFlag}) => compressed ?green(formatFlag("compressed")) : "");});});}
compress
- 遍历源 asset 进行压缩,会通过缓存已压缩文件来优化性能
asset 数据结构
async compress(compiler, compilation, assets) {const cache = compilation.getCache("CompressionWebpackPlugin");// 遍历文件const assetsForMinify = (await Promise.all(Object.keys(assets).map(async name => {// 获取文件信息const {info,source} =compilation.getAsset(name);})if (info.compressed) { // 当插件第一次添加压缩文件后,因为 additionalAssets:true 会第二次触发插件回调,如果第一次被压缩了 info.compressed 为 truereturn false;}// 通过开发者传递的 test、exclude、include 匹配文件if (!compiler.webpack.ModuleFilenameHelpers.matchObject.bind(undefined, this.options)(name)) {return false;}// 获取压缩相关 namelet relatedName; // "gzipped"if (typeof this.options.algorithm === "function") {if (typeof this.options.filename === "function") {relatedName = `compression-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,compressionOptions: this.options.compressionOptions}), cache.getLazyHashedEtag(source)); // 第二个参数 etag: 根据资源文件内容生成 hash// 返回缓存内容const output = (await cacheItem.getPromise()) || {};// 返回文件 bufferlet buffer; // No need original buffer for cached filesif (!output.source) {if (typeof source.buffer === "function") {buffer = source.buffer();} // Compatibility with webpack plugins which don't use `webpack-sources`// See https://github.com/webpack-contrib/compression-webpack-plugin/issues/236else {buffer = source.source();if (!Buffer.isBuffer(buffer)) {// eslint-disable-next-line no-param-reassignbuffer = 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} = compiler.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.compressed) {try {// 文件内容压缩output.compressed = await this.runCompressionAlgorithm(buffer);} catch (error) {compilation.errors.push(error);return;}}// 压缩效果相关阈值,> 开发者传入的值跳过if (output.compressed.length / buffer.length > this.options.minRatio) {await cacheItem.storePromise({compressed: output.compressed});return;}// 根据压缩后的内容生成文件output.source = new RawSource(output.compressed);await cacheItem.storePromise(output); // 存入 source、compressed}// this.options.filename:"[path][base].gz" , filename:"main.css"// newFilename:'main.css.gz'const newFilename = compilation.getPath(this.options.filename, {filename: name // name:"main.css"});const newInfo = {compressed: true};// 是否删除源文件,通过 compilation.updateAsset 更新源文件信息if (this.options.deleteOriginalAssets) {if (this.options.deleteOriginalAssets === "keep-source-map") {compilation.updateAsset(name, source, {// @ts-ignorerelated: {sourceMap: null}});}compilation.deleteAsset(name);} else {compilation.updateAsset(name, source, {related: {[relatedName]: newFilename}});}// 生成压缩文件compilation.emitAsset(newFilename, output.source, newInfo);})}
runCompressionAlgorithm
- 通过 zlib 进行压缩
const zlib = require("zlib");
this.algorithm = zlib['gzip'];runCompressionAlgorithm(input) {return new Promise((resolve, reject) => {this.algorithm(input, this.options.compressionOptions, (error, result) => {if (error) {reject(error);return;}if (!Buffer.isBuffer(result)) {resolve(Buffer.from(result));} else {resolve(result);}});});}