因为公司网站每次上传图片都要手动压缩,图片多了效率就很低,所以干脆写一个脚本,配置在package.json中,这样想压缩在终端执行一下命令,就一键压缩啦😄
压缩工具
尝试用了两个压缩图片的库,一个是Tinify,一个是imagemin,感觉压缩效果没啥区别,不过Tinify 的api每个月有500张的限制额度,要去官网上获取一下key值。
实现思路
实现过程很简单:
- 递归查找指定目录下的图片文件,存入一个数组中,并创建对应输出目录(递归查找为了保持原有的目录结构,即使是多层嵌套也某闷题啦😋)
- 遍历数组,压缩图片,输出到指定目录(对于压缩失败的图片,直接复制原文件到目标目录)
- 统计压缩前后文件体积对比
核心代码
Tinify
// 调用tinify压缩图片
compressImage(oldPath,newPath){
const rs = fs.createReadStream(oldPath)
const ws = fs.createWriteStream(newPath)
return new Promise((resolve,reject)=>{
rs.on("data",(chunkData)=>{
tinify.fromBuffer(chunkData).toBuffer((error,resultData)=>{
if(error) return reject(error.message)
else{
ws.end(resultData)
ws.on("finish",()=>resolve())
ws.on("err",()=>reject(err.message))
}
})
})
})
}
imagemin
imagemin([inputPath], {
destination: outputPath,
plugins: [
imageminJpegtran({
quality: [0.6, 0.8]
}),
imageminPngquant({
quality: [0.6, 0.8]
})
]
}).then((res)=>{
let { data } = res[0]
this.outputSize += data.length
console.log(`${inputPath} 压缩完成!`)
})
.catch((err)=>{
this.outputSize += imgData.length
console.log(`${inputPath} 压缩失败,原因如下:${err}`)
// 压缩失败则直接复制图片文件到目标文件夹
fs.copyFileSync(inputPath,targetPath)
})
})
具体代码
Tinify
const fs = require("fs")
const tinify = require("tinify");
const path = require("path")
class TinifyImgApplication{
constructor(tinifyKey,inputPath,outputPath){
this.key = tinifyKey
this.inputPath = inputPath
this.outputPath = outputPath
this.initTinify()
}
initTinify(){
tinify.key = key
tinify.validate((error)=>{
if(error){
console.log("error",error)
}
if (this.remainingCompressions() <= 0) {
console.log('压缩数量已经用完');
}
// 初始化验证成功
console.log('压缩图片工具初始化成功!');
})
}
// 计算剩余压缩张数
remainingCompressions () {
return 500 - tinify.compressionCount;
}
byteSize = (byte = 0) => {
Esize = ["B","KB", "MB","GB","TB","PB","EB","ZB","YB"]
if (byte === 0) return '0 B'
const unit = 1024
const i = Math.floor(Math.log(byte) / Math.log(unit))
return (byte / Math.pow(unit, i)).toPrecision(3) + ' ' + Esize[i]
}
//递归查找指定目录下的图片文件,并创建对应输出目录
deepFindFile(inputPath,outputPath){
let imgArrayInfo = []
fs.readdirSync(inputPath).forEach(file=>{
let filePath = path.resolve(inputPath,file)
let targetPath = path.resolve(outputPath,file)
let fileStat = fs.statSync(filePath)
if(fileStat.isDirectory()){
// 创建对应输出目录
if(!fs.existsSync(targetPath)) fs.mkdirSync(targetPath)
imgArrayInfo = [...imgArrayInfo,...this.deepFindFile(filePath,targetPath)]
}else{
const fileNameReg = /\.(jpe?g|png|svg|gif)$/
if(fileNameReg.test(filePath)){
let imgData = fs.readFileSync(filePath)
imgArrayInfo.push({
inputPath:filePath, //图片文件 原路径
file:imgData, //图片文件 目标存放路径
targetPath
})
}
}
})
return imgArrayInfo
}
// 调用tinify压缩图片
compressImage(oldPath,newPath){
const rs = fs.createReadStream(oldPath)
const ws = fs.createWriteStream(newPath)
return new Promise((resolve,reject)=>{
rs.on("data",(chunkData)=>{
tinify.fromBuffer(chunkData).toBuffer((error,resultData)=>{
if(error) return reject(error.message)
else{
ws.end(resultData)
ws.on("finish",()=>resolve())
ws.on("err",()=>reject(err.message))
}
})
})
})
}
// 压缩图片,并输出到指定文件夹
processImages(){
// 创建输出根目录
if(!fs.existsSync(this.outputPath)) fs.mkdirSync(this.outputPath)
this.imageInfo = this.deepFindFile(this.inputPath,this.outputPath)
this.imageInfo.forEach(i=>{
console.log(i)
})
this.imageInfo.forEach(imgInfo=>{
const { inputPath,targetPath,imgData } = imgInfo
this.compressImage(inputPath,targetPath)
.then(()=>{
console.log(`${inputPath} 压缩完成`)
})
.catch((err)=>{
console.log(`${inputPath} 压缩失败,原因如下:${err}`)
// 压缩失败则直接复制图片文件到目标文件夹
fs.copyFileSync(inputPath,targetPath)
})
})
}
}
const key = "xxxxxxxx"
const tinifyImgApplication = new TinifyImgApplication(key,"./in1","./out1")
tinifyImgApplication.processImages()
imagemin
import fs from "fs"
import path from "path"
import imagemin from 'imagemin';
import imageminJpegtran from 'imagemin-jpegtran';
import imageminPngquant from 'imagemin-pngquant';
class MinifyImgApplication{
constructor(inputPath,outputPath){
this.inputPath = inputPath
this.outputPath = outputPath
this.inputSize = 0
this.outputSize = 0
}
//字符单位转换
byteSize = (byte = 0) => {
const Esize = ["B","KB", "MB","GB","TB","PB","EB","ZB","YB"]
if (byte === 0) return '0 B'
const unit = 1024
const i = Math.floor(Math.log(byte) / Math.log(unit))
return (byte / Math.pow(unit, i)).toPrecision(3) + ' ' + Esize[i]
}
//递归查找指定目录下的图片文件,并创建对应输出目录
deepFindFile(inputPath,outputPath){
let imgArrayInfo = []
fs.readdirSync(inputPath).forEach(file=>{
let filePath = path.resolve(inputPath,file)
let targetPath = path.resolve(outputPath,file)
let fileStat = fs.statSync(filePath)
if(fileStat.isDirectory()){
// 创建对应输出目录
if(!fs.existsSync(targetPath)) fs.mkdirSync(targetPath)
imgArrayInfo = [...imgArrayInfo,...this.deepFindFile(filePath,targetPath)]
}else{
const fileNameReg = /\.(jpe?g|png|svg|gif)$/
if(fileNameReg.test(filePath)){
let imgData = fs.readFileSync(filePath)
imgArrayInfo.push({
inputPath:filePath, //图片文件 原路径(这个包括图片文件名)
imgData,
outputPath, //图片文件 目标存放路径
targetPath //图片文件 目标存放路径(这个包括图片文件名)
})
}
}
})
return imgArrayInfo
}
// 压缩图片,并输出到指定文件夹
processImages(){
// 创建输出根目录
if(!fs.existsSync(this.outputPath)) fs.mkdirSync(this.outputPath)
this.imageInfo = this.deepFindFile(this.inputPath,this.outputPath)
const promises = this.imageInfo.map((imgInfo) =>{
const { inputPath,targetPath,imgData,outputPath } = imgInfo
this.inputSize += imgData.length
return imagemin([inputPath], {
destination: outputPath,
plugins: [
imageminJpegtran({
quality: [0.6, 0.8]
}),
imageminPngquant({
quality: [0.6, 0.8]
})
]
}).then((res)=>{
let { data } = res[0]
this.outputSize += data.length
console.log(`${inputPath} 压缩完成!`)
})
.catch((err)=>{
this.outputSize += imgData.length
console.log(`${inputPath} 压缩失败,原因如下:${err}`)
// 压缩失败则直接复制图片文件到目标文件夹
fs.copyFileSync(inputPath,targetPath)
})
})
return Promise.allSettled(promises)
}
run(){
this.processImages()
.then(()=>{
console.log(`一共处理图片文件 ${this.imageInfo.length} 个`)
console.log(`压缩前体积为${this.byteSize(this.inputSize)},压缩后体积为${this.byteSize(this.outputSize)} `)
})
.catch((err)=>{
console.log('error:',err)
})
}
}
const minifyApp = new MinifyImgApplication("./in","./in") //构造函数传入压缩图片的 源目录以及输出目录
minifyApp.run()
运行效果如下
运行的是imagemin的脚本