- 作者:老汪软件技巧
- 发表时间:2024-09-26 10:01
- 浏览量:
前言
我们团队的业务比较重视界面的效果,因此在很多时候都会采用贴图的方式,为了保证页面的性能,我们比较关心图片资源的大小,图片资源如果过大,影响页面的加载速度不说,如果遇到某个页面访问量特别大的话,流量也会很大,对公司也是一定程度上的资损。
在前任架构师设计的方案是在本地进行构建,然后再将构建的图片产物上传至阿里云OSS,然后将html文件推送至Git仓库,通过CI流水线托管至最终的服务器静态文件目录进行部署。
但是这种方案是是不太友好的,因为他使用的是imagemin插件进行的图像压缩,这个插件在开发机上因为被网络墙了,导致这个包安装不下来,所以想要在CI环境下自动化这个操作很困难。
后来,我偶然间看了一下vite的插件库,看到了一个叫做unplugin-imagemin的插件。看了一下作者的实现,采用的是Sharp和Squoosh进行图片压缩,但是这个库我在我的CommonJS规范的Node运行时下面跑不起来,降了几个版本,仍然无法运行,这个仓库上我看也有用户在提issue,跟我遇到的是同一个问题。
unplugin/unplugin-imagemin: Compression Image compression plugin based on squoosh and sharp ()
通过阅读源码,发现这个库的实现其实也不是特别复杂,于是就决定自己做一个,以实现在CI环境下可以自动压缩图片资源的能力。
理论基础与插件设计思路
首先,这个插件是跑在CI环境下的,插件只需要在构建阶段生效。
压缩资源是一个一次性的操作,即产物不应该影响仓库里面本身托管的内容,如果说将压缩后的内容托管回仓库的话,下次再运行自动化任务的时候就可能对资源进行重复压缩,就可能会出现过渡压缩的问题,但是也会带来的是每次都要重复压缩,构建时间会稍微长一些,不过这个没有办法避免。
对于开发业务的同学来说也得有一个平衡的点,假设我们的自动压缩插件可能对某个图片压缩的不太理想的话,如出现了锯齿,无法通过视觉走查,这种场景就只能放弃压缩或者自行压缩,因此,我们得考虑到针对某些图片不压缩的情况。
另外,图片的压缩只要走在上传到阿里云之前其实就可以了,之前我在做那个阿里云上传插件的时候,使用的生命周期是closeBundle,所以我们能选择的生命周期就比较多了,为了简便起见,我就使用buildStart进行操作吧,并且把这个插件放到比较前面的位置执行。
在这个文档中有Rollup的所有生命周期的解释:Plugin Development | Rollup ()
最后,如果图片本身比较小的话,其实压不压也没多大的关系,所以我们应该考虑到一个最小的压缩阈值。
编码实现
设置插件的选项,apply:build,使得这个插件仅仅在构建时才执行,enforce: 'pre',把这个插件放在前面执行。
首先是
// 虽然我用的是ESM的语法,但是实际上我的产物是CommonJS的规范
import Sharp from 'sharp'
function assetsCompression(params: { cwd: string }): PluginOption {
// 我的构建路径的cwd是一个业务路径,所以需要由外界传递路径的上下文
const { cwd } = params
return {
name: 'vite-plugin-assets-compression',
apply: 'build',
enforce: 'pre',
async buildStart() {
logger.success(
'=========================================准备开始进行资源压缩========================================='
)
return new Promise(async (resolve) => {
const matchPattern = cwd + '/**/*.{jpe?g,png,gif,webp}'
// 使用glob匹配到项目中所有的图片文件
const matchPass = glob.sync(matchPattern)
for (const imgFile of matchPass) {
const imgFileRelativePath = imgFile.replace(cwd + '/', '')
const originalFileInfo = statSync(imgFile)
const originSize = Math.floor(originalFileInfo.size / 1024)
// 小于10KB的图就不压缩了
if (originSize <= 10) {
continue
}
// 跳过已经压缩过的图片,我们项目中是以文件路径中出现minify这个单词作为依据的
if (/minify\//.test(imgFile)) {
logger.success(imgFileRelativePath + '已压缩,跳过压缩')
continue
}
// 压缩过程中如果出现错误,不要打断后续的构建流程
try {
const extName = extname(imgFile)
const outputFile = imgFile.replace(extName, '') + '__compressed' + extName
await Sharp(imgFile)
.png({
quality: 60,
compressionLevel: 9,
progressive: true,
})
.webp({
quality: 70,
effort: 6,
})
.jpeg({
mozjpeg: true,
quality: 60,
progressive: true,
})
.gif({
effort: 10,
progressive: true,
})
.toFile(outputFile)
const compressedFileInfo = statSync(outputFile)
if (originalFileInfo.size > compressedFileInfo.size) {
logger.info(
'图片' +
imgFileRelativePath +
'压缩成功,原始文件大小:' +
originSize +
'KB,压缩文件大小:' +
Math.floor(compressedFileInfo.size / 1024) +
'KB'
)
// 删除原文件
unlinkSync(imgFile)
// 将压缩之后的文件重命名为新文件
renameSync(outputFile, outputFile.replace('__compressed', ''))
} else {
logger.success('图片' + imgFileRelativePath + '已压缩,无需重复压缩')
// 文件压缩之后较大,取消压缩之后的文件
unlinkSync(outputFile)
}
} catch (exp) {
logger.error('压缩出错,跳过压缩图片:' + imgFileRelativePath)
}
}
logger.success('=========================================资源压缩完成=========================================')
resolve()
})
},
}
}
总结
插件运行效果:
总的来说,其实这个插件没有什么难度,就是在vite和Rollup的构建流程中集成了一个Sharp工具对资源进行压缩。
所以插件的本质就是编写一些桥接代码,把一些第三方库集成在自己的构建流程中,不同的构建工具有不同的规范,但是都是大同小异,只需要按照其API规范接入即可。
对于我的文章中所提到的内容,那些参数我都是写死的(因为我们的项目都是使用我开发的cli进行的自动流水线任务,哈哈哈,所以业务同学并不需要关心vite配置),如果各位读者的项目有扩展需求,可以将其抽离成配置供外界传入,这样在使用的时候更加灵活。