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

leeing發表於2018-10-26

https://user-gold-cdn.xitu.io/2018/10/27/166b172f7ccdcdab?w=1020&h=510&f=jpeg&s=98515

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

主檔案

在主檔案中,我們主要用到了nodefs模組。 首先我們會判斷輸入的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物件,因此,我們把readFiletinify.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

參考連結

相關文章