简介
- 前端项目中会有一些静态的图片资源。
- 在发布到生成环境之前,一般会进行图片压缩。生成环境使用压缩后的图片
- 常用的图片压缩方法是上传到Tinypng.com站点上面压缩
需求
- 编写方法,实现通过输入命令可以压缩某个目录及其子目录下面的图片文件
- 压缩使用Tinypng.com站点提供的接口实现
思路
- 通过
Node.js
的fs
模块读取目录及其子目录下面的所有文件 - 对文件进行过滤。保留符合要求的图片文件
- 对符合要求的图片文件挨个调用压缩接口
- 接口请求使用
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()
方法读取文件或目录的属性- 如果是目录,读取该子目录下的所有文件和目录
- 如果是文件并满足条件。将文件
push
到list
数组中 - 都执行完后,就得到了所有可压缩文件的数组
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
测试下
-
这里我们加上一个
User-Agent
:Mozilla/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"
}
-
接口地址是
https
地址。这里通过axios
请求时有签名问题。解决方法参考Axios遇到自签名服务后台的解决方案 -
具体实现如下:
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
发表评论