写一个压缩png图片的vite插件
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
之前都是通过python
的subprocess
来调用的,现在换成了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
文件夹中的图片信息。
因此,此处用node
的fs
模块递归遍历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~
转载自:https://juejin.cn/post/7235485148417769532