• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

写压缩png图片的vite插件

武飞扬头像
Canvas
帮助1

1. 碎碎念

之前项目里的图片都是在外部通过python批量调用pngquant.exe来压缩的。最近一想,其实可以写个vite插件来调用pngquant.exe,在每次打包的时候自动压缩图片就行。
不过这种比较常用的功能,github应该早有了,即vite-plugin-imagemin
但是这个插件有几个缺陷:

  • 国内安装貌似有丶麻烦
  • 还要安装很多依赖
  • 不支持对base64的压缩(vite有时候会把图片转为base64再用export导出,我项目里就有这情况)

于是我想了想不如自己写个压缩的插件。其实,看了看vite-plugin-imagemin及其相关依赖的源码,它也是基于pngquant.exe对图片进行压缩的。不过,它考虑了很多系统兼容情况,我这儿就不需要考虑这个了。

项目地址:vite-plugin-pngmin

2. 目录结构

|--plugin
    |--exe
    |   |--pngquant.exe
    |--index.js
|--public
    |--images
    |   |img3.png
    |--img4.png
|--src
    |--images
    |   |img1.png
    |   |img2.png
    |--App.vue
    |--main.js
|--vite.config.js
|--others

3. 调用pngquant.exe

之前都是通过pythonsubprocess来调用的,现在换成了node流程也差不多。
此处我用execa这个库来执行exe文件,简单来说它用promise封装了node原生的child_process,方便了很多。(因为imagemin-pngquant也用的它,不是=,=)

import { execa } from 'execa'
import { fileURLToPath } from 'url'

// 获得pngquant.exe文件的绝对路径 即:your_demo_path/plugin/exe/pngquant.exe
const ESFilename = fileURLToPath(import.meta.url)
const ESDirname = path.dirname(ESFilename)
const pngquant = path.join(ESDirname, 'exe', 'pngquant.exe')

// imgBuffer原始图片的buffer数据,在 4. 中会用到。
async function compress(imgBuffer) {
    // 通过路径执行exe文件,第二个参数['-']为指令数组
    const res = await execa(pngquant, ['-'], {
        encoding: null,
        maxBuffer: Infinity,
        input: imgBuffer,
     })
     
    // 返回压缩后的buffer数据
    return res.stdout
}

上文execa的第二个参数为pngquant指令数组,我这儿就默认了,默认的压缩效果也挺好~
此处的compress函数流程即:传入原始图片的buffer数据 >> 通过pngquant.exe对输入数据进行压缩 >> 返回压缩后的buffer数据。

4. vite&rollup的钩子函数

本节将在各个钩子函数中调用 3. 中的compress函数对图片进行压缩。

4.1. configResolved

这是vite提供的一个钩子函数,和名字一样,主要给你提供打包的配置信息。
这部分主要获取图片的静态资源目录(默认public)和打包目录(默认dist)并用于 4.4.

configResolved(config) {
    // publicDir可能为string或false
    if (typeof config.publicDir === 'string') {
        publicDir = config.publicDir
    }
    outDir = config.build.outDir
}

4.2. transform

该部分主要在transform这个钩子函数中处理将被转为base64的png图片。

