使用zx编写脚本对图片进行压缩

使用zx编写脚本对图片进行压缩

什么是zx

 zx 工具是Google提供的工具,一个基于 Node.js 的命令行脚本运行器,旨在让 shell 脚本的编写更加简单和强大。它通过提供一系列的实用功能以及对现代 JavaScript 语言特性的支持,使得编写和执行复杂的脚本任务变得更加轻松。
  1. zx支持简单的命令执行,通过$ 函数可以轻松地执行shell命令并获取输出
  1. 内置的包管理器支持,zx支持命令执行时安装依赖,可以在脚本中使用 npm yarn 命令
  1. zx支持与 JS 库的完整生态系统进行交互
  1. zx可以分析传递给脚本的参数,无需额外的解析库
  1. 错误机制:执行命令的时候如果有错误发生,会默认抛出异常,简化错误处理逻辑
  1. 支持顶层的await,以及其他es6+的特性
 
为什么使用zx进行图片压缩
  1. zx可以在package.json中运行脚本,并且能够分析传递给脚本的参数
  1. zx安装脚本的依赖,可以在执行脚本时进行安装,而且依赖的信息不会记录在package.json中
  1. 压缩图片其他方案:
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文件中
 

前期准备工作

  1. 在根目录中新建一个script文件夹,在script文件夹中新建一个 optimizationImg.mjs文件来执行脚本
  1. 安装项目的依赖,因为要使用zx
  1. 在项目package.json script中写入脚本执行的方法
"compressImg": "zx --install scripts/optimizationImg.mjs --inputDir=src --quality=80”
 
具体的代码:
  1. 使用zx的各种工具, https://google.github.io/zx/
  1. 使用fastq ,对任务队列进行控制 https://www.npmjs.com/package/fastq
  1. 使用sharp 对图片进行压缩 https://sharp.pixelplumbing.com/
  1. 使用CryptoJS 获取图片的hash
    1. 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]; }