webpack plugin源码解析(六) CompressionWebpackPlugin

news/2025/2/22 2:02:27/

文章目录

  • 作用
  • 涉及 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));
});
  • 返回或新建缓存:compilation.getCache

    • 具体查看 copy-webpack-plugin 解析文章
  • 返回 asset 文件信息:compilation.getAsset

   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);}});});}

http://www.ppmy.cn/news/48541.html

相关文章

RabbitMQ的介绍和安装

文章目录 1.1 RabbitMQ介绍1.2 RabbitMQ的安装1.2.1 下载镜像1.2.2 安装启动1.2.3 测试 1.1 RabbitMQ介绍 RabbitMQ是一个开源的消息队列中间件&#xff0c;可以用于构建分布式应用程序&#xff0c;使应用程序能够快速、可靠地处理大量消息。它实现了AMQP&#xff08;高级消息队…

MyBatis小技巧

一、MyBatis中接口代理机制及使用 我们不难发现&#xff0c;以前编写dao/mapper实现类中的方法代码很固定&#xff0c;基本上就是一行代码&#xff0c;通过SqlSession对象调用insert、delete、update、select等方法&#xff0c;这个类中的方法没有任何业务逻辑&#xff0c;既然…

八、express框架解析

文章目录 前言一、express 路由简介1、定义2、基础使用 二、express 获取参数1、获取请求报文参数2、获取路由参数 三、express 响应设置1、一般响应设置2、其他响应设置 四、express 防盗链五、express 路由模块化1、模块中代码如下&#xff1a;2、主文件中代码如下&#xff1…

常见的Web漏洞

一、SQL 注入 SQL 注入就是通过把 SQL 命令插入到 Web 表单&#xff0c;递交或输入域名或页面请求的查询字符串&#xff0c;最终达到欺骗服务器执行恶意的 SQL 命令的目的。 1、原理&#xff1a; 网站数据过滤不合格&#xff0c;过分依赖用户输入的数据&#xff0c;没有过滤用…

SpringBoot配置多环境,dev,prod,test

springboot与maven配置多环境 开发任何的项目至少都需要三个环境&#xff0c;分别是 开发环境 测试环境 生产环境 环境不同意味着配置也是不相同的&#xff0c;比如数据库&#xff0c;文件路径等等配置&#xff0c;那么如何解决这么多环境整合在一起既不混乱又能很优雅呢&…

Maven项目中出现【不再支持目标选项 1.5】的解决办法

1 快速解决【单项目】 本方法只适用于单个项目&#xff0c;新建项目使用maven还会出现问题。 在pom.xml配置&#xff1a; <properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target>&l…

grpc实战-pb文件生成问题/空消息体问题

报错信息&#xff1a; proto: message pb.Empty is already registered See https://protobuf.dev/reference/go/faq#namespace-conflict 对比老版本的工具生成的xxxx.pb.go文件。import导入的proto链接不一样&#xff1a; 旧版本&#xff1a;import github.com/golang/proto…

C++篇 ---- 命名空间namespace

由于在c语言中在定义时可能会出现重命名现象&#xff0c;造成空间冲突&#xff0c;c语言中有命名冲突&#xff1a;1 和库冲突。2 互相之间的冲突&#xff0c;变量命名冲突。所以c中就有了对其改进的关键字namespace&#xff0c;针对重定义&#xff0c;解决空间冲突。 文章目录 …