手把手教你用node擼一個圖片壓縮工具

上篇文章中我們提到了用node擼一個簡易的爬蟲,本次基於上一篇文章中的專案 ofollow,noindex">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的API tinify.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…