持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第1天,点击查看活动详情
Hello,我是Xc,一位因antfu结缘开源的前端菜鸟,今天和大家分享最近开源的一个高效的图片压缩工具fast-imagemin-cli。
什么是图片压缩
顾名思义就是压缩图片大小,有效的图片压缩会优化网页的加载,我们常见的图片压缩处理主要有以下2种:
- 借助tinypng等图片压缩网站或者工具提前压缩好再放入项目中
- 借助脚手架插件在编译时进行压缩输出
为什么做这个工具
源于实际的需求吧,部分类型项目存在较多的图片资源,基于上面2种方案先说下在生产中存在的一些问题吧。
- 借助压缩工具:由于项目是多人开发,可能存在有时候忘记压缩的情况,导致一个较大的图片资源直接给编译到生产中了,影响了网页的加载。
- 借助脚手架插件:因为图片资源较多,每次编译时在图片压缩上都消耗大量时间去做重复的事情。
如何做这个工具
就在思考要如何解决这类问题时候,正好在 antfu 的 开源探店直播项目收集 中看到了 速冻鱼 大佬开源的 easy-tinypng-cli 插件,它可以自动帮忙压缩替换图片资源。
然后又结合了目前项目在用的vite-plugin-imagemin,整理了下这两个不满足我的需求的地方:
- 使用
vite-plugin-imagemin
时,每次构建都必须重新压缩,我认为这是不必要的。 - 使用
easy-tinypng-cli
时,需要联网,有500个限制,不能满足大量处理。
好奇心让我去看了这两个工具的源码,发现easy-tinypng-cli
其中设计的recode.json来作为一个记录表,防止图片进行二次压缩,导致浪费每个月的次数。
vite-plugin-imagemin
是一个基于imagemin
系列插件开发一个组合压缩图片插件。
瞬间有个想法戛然而生,我把他们结合一下是不是就行了?
- 利用
vite-plugin-imagemin
中的压缩功能解决easy-tinypng-cli
的联网和数量限制问题。 - 利用
easy-tinypng-cli
中的recode设计来解决vite-plugin-imagemin
每次的重复压缩工作问题。
开发
工具类型定位
是打算将其做成cli工具还是一个编译插件,这里我选择将其作为类似easy-tinypng-cli
的工具,做成插件开发成本较大,要需要处理多种脚手架的问题,虽然说可以用unplugin来开发兼容多种脚手加插件,但是还是觉得cli工具更方便,所以决定将其作为一个cli工具进行开发。
工具整体流程设计
graph TD
A[执行命令] --> B(读取配置)
B --> |未读取到配置文件| C(结束)
B --> |读取到配置文件| D(获取配置目录下的所有图片路径)
D --> E(加载imagemin插件)
E --> F(遍历文件路径)
F --> G(根据文件内容获取hash)
G --> R(比对记录表)
R --> |hash存在| K(跳过该路径)
R --> |hash不存在| L(压缩并替换源文件并写入记录)
L --> 结束
实现配置加载
参考了easy-tinypng-cli
中使用的unconfig进行配置文件的加载,可以加载执行命令目录下文件名与files配置匹配的文件,支持多种拓展名,拓展名匹配优先级按照extensions
import { loadConfig } from 'unconfig'
const { config } = await loadConfig<FmConfig>({
sources: [
{
files: 'fm.config',
extensions: ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json', ''],
},
],
})
遍历图片文件路径
首先需要一个遍历方法
export function walkSync(currentDirPath: string, callback: (filePath: string, dirent: fs.Dirent) => void) {
fs.readdirSync(currentDirPath, { withFileTypes: true }).forEach((dirent: fs.Dirent) => {
const filePath = path.join(currentDirPath, dirent.name)
if (dirent.isFile())
callback(filePath, dirent)
else if (dirent.isDirectory())
walkSync(filePath, callback)
})
}
根据配置文件中获取到的目录配置,进行遍历
const { include, options } = config
const includeArr = Array.isArray(include) ? include : [include]
const filePaths: string[] = []
includeArr.forEach((targetDir) => {
const currentDirPath = path.resolve(process.cwd(), targetDir)
walkSync(currentDirPath, (filePath: string, dirent: fs.Dirent) => {
if (isImageExp(dirent.name)) { // 判断是否是图片文件
const hash = getHashOfFile(filePath) // 获取文件hash
if (force || !oldTinyMap.has(hash)) // 判断是否已经压缩过
filePaths.push(filePath)
}
})
})
压缩方法
const plugins = getImageminPlugins(options) // 复用vite-plugin-imagemin中的方法
if (plugins.length === 0) {
debug(chalk.redBright('Please configure at least one compression configuration parameter.'))
return
}
const tasks = filePaths.map(filePath => Promise.resolve(processFile(plugins!, filePath, fs.readFileSync(filePath))))
const spinner = ora({ text: chalk.bold.yellow('compressing...'), color: 'yellow' }).start()
// 利用promise.all去执行压缩操作任务
await Promise.all(tasks).then((results) => {
spinner.stop()
const total = results.length
const success = results.filter(result => result).length
const fail = total - success
debug(`compress results: total: ${chalk.bold.blue(total)} success: ${chalk.bold.green(success)} fail: ${chalk.bold.red(fail)}`)
// eslint-disable-next-line no-console
console.table(Array.from(tinyMap.values()), ['relativePath', 'oldSize', 'size', 'ratio', 'time(ms)'])
Array.from(tinyMap.entries()).forEach(([key, value]) => {
oldTinyMap.set(key, value)
})
// 写入日志
fs.writeFileSync(RecordFilePath, JSON.stringify(Object.fromEntries(oldTinyMap.entries()), null, '\t'))
})
主体功能就开发完成了,有兴趣的可以去fast-imagemin-cli查看源码
应用
安装
# pnpm
pnpm install fast-imagemin-cli -D
# npm
npm install fast-imagemin-cli -D
# yarn
yarn add fast-imagemin-cli -D
imagemin 安装问题
感谢大佬指出这个问题,这个问题我在vite-plugin-imagemin的README中有看到相应的解决方案,如下
由于 imagemin 在国内不好安装。现提供几个解决方案
- 使用 yarn 在 package.json 内配置(推荐)
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
2. 使用 npm,在电脑 host 文件加上如下配置即可
199.232.4.133 raw.githubusercontent.com
Config
fm.config.ts
or fm.config.json
- inclde(required): 需要压缩的图片目录会自动递归遍历.
- options: imagemin 配置项
// fm.config.ts
import { defineFmConfig } from 'fast-imagemin-cli/support'
export default defineFmConfig({
include: ['./src/assets'], // string | string[]
options: {
gifsicle: {
optimizationLevel: 7,
interlaced: false,
},
optipng: {
optimizationLevel: 7,
},
mozjpeg: {
quality: 20,
},
pngquant: {
quality: [0.8, 0.9],
speed: 4,
},
svgo: {
plugins: [
{
name: 'removeViewBox',
},
{
name: 'removeEmptyAttrs',
active: false,
},
],
},
},
})
Options
params | type | default | default |
---|---|---|---|
svgo | object or false | - | See Options |
gifsicle | object or false | - | See Options |
mozjpeg | object or false | - | See Options |
optipng | object or false | - | See Options |
pngquant | object or false | - | See Options |
webp | object or false | - | See Options |
添加 Script
// package.json
{
"scripts": {
"fm": "fm" // or "fm --force" 强制压缩,跳过记录
// 如果担心会忘记压缩的情况,可以直接把命令配置到编译命令中
"build": "fm && vue-tsc --noEmit && vite build"
}
}
Demo
first
你会发现其中一个 .png
文件压缩耗时 76930
ms, 如果每次都花这么长时间,会大大影响工作效率。
second
第二次就只需要5
ms,节省了77000
ms。
最后
感谢easy-tinypng-cli
和vite-plugin-imagemin
提供的灵感
项目地址
使用中有任何问题可以提issue哦~~
如果这个插件能够帮助到你或者让你觉得有用可以给个star和关注~~