什么是zx
zx
工具是Google提供的工具,一个基于 Node.js 的命令行脚本运行器,旨在让 shell 脚本的编写更加简单和强大。它通过提供一系列的实用功能以及对现代 JavaScript 语言特性的支持,使得编写和执行复杂的脚本任务变得更加轻松。- zx支持简单的命令执行,通过
$
函数可以轻松地执行shell命令并获取输出
- 内置的包管理器支持,zx支持命令执行时安装依赖,可以在脚本中使用
npm
yarn
命令
- zx支持与 JS 库的完整生态系统进行交互
- zx可以分析传递给脚本的参数,无需额外的解析库
- 错误机制:执行命令的时候如果有错误发生,会默认抛出异常,简化错误处理逻辑
- 支持顶层的await,以及其他es6+的特性
为什么使用zx进行图片压缩
- zx可以在package.json中运行脚本,并且能够分析传递给脚本的参数
- zx安装脚本的依赖,可以在执行脚本时进行安装,而且依赖的信息不会记录在package.json中
- 压缩图片其他方案:
3.1 在使用webpack/vite打包的时候进行压缩
3.2 pre commit阶段进行压缩,使用husky lint-stage获取到暂存区的图片,对这些图片进行压缩并且替换掉源文件,再将替换后的文件添加到暂存区。lint-stage执行完成后将压缩的图片提交到代码库
都未采用的原因:
3.1方案在打包阶段进行压缩的话,每次打包都会全部压缩一遍
3.2方案需要依赖于husky,如果husky无法使用那么便无法使用压缩,而且在
pre commit阶段每次图片有更新都会自动进行压缩,意味着会增加提交时间, 而且我如果想其他时间进行压缩,那么将不可能
如何实现
简略得进行逻辑梳理一下,具体看代码,:将压缩后文件的hash存入到json文件中,执行脚本的时候,获取json文件的hash,并且获取项目中所有的图片资源并获取hash,如果存在那么将不再压缩。如果不存在那么对图片进行压缩并放入到内存中并求出hash,如果出现压缩的图片比未压缩图片还大的情况,那么不进行压缩直接将hash放入到json文件中,如果未出现,那么对图片进行压缩并获取hash,放入到json文件中
前期准备工作
- 在根目录中新建一个
script
文件夹,在script文件夹中新建一个optimizationImg.mjs
文件来执行脚本
- 安装项目的依赖,因为要使用
zx
- 在项目
package.json
script
中写入脚本执行的方法
"compressImg": "zx --install scripts/optimizationImg.mjs --inputDir=src --quality=80”
具体的代码:
- 使用
zx
的各种工具, https://google.github.io/zx/
- 使用
fastq
,对任务队列进行控制 https://www.npmjs.com/package/fastq
- 使用
sharp
对图片进行压缩 https://sharp.pixelplumbing.com/
- 使用
CryptoJS
获取图片的hash
import { argv, glob, fs, sleep, echo, $ } from 'zx'; import fastq from 'fastq' import sharp from 'sharp' import CryptoJS from 'crypto-js' const cwd = process.cwd() const { inputDir, quality = 80 } = argv; const imgFiles = await glob(`${inputDir}/**/*.{jpg,jpeg,png}`, { ignore: [`${inputDir}/**/*.{nocompress.jpg,nocompress.jpeg,nocompress.png}`] }); const compressed_images_cache = await fs.readJson(`${cwd}/.compressed_images_cache.json`).catch(() => ([])); const new_compressed_images_cache = [...compressed_images_cache]; const getFileMD5 = async (filePath) => { // 读取文件的内容 file协议 打印出来为16进制进行表示的 <Buffer ff... > const data = await fs.readFile(filePath); // 加密 字数组:常用的数据结构,用来表示二进制数据。 可以用于不同格式数据之间的转换 还可以截取数据等 const wordArray = CryptoJS.lib.WordArray.create(data); // MD5 加密后的结果是一个包含 words 和 sigBytes 属性的对象。这是因为在 CryptoJS 中,哈希计算的结果通常以 WordArray 对象的形式表示 const hash = CryptoJS.MD5(wordArray); return hash.toString(); } // 通过 buf 得到md5 const getBufMD5 = async (data) => { const wordArray = CryptoJS.lib.WordArray.create(data); const hash = CryptoJS.MD5(wordArray); return hash.toString(); } const q = fastq.promise(async (task) => { const timeStart = Date.now() const { input, relativePath } = task; const fileTypes = path.extname(input) === '.png' ? 'png' : 'jpeg' let newFileBuf; const oldFileSize = (await fs.stat(input)).size; // 获取旧的md5格式 const oldFileMd5 = await getFileMD5(input); echo(`MD5 hash of ${relativePath}: ${oldFileMd5}`); if (compressed_images_cache.includes(oldFileMd5)) { echo(`skip ${relativePath}`) return } try { if (fileTypes === 'png') { newFileBuf = await sharp(input) .png({ quality }).toBuffer() } else { newFileBuf = await sharp(input) .jpeg({ quality }).toBuffer() } {/* <Buffer ff d8...> */ } } catch (error) { echo.error(error) return } if (newFileBuf.length >= oldFileSize) { new_compressed_images_cache.push(oldFileMd5) echo(`skip ${relativePath} ${Date.now() - timeStart}ms ${formatBytes(oldFileSize)} -> ${formatBytes(newFileBuf.length)}, ${oldFileMd5}`) return } console.log("input", input) await fs.writeFile(input, newFileBuf) const newFileMd5 = await getBufMD5(newFileBuf); new_compressed_images_cache.push(newFileMd5) echo(`done ${relativePath} ${Date.now() - timeStart}ms ${formatBytes(oldFileSize)} -> ${formatBytes(newFileBuf.length)}, ${newFileMd5}`) }, os.cpus().length) const allTask = imgFiles.map(input => q.push({ input: path.resolve(cwd, input), relativePath: input })) console.time('done') Promise.all(allTask).then(() => { console.timeEnd('done'); console.log("new_compressed_images_cache", new_compressed_images_cache) fs.writeJson(`${cwd}/.compressed_images_cache.json`, new_compressed_images_cache, { spaces: 2 }) }) function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; }