手把手教你用node撸一个图片压缩工具

https://tinypng.com/images/social/developer-api.jpg

上篇文章中我们提到了用node撸一个简易的爬虫,本次基于上一篇文章中的项目get_picture给大家分享下我是如何用node撸一个图片压缩工具的。

历史
《手把手教你用node撸一个简易的headless爬虫cli工具》

tinypng

依然是先介绍一下工具,本次我们主要用到了 tinypng 这个工具。tinypng是一个主流的图片压缩工具,他可以实现高保真的压缩我们的图片,一般我们可以进入他的官网https://tinypng.com/压缩图片,手动点击上传,但是每次只能压缩20张,这对于追求方便的我们来说肯定是不能满足的。我们需要一次性将所有图片都压缩!

这怎么办呢?tinypng官网十分的人性化,提供了各种服务端直接调用的接口,我们点开他的文档看一看,找到node.js,通过npm i --save tinify安装在我们的项目中,其次可以看到他提供了各种各样的功能,包括压缩图片resize图片上传cdn等。我们主要用到了他的压缩图片验证key查看已用数

目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|-- Documents
|-- .gitignore
|-- README.md
|-- package.json
|-- bin
| |-- gp
|-- output
| |-- .gitkeeper
|-- src
|-- app.js
|-- clean.js
|-- imgMin.js
|-- index.js
|-- config
| |-- default.js
|-- helper
|-- questions.js
|-- regMap.js
|-- srcToImg.js
|-- tinify.js

基于上一个项目,我们新增了两个文件

  • /src/imgMin.js。即我们的主文件。
  • /src/helper/tinify.js。主要用于操作tinypng的相关API

主文件

在主文件中,我们主要用到了nodefs模块
首先我们会判断输入的key是否有效,其次我们会判断该key剩余可用数是不是小于0,如果没问题的话,我们就开始查找检索路径下的所有文件。

检索路径
首先我们会通过fs.stat判断该路径是否是文件夹,如果是,则通过fs.readdir获取当前文件列表,遍历后然后将其传给获取图片方法。注意这边有个坑点,因为我们的操作几乎都是异步操作,所以我一开始也很理所当然的用了forEach来遍历,伪代码如下

1
2
3
files.forEach(async (file) => {
await getImg(file);
});

后来发现,这种写法会导致await并不能如我们预期的阻断来执行,而是变成了一个同步的过程(一开始的预期是一张图片压缩输出完才执行第二张,虽然这样会导致很慢。所以后面还是换成了同步压缩),这是因为forEach可以理解为传入一个function,然后在内部执行循环,在循环中执行function并传回index和item,如果传入的是async函数的话,则其实是并行执行了多个匿名async函数自调,因此await无法按照我们预期的来执行。所以该处我们采用for-of循环,伪代码如下

1
2
3
for(let file of files){
await getImg(file);
}

获取图片
在获取图片中,我们依然会通过fs.stat来判断,如果当前文件依然是个文件夹,我们则递归调用findImg检索其下的文件,如果是图片,先判断当前累计图片总数有没有超过剩余数的最大值(如果使用异步压缩,则不需要进行这一步,因为每一次图片处理都是等待上一张图片处理完成后再进行处理;如果是同步压缩,则必须要这一步,否则如果压缩过程中超数量了,会导致整批压缩失败),如果没有超过,则通过调用tinify.js中的imgMin方法开始进行压缩。

压缩图片
在这一步中,我们先通过fs.readFile读取文件内容sourceData,再通过tinypng的APItinify.fromBuffer(sourceData).toBuffer((err, resultData) => {})方法获取图片压缩后的数据resuleData,最后通过fs.writeFile对原图片进行覆盖。需要注意一点,async/await中,只有遇到await才会等待执行,并且await后面需要跟一个promise对象,因此,我们把readFiletinify.fromBuffer(sourceData).toBuffer((err, resultData) => {})fs.writeFile用promise进行封装。
至此,我们的主程序就大功告成了!怎么样,是不是依然非常简单。
最后只要在commander中加入我们的新命令就好了。

/src/imgMin.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
const path = require('path');
const fs = require('fs');
const chalk = require('chalk');
const defaultConf = require('./config/default');
const { promisify } = require('util');
const readdir = promisify(fs.readdir);
const stat = promisify(fs.stat);
const regMap = require('./helper/regMap');
const { validate, leftCount, imgMin } = require('./helper/tinify');

