通过 Tinypng 接口批量压缩图片

简介

  • 前端项目中会有一些静态的图片资源。
  • 在发布到生成环境之前,一般会进行图片压缩。生成环境使用压缩后的图片
  • 常用的图片压缩方法是上传到Tinypng.com站点上面压缩

需求

  • 编写方法,实现通过输入命令可以压缩某个目录及其子目录下面的图片文件
  • 压缩使用Tinypng.com站点提供的接口实现

思路

  • 通过Node.jsfs模块读取目录及其子目录下面的所有文件
  • 对文件进行过滤。保留符合要求的图片文件
  • 对符合要求的图片文件挨个调用压缩接口
  • 接口请求使用axios实现
  • 下载压缩后的文件,覆盖源文件
  • 生成压缩成功或者失败的日志

备注:

  • 尽量一个图片一个图片地调用接口。一起调用的话很容易出现超过最大同时上传数量的限制

实现

配置目录、可压缩文件

  • 图片所在目录src/assets/img/。这里先在文件中定义,后期会读取命令的参数
  • 扩展名为.jpg.png的图片文件
  • 文件大小不能超过5MB
const root = './src/assets/img/' const exts = ['.jpg', '.png'] const max = 5 * 1024 * 1024,

读取所有可压缩文件

  • 读取出所有的可压缩文件路径和名称,放在list数组中
  • getfileList()方法:通过fs.readdir()方法读取目录下的所有文件和目录
  • 每个文件或目录,循环过滤
  • fileFilter()方法:通过fs.stat()方法读取文件或目录的属性
  • 如果是目录,读取该子目录下的所有文件和目录
  • 如果是文件并满足条件。将文件pushlist数组中
  • 都执行完后,就得到了所有可压缩文件的数组
const fs = require('fs') const path = require('path') const list = [] // 获取文件列表 function getfileList(folder) { return new Promise((resolve, reject) => { fs.readdir(folder, async (err, files) => { if (err) console.log(err) for (let i = 0; i < files.length; i++) { await fileFilter(folder + files[i]) } resolve() }) }) } // 过滤文件格式,返回所有jpg、png图片 function fileFilter(file) { return new Promise((resolve, reject) => { fs.stat(file, async (err, stats) => { if (err) console.error(err) if (stats.isDirectory()) { await getfileList(file + '/') } else if ( // 必须是文件,小于5MB,后缀 jpg||png stats.isFile() && stats.size <= max && exts.includes(path.extname(file)) ) { list.push(file) } resolve() }) }) }

图片压缩

  • 接口地址:https://tinypng.com/web/shrink

  • 请求方式:POST

  • 我们这里先用postman测试下

image.png

image.png

  • 这里我们加上一个User-AgentMozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36

  • 返回的代码格式如下:

