上篇文章中我們提到了用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…