class ImgMin {
constructor(conf) {
this.conf = Object.assign({}, defaultConf, conf);
this.imgs = 0;
}

async isDir(filePath) {
try {
const stats = await stat(filePath);
if(stats.isDirectory()){
return true;
}
return false;
} catch (error) {
return false;
}
}

async findImg(filePath) {
try {
const isDirectory = await this.isDir(filePath);
if(!isDirectory){
return;
}
const files = await readdir(filePath);
for(let file of files){
// 这里不能用forEach,只能用for循环
// 加上await,则是一张张异步压缩图片,如果中间出错,则部分成功
// 不加await,则是同步发起压缩图片请求,异步写入,如果中间出错,则全部失败
// 这里为了压缩更快,采用同步写法

// await this.getImg(file);
const fullPath = path.join(filePath, file);
this.getImg(fullPath);
}
} catch (error) {
console.log(error);
}
}

async getImg(file) {
const stats = await stat(file);
// 如果是文件夹,则递归调用findImg
if(stats.isDirectory()){
this.findImg();
}else if(stats.isFile()){
if(regMap.isTinyPic.test(file)){
this.imgs ++;
const left = leftCount();
// 剩余数判断,解决同步时剩余数不足导致的全部图片压缩失败问题
if(this.imgs > left || left < 0){
console.log(chalk.red(`当前key的可用剩余数不足!${file} 压缩失败!`));
return;
}
await imgMin(file);
}else{
console.log(chalk.red(`不支持的文件格式 ${file}`));
}
}
}

async start() {
try {
const isValidated = await validate(this.conf.key);
if(!isValidated){
return;
}
const filePath = this.conf.imgMinPath;
await this.findImg(filePath);
} catch (error) {
console.log(error);
}
}
}

module.exports = ImgMin;

/src/helper/tinify.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const fs = require('fs');
const tinify = require('tinify');
const chalk = require('chalk');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);

function setKey(key) {
tinify.key = key;
}

async function validate(key) {
console.log(chalk.green('正在认证tinyPng的key...'));
setKey(key);
return new Promise(resolve => {
tinify.validate((err) => {
if(err){
console.log(err);
return resolve(false);
}
console.log(chalk.green('认证成功!'));
const left = leftCount();
if(left <= 0){
console.log(chalk.red('当前key的剩余可用数已用尽,请更换key重试!'));
return resolve(false);
}
console.log(chalk.green(`当前key剩余可用数为 ${left}`));
resolve(true);
});
});
};

function compressionCount() {
return tinify.compressionCount;
};

function leftCount() {
const total = 500;
return total - Number(compressionCount());
};

function writeFilePromise(file, content, cb) {
return new Promise((resolve, reject) => {
fs.writeFile(file, content, (err) => {
if(err){
return reject(err);
}
cb && cb();
resolve();
});
});
};

function toBufferPromise(sourceData) {
return new Promise((resolve, reject) => {
tinify.fromBuffer(sourceData).toBuffer((err, resultData) => {
if (err) {
return reject(err);
}
resolve(resultData);
})
});
};

async function imgMin(img) {
try {
console.log(chalk.blue(`开始压缩图片 ${img}`));
const sourceData = await readFile(img);
const resultData = await toBufferPromise(sourceData);
await writeFilePromise(img, resultData, () => console.log(chalk.green(`图片压缩成功 ${img}`)));
} catch (error) {
console.log(error);
}
};

module.exports = { validate, compressionCount, leftCount, imgMin };

命令行工具
在index.js中,我们加入以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
program
.command('imgMin')
.alias('p')
.option('-k, --key [key]', `Tinypng's key, Required`)
.option('-p, --path [path]', `Compress directory. By default, the /images in the current working directory are taken.
Please enter an absolute path such as /Users/admin/Documents/xx...`)
.description('Compress your images by tinypng.')
.action(options => {
let conf = {};
if(!options.key){
console.log(chalk.red(`Please enter your tinypng's key by "gp p -k [key]"`));
return;
}
options.key && (conf.key = options.key);
options.path && (conf.imgMinPath = options.path);
const imgMin = new ImgMin(conf);
imgMin.start();
});

commander具体的用法本章就不再重复了,相信有心的同学通过上章的学习已经掌握基本用法了~

这样,我们就完成了我们的需求,再将其更新到npm中,我们就可以通过gp p -k [key]来压缩我们的图片。

项目下载

npm i get_picture -g

参考链接

坚持原创技术分享,您的支持将鼓励我继续创作!