{ "input": { "size": 200608, "type": "image/png" }, "output": { "size": 42613, "type": "image/png", "width": 512, "height": 512, "ratio": 0.2124, "url": "https://tinypng.com/web/output/hcnx6uqzgwkd9fp74qe4r8nux2bqdm95" } }
  • 如果发生错误,返回的代码格式如下:
{ "error": "Bad request", "message": "Request is invalid" }
const axios = require('axios') const httpAdapter = require('axios/lib/adapters/http') function fileUpload(img) { return new Promise((resolve, reject) => { axios({ method: 'POST', url: 'https://tinypng.com/web/shrink', adapter: httpAdapter, headers: { 'Postman-Token': Date.now(), 'Cache-Control': 'no-cache', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' }, data: fs.readFileSync(img) // responseType: 'application/json' }).then(async res => { const data = res.data console.log(data) resolve() }).catch(err => { resolve() }) }) }

下载压缩后的图片

  • 通过axios请求压缩后的图片地址
  • 返回的应该是二进制数据
  • 通过fs.writeFile()方法覆盖源文件
function fileUpdate(imgpath, obj) { return new Promise((resolve, reject) => { axios({ // adapter: httpAdapter, method: 'GET', url: obj.output.url, responseType: 'arraybuffer' }).then(res => { fs.writeFile(imgpath, res.data, 'binary', err => { if (err) console.error(err) resolve() }) }).catch(err => { resolve() }) }) }

生成成功和错误日志

// 若文件不存在,创建一个 function fileCreate(filename) { return new Promise((resolve, reject) => { fs.exists(filename, function(exists) { if(!exists) { fs.writeFile(filename, '', function(err) { if(err) console.error('file create failed') resolve() }) } else { resolve() } }) }) } // 写成功日志 function setSuccessLog(url, obj) { console.log(url, '压缩成功!') fileCreate(succLog).then(() => { let data = '' data += `[${url}] 压缩成功!\n` data += `原始大小:${obj.input.size}\n` data += `压缩大小:${obj.output.size}\n` data += `压缩后图片:${obj.output.url}\n` data += `优化比例:${obj.output.ratio * 100}%\n` data += '\r\n' fs.appendFile(succLog, data, err => { if (err) console.error(`[${url}]:成功日志写入失败`) }) }) } // 写失败日志 function setErrorLog(url, errMsg) { console.log(url, '压缩失败!') fileCreate(errLog).then(() => { let data = '' data += `[${url}] 压缩失败!\n` data += `报错:${errMsg}\n` data += '\r\n' fs.appendFile(errLog, data, err => { if (err) console.error(`[${url}]:错误日志写入失败`) }) }) }

unhandledRejection处理

  • 使用了Promise,如果没有捕获错误处理的话,Node.js会报如下错误

UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch()

  • 正常应该每个Promise都需要有.catch()来处理error

  • 这里简单点,使用一个stackoverflow

process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason) // application specific logging, throwing an error, or other logic here })

配置命令

  • 修改package.json,在scripts中添加:"compress": "cross-env root=./src/assets/img/ node ./compress.js"

  • compress.js文件中通过如下方法接收root

const root = process.env.root

总结实现

  • 在根目录(哪个目录都可以)下新建文件compress.js
const fs = require('fs') const path = require('path') const axios = require('axios') const httpAdapter = require('axios/lib/adapters/http') const root = process.env.root const exts = ['.jpg', '.png'] const max = 5 * 1024 * 1024 const list = [] // 成功和错误日志 const errLog = root + 'compress-error.log' const succLog = root + 'compress-success.log' process.on('unhandledRejection', (reason, p) => { console.log('Unhandled Rejection at: Promise', p, 'reason:', reason) // application specific logging, throwing an error, or other logic here }) getfileList(root).then(async res => { for (let i = 0; i < list.length; i++) { await fileUpload(list[i]) } }) // 获取文件列表 function getfileList(folder) { return new Promise((resolve, reject) => { fs.readdir(folder, async (err, files) => { if (err) console.log(err) for (let i = 0; i < files.length; i++) { await fileFilter(folder + files[i]) } resolve() }) }) } // 过滤文件格式,返回所有jpg、png图片 function fileFilter(file) { return new Promise((resolve, reject) => { fs.stat(file, async (err, stats) => { if (err) console.error(err) if (stats.isDirectory()) { await getfileList(file + '/') } else if ( // 必须是文件,小于5MB,后缀 jpg||png stats.isFile() && stats.size <= max && exts.includes(path.extname(file)) ) { list.push(file) } resolve() }) }) } // 异步API,压缩图片 function fileUpload(img) { return new Promise((resolve, reject) => { axios({ method: 'POST', url: 'https://tinypng.com/web/shrink', adapter: httpAdapter, headers: { 'Postman-Token': Date.now(), 'Cache-Control': 'no-cache', 'Content-Type': 'application/x-www-form-urlencoded', 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36' }, data: fs.readFileSync(img) // responseType: 'application/json' }).then(async res => { const data = res.data if (data.error) { setErrorLog(img, data.message) } else { await fileUpdate(img, data) } resolve() }).catch(err => { const res = err.response if (res.data) { setErrorLog(img, res.data.message) } else { setErrorLog(img, err) } resolve() }) }) } // 该方法被循环调用,请求图片数据 function fileUpdate(imgpath, obj) { return new Promise((resolve, reject) => { axios({ // adapter: httpAdapter, method: 'GET', url: obj.output.url, responseType: 'arraybuffer' }).then(res => { fs.writeFile(imgpath, res.data, 'binary', err => { if (err) console.error(err) setSuccessLog(imgpath, obj) resolve() }) }).catch(err => { setErrorLog(imgpath, err) resolve() }) }) } // 若文件不存在,创建一个 function fileCreate(filename) { return new Promise((resolve, reject) => { fs.exists(filename, function(exists) { if(!exists) { fs.writeFile(filename, '', function(err) { if(err) console.error('file create failed') resolve() }) } else { resolve() } }) }) } // 写成功日志 function setSuccessLog(url, obj) { console.log(url, '压缩成功!') fileCreate(succLog).then(() => { let data = '' data += `[${url}] 压缩成功!\n` data += `原始大小:${obj.input.size}\n` data += `压缩大小:${obj.output.size}\n` data += `压缩后图片:${obj.output.url}\n` data += `优化比例:${obj.output.ratio * 100}%\n` data += '\r\n' fs.appendFile(succLog, data, err => { if (err) console.error(`[${url}]:成功日志写入失败`) }) }) } // 写失败日志 function setErrorLog(url, errMsg) { console.log(url, '压缩失败!') fileCreate(errLog).then(() => { let data = '' data += `[${url}] 压缩失败!\n` data += `报错:${errMsg}\n` data += '\r\n' fs.appendFile(errLog, data, err => { if (err) console.error(`[${url}]:错误日志写入失败`) }) }) }
  • 修改package.json,在scripts中添加:"compress": "cross-env root=./src/assets/img/ node ./compress.js"
  • 需要压缩文件时,执行npm run compress
  • 修改.gitignore文件,添加/src/assets/img/*.log

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: