上篇文章中我们提到了用node撸一个简易的爬虫,本次基于上一篇文章中的项目get_picture给大家分享下我是如何用node撸一个图片压缩工具的。原文链接leeing.site/2018/10/27/…
历史: 《手把手教你用node撸一个简易的headless爬虫cli工具》
tinypng
依然是先介绍一下工具,本次我们主要用到了 tinypng
这个工具。tinypng是一个主流的图片压缩工具,他可以实现高保真的压缩我们的图片,一般我们可以进入他的官网tinypng.com/压缩图片,手动点击上传,但是每次只能压缩20张,这对于追求方便的我们来说肯定是不能满足的。我们需要一次性将所有图片都压缩!
这怎么办呢?tinypng官网十分的人性化,提供了各种服务端直接调用的接口,我们点开他的文档看一看,找到node.js,通过npm i --save tinify
安装在我们的项目中,其次可以看到他提供了各种各样的功能,包括压缩图片
、resize图片
、上传cdn
等。我们主要用到了他的压缩图片
、验证key
、查看已用数
。
目录结构
|-- 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
主文件
在主文件中,我们主要用到了node
的fs模块
。 首先我们会判断输入的key是否有效,其次我们会判断该key剩余可用数是不是小于0,如果没问题的话,我们就开始查找检索路径下的所有文件。
检索路径 首先我们会通过fs.stat
判断该路径是否是文件夹,如果是,则通过fs.readdir
获取当前文件列表,遍历后然后将其传给获取图片方法。注意这边有个坑点,因为我们的操作几乎都是异步操作,所以我一开始也很理所当然的用了forEach来遍历,伪代码如下
files.forEach(async (file) => {
await getImg(file);
});
复制代码
后来发现,这种写法会导致await并不能如我们预期的阻断来执行,而是变成了一个同步的过程(一开始的预期是一张图片压缩输出完才执行第二张,虽然这样会导致很慢。所以后面还是换成了同步压缩),这是因为forEach
可以理解为传入一个function,然后在内部执行循环,在循环中执行function并传回index和item,如果传入的是async函数的话,则其实是并行执行了多个匿名async函数自调,因此await无法按照我们预期的来执行。所以该处我们采用for-of
循环,伪代码如下
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对象,因此,我们把readFile
、tinify.fromBuffer(sourceData).toBuffer((err, resultData) => {})
、fs.writeFile
用promise进行封装。 至此,我们的主程序就大功告成了!怎么样,是不是依然非常简单。 最后只要在commander中加入我们的新命令就好了。
/src/imgMin.js代码如下:
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代码如下:
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中,我们加入以下代码
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
参考链接
- 该项目的git链接 github.com/1eeing/get_…
- tinypng官网 tinypng.com/
- commander git链接 github.com/tj/commande…