// code为图片数据,id为图片路径
async transform(code, id) {
          // 获取文件后缀名
          const extname = path.extname(id)

          // 判断是否是图片转为base64的js文件格式
          const b64Reg = /^export default (\"data:image\/png;base64,[A-Za-z0-9 /=]*\")$/

          // 文件后缀为.png,但code已经是base64格式了
          if (extname === '.png' && b64Reg.test(code)) {
              // 根据图片路径读取源文件
              const imgBuffer = fs.readFileSync(id)

              // 通过 3. 的方法进行压缩
              const source = await compress(imgBuffer)

              // 将buffer数据转为base64并导出
              return `export default "data:image/png;base64,${source.toString('base64')}"`
          }
      }

4.3. generateBundle

该部分主要在generateBundle这个钩子函数中处理不会转为base64的png图片。

 // 这里的bundle即所有已经打包好的文件信息数组
 async generateBundle(_, bundle){
            // 用于存放图片路径
            let handles = []

            // bundle的key为该资源路径
            Object.keys(bundle).forEach((key) => {
                const extname = path.extname(key)
                if (extname === '.png') {
                    // 如果路径后缀为.png则放入数组中
                    handles.push(key)
                }
             })

             handles = handles.map(async (imgPath) => {
                 // 通过 3. 的方法进行压缩,bundle[imgPath].source即原始文件的buffer数据
                 const source = await compress(bundle[imgPath].source)

                 // 替换该bundle的原始数据为压缩后的buffer数据
                 bundle[imgPath].source = source
             })

             // 通过Promise.all优化性能,一个个压缩太费时了
             // 执行完Promise.all后,对应的bundle对象也就更新了
             await Promise.all(handles)
            }
        }

4.4. closeBundle

该部分主要在closeBundle这个钩子函数中处理public目录下的png图片。
由于vite不会处理public中的资源,所以提供的钩子都获取不到public文件夹中的图片信息。
因此,此处用nodefs模块递归遍历public文件夹来找到png图片并压缩。

  async closeBundle() {
            // 项目的静态资源目录(默认public),见 4.1. 
            if (typeof publicDir !== 'string') return
            
            // 递归遍历public文件夹返回png图片路径
            const getImgPath = (imgPath) => {
                const res = []
                if (fs.existsSync(imgPath)) {
                    const stat = fs.lstatSync(imgPath)
                    if (stat.isDirectory()) {
                        const files = fs.readdirSync(imgPath)
                        files.forEach((file) => {
                            const temp = getImgPath(path.join(imgPath, file))
                            res.push(...temp)
                        })
                    }
                    else if (path.extname(imgPath) === '.png') res.push(imgPath)
                }
                return res
            }
            
            // 获得public文件夹下所有的png图片路径
            const imgPaths = getImgPath(publicDir)
            
            const handles = imgPaths.map(async (imgPath) => {
                // 读取public文件夹下的图片并压缩
                const imgBuffer = fs.readFileSync(imgPath)
                const source = await compress(imgBuffer)
                
                // 构建打包路径 路径结构可见 2.
                // 比如将your_demo_path/public/images/img3.png
                // 转为images/img.png
                let targetPath = imgPath.replace(publicDir   path.sep, '')
                
                // 再将images/img3.png
                // 转为dist/images/img3.png
                // outDir(默认dist)为图片的打包目录,见 4.1.
                targetPath = path.join(outDir, targetPath)
                
                // 将原始图片替换为压缩后的图片
                fs.writeFileSync(targetPath, source)
            })

            await Promise.all(handles)
        },

5. 压缩效果测试

5.1. 配置一下vite.config.js

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// 导入插件
import pngmin from './plugin/index.js'

export default defineConfig({
    // 添加插件
    plugins: [vue(), pngmin()],
    build: {
        // 将300kb以内的png转为base64,主要用于测试能不能压缩base64
        assetsInlineLimit: 307200,
        rollupOptions: {
            output: {
                // 由于图片压缩成base64后会一同打包到index.js
                // 为了更好的观察base64的压缩情况,我这把base64图片从index.js分离了出来
                manualChunks(id) {
                    if (id.indexOf('/src/images/') !== -1) {
                        return id.split('src/')[1]
                    }
                },
            },
        },
    },
})

5.2. 查看效果

学新通 可以看到pngquant的压缩效果挺明显的。
不过需要注意的是这里没有展示public文件夹下图片的压缩效果,我本地看了看效果差不多,懒得再放图了~
此外可以看到img1.png被转为base64并通过js导出了 (原图不到300kb,打包为base64后变成了346.28kb),但是没关系,我这插件也可以识别并压缩到108.83kb~

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhbjgbbc
系列文章
更多 icon
同类精品
更多 icon
继续加载