• 作者:老汪软件技巧
  • 发表时间: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配置),如果各位读者的项目有扩展需求,可以将其抽离成配置供外界传入,这样在使用的时候更加灵活。