在NPM釋出自己造的輪子

Lemoncool發表於2020-07-29

在NPM釋出自己造的輪子

1、前言

自從Node.js出現,它的好基友npm(node package manager)也是我們日常開發中必不可少的東西。npm讓js實現了模組化,使得複用其他人寫好的模組(搬磚)變得更加方便,也讓我們可以分享一些自己的作品給大家使用(造輪子),今天這裡我就給大家分享一個用命令列壓縮圖片的工具,它的用法大致是這樣的:

// 全域性安裝後,在圖片目錄下,執行這行
$ tinyhere
複製程式碼

這樣就把資料夾內的圖片進行壓縮。這裡壓縮採用的是 tinypng 提供的介面,壓縮率大致上是50%,基本可以壓一半的大小。以前在寫專案的時候,測試驗收完成後總是要自己手動去壓一次圖片,後來想把這個枯燥重複的事自動化去完成(懶),但是公司腳手架又沒有整合這個東西,就想自己寫一個輪子做出來用用就好了。它的名字叫做tinyhere,大家可以去安裝使用試一下

$ npm i tinyhere -g
複製程式碼

2、npm簡介

如果要寫一個模組釋出到npm,那麼首先要了解一下npm的用法。

給這個模組建一個資料夾,然後在目錄內執行npm init來初始化它的package.json,就是這個包的描述

// 個人比較喜歡後面帶--yes,它會生成一個帶預設引數的package.json
$ npm init (--yes)
複製程式碼

package.json詳情:

{
  "name": "pkgname", // 包名,預設資料夾的名字
  "version": "1.0.0",
  "description": "my package",
  "main": "index.js", // 如果只是用來全域性安裝的話,可以不寫
  "bin": "cli", // 如果是命令列使用的話,必須要這個,名字就是命令名
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1" // npm run test對應的test
  },
  "keywords": ['cli', 'images', 'compress'],
  "author": "croc-wend",
  "license": "MIT",
  ...
}

複製程式碼

更多配置資訊可以參考一下vue的package.json的github.com/vuejs/vue/b…

初始化完成之後,你就可以著手寫這個包了,當你覺得你寫好了之後,就可以釋出到npm上面

npm login
npm publish
+ pkgname@1.0.0 // 成功
複製程式碼

這時,你在npm上面搜你的包名,你寫在package.json 的資訊都會被解析,然後你的包的頁面介紹內容就是你的README.md

3、寫這個包

包初始化好了之後,我們就可以開始寫這個包了

對於這個壓縮工具來說,要用到的素材只有兩個,tinypng介面要用到的 api-key,需要壓縮的圖片,所以我對這兩個素材需要用到的一些操作進行了以下分析:

在NPM釋出自己造的輪子

我的初衷是想把這個命令寫的儘量簡單,讓我可以聯想到壓縮圖片=簡單,所以我待定了整個包只有一個單詞就能跑,是這樣:

$ tinyhere
複製程式碼

其他的操作都放在子命令和可選項上。

然後開始劃分專案結構

在NPM釋出自己造的輪子

大致上是這樣,把全域性命令執行的 tinyhere 放在bin目錄下,然後subCommand負責提供操作函式,然後把可複用的函式(比如讀寫操作)抽離出來放在util上,比較複雜的功能單獨抽離成一個檔案,比如compress,然後匯出一個函式給subCommand。至於存放使用者的api-key,就存放在data下面的key裡。

tinyhere的執行檔案就負責解析使用者的輸入,然後執行subCommand給出的對應函式。

4、過程解析

壓縮圖片的這個包的過程是這樣的:

1、解析當前目錄內的所有圖片檔案,這裡應該根據二進位制流及檔案頭獲取檔案型別mime-type,然後讀取檔案二進位制的頭資訊,獲取其真實的檔案型別,來判斷它是否真的是圖片檔案,而不是那些僅僅是字尾名改成.png的假貨

2、 如果使用者有要求把壓縮的圖片存放到指定目錄,那就需要生成一個資料夾來存放它們。那麼,首先要判斷這個路徑是否合法,然後再去生成這個目錄

3、判斷使用者的api-key的剩餘次數是否足夠這次的圖片壓縮,如果這個key不夠,就換到下一個key,知道遍歷檔案內所有的key找到有可用的key為止。

4、圖片和key都有了,這時可以進行壓縮了。用一個陣列把壓縮失敗的存起來,然後每次壓縮完成都輸出提示,在所有圖片都處理完成後,如果存在壓縮失敗的,就詢問是否把壓縮失敗的圖繼續壓縮

5、這樣,一次壓縮就處理完成了。壓縮過的圖片會覆蓋原有的圖片,或者是存放到指定的路徑裡

ps:$ tinyhere deep >>> 把目錄內的所有圖片都進行壓縮(含子目錄)。這個命令和上述的主命令的流程有點不同,目前有點頭緒,還沒有開發完成,考慮到檔案系統是樹形結構,我目前的想法是通過深度遍歷,把存在圖片的資料夾當作一個單位,然後遞迴執行壓縮。

其他:

這裡吐槽一下tinypng 的介面寫的真的爛。。在查詢key的合法性的 validate 函式只接受報錯的回撥,但是成功卻沒有任何動作。我真是服了,之前是做延時來判斷使用者的key的合法性,最後實在是受不了這個bug一樣的寫法了,決定用Object.defineProperty來監聽它的使用次數的變化。如果它的setter被呼叫則說明它是一個合法的key了

5、小結

在這裡,我想跟大家說,如果你做了一個你覺得很酷的東西,也想給更多的人去使用,來讓它變得更好,選擇釋出在NPM上面就是一個非常好的途徑,看了上面的內容你會發現分享其實真的不難,你也有機會讓世界看到屬於你的風采!

如果大家覺得我有哪裡寫錯了,寫得不好,有其它什麼建議(誇獎),非常歡迎大家補充。希望能讓大家交流意見,相互學習,一起進步! 我是一名 19 的應屆新人,以上就是今天的分享,新手上路中,後續不定期周更(或者是月更哈哈),我會努力讓自己變得更優秀、寫出更好的文章,文章中有不對之處,煩請各位大神斧正。如果你覺得這篇文章對你有所幫助,請記得點贊或者品論留言哦~。

6、寫在最後

歡迎大家提issue或者建議!地址在這:

https://github.com/Croc-ye/tinyhere

https://www.npmjs.com/package/tinyhere

最後貼上部分程式碼,內容過長,可以跳過哦

bin/tinyhere

#!/usr/bin/env node

const commander = require('commander');
const {init, addKey, deleteKey, emptyKey, list, compress} = require('../libs/subCommand.js');
const {getKeys} = require('../libs/util.js');

// 主命令
commander
.version(require('../package').version, '-v, --version')
.usage('[options]')
.option('-p, --path <newPath>', '壓縮後的圖片存放到指定路徑(使用相對路徑)')
.option('-a, --add <key>', '新增api-key')
.option('--delete <key>', '刪除指定api-key')
.option('-l, --list', '顯示已儲存的api-key')
.option('--empty', '清空已儲存的api-key')

// 子命令
commander
.command('deep')
.description('把該目錄內的所有圖片(含子目錄)的圖片都進行壓縮')
.action(()=> {
    // deepCompress();
    console.log('尚未完成,敬請期待');
})

commander.parse(process.argv);


// 選擇入口
if (commander.path) {
    // 把圖片存放到其他路徑
    compress(commander.path);
} else if (commander.add) {
    // 新增api-key
    addKey(commander.add);
} else if (commander.delete) {
    // 刪除api-key
    deleteKey(commander.delete);
} else if (commander.list) {
    // 顯示api-key
    list();
} else if (commander.empty) {
    // 清空api-key
    emptyKey();
} else {
    // 主命令
    if (typeof commander.args[0] === 'object') {
        // 子命令
        return;
    }
    if (commander.args.length !== 0) {
        console.log('未知命令');
        return;
    }
    if (getKeys().length === 0) {
        console.log('請初始化你的api-key')
        init();
    } else {
        compress();
    }
};
複製程式碼

libs/compress.js

const tinify = require('tinify');
const fs = require("fs");
const path = require('path');
const imageinfo = require('imageinfo');
const inquirer = require('inquirer');
const {checkApiKey, getKeys} = require('./util');

// 對當前目錄內的圖片進行壓縮
const compress = (newPath = '')=> {
    const imageList = readDir();
    if (imageList.length === 0) {
        console.log('當前目錄內無可用於壓縮的圖片');
        return;
    }
    newPath = path.join(process.cwd(), newPath);
    mkDir(newPath);

    findValidateKey(imageList.length);
    console.log('===========開始壓縮=========');
    if (newPath !== process.cwd()) {
        console.log('壓縮到:  ' + newPath.replace(/\./g, ''));
    }
    compressArray(imageList, newPath);
};

// 生成目錄路徑
const mkDir = (filePath)=> {
    if (filePath && dirExists(filePath) === false) {
        fs.mkdirSync(filePath);
    }
}

// 判斷目錄是否存在
const dirExists = (filePath)=> {
    let res = false;
    try {
        res = fs.existsSync(filePath);
    } catch (error) {
        console.log('非法路徑');
        process.exit();
    }
    return res;
};


/**
 * 檢查api-key剩餘次數是否大於500
 * @param {*} count 本次需要壓縮的圖片數目
 */
const checkCompressionCount = (count = 0)=> {
    return (500 - tinify.compressionCount - count) >> 0;
}

/**
 * 找到可用的api-key
 * @param {*} imageLength 本次需要壓縮的圖片數目
 */
const findValidateKey = async imageLength=> { // bug高發處
    const keys = getKeys();
    for (let i = 0; i < keys.length; i++) {
        await checkApiKey(keys[i]);
        res = checkCompressionCount(imageLength);
        if (res) return;
    }
    console.log('已儲存的所有api-key都超出了本月500張限制,如果要繼續使用請新增新的api-key');
    process.exit();
}

// 獲取當前目錄的所有png/jpg檔案
const readDir = ()=> {
    const filePath = process.cwd()
    const arr = fs.readdirSync(filePath).filter(item=> {
        // 這裡應該根據二進位制流及檔案頭獲取檔案型別mime-type,然後讀取檔案二進位制的頭資訊,獲取其真實的檔案型別,對與通過字尾名獲得的檔案型別進行比較。
        if (/(\.png|\.jpg|\.jpeg)$/.test(item)) { // 求不要出現奇奇怪怪的檔名。。
            const fileInfo = fs.readFileSync(item);
            const info = imageinfo(fileInfo);
            return /png|jpg|jpeg/.test(info.mimeType);
        }
        return false;
    });
    return arr;
};

/**
 * 對陣列內的圖片名進行壓縮
 * @param {*} imageList 存放圖片名的陣列
 * @param {*} newPath 壓縮後的圖片的存放地址
 */
const compressArray = (imageList, newPath)=> {
    const failList = [];
    imageList.forEach(item=> {
        compressImg(item, imageList.length, failList, newPath);
    });
}

/**
 * 壓縮給定名稱的圖片
 * @param {*} name 檔名
 * @param {*} fullLen 全部檔案數量
 * @param {*} failsList 壓縮失敗的陣列
 * @param {*} filePath 用來存放的新地址
 */
const compressImg = (name, fullLen, failsList, filePath)=> {
    fs.readFile(name, function(err, sourceData) {
        if (err) throw err;
        tinify.fromBuffer(sourceData).toBuffer(function(err, resultData) {
          if (err) throw err;
          filePath = path.join(filePath, name);
          const writerStream = fs.createWriteStream(filePath);
          // 標記檔案末尾
          writerStream.write(resultData,'binary');
          writerStream.end();
      
          // 處理流事件 --> data, end, and error
          writerStream.on('finish', function() {
            failsList.push(null);
            record(name, true, failsList.length, fullLen);
            if (failsList.length === fullLen) {
                finishcb(failsList, filePath);
            }
          });

          writerStream.on('error', function(err){
            failsList.push(name);
            record(name, false, failsList.length, fullLen);
            if (failsList.length === fullLen) {
                finishcb(failsList, filePath);
            }
          });
        });
    });
}

// 生成日誌
const record = (name, success = true, currNum, fullLen)=> {
    const status = success ? '完成' : '失敗';
    console.log(`${name} 壓縮${status}${currNum}/${fullLen}`);
}

/**
 * 完成呼叫的回撥
 * @param {*} failList 儲存壓縮失敗圖片名的陣列
 * @param {*} filePath 用來存放的新地址
 */
const finishcb = (failList, filePath)=> {
    const rest = 500 - tinify.compressionCount;
    console.log('本月剩餘次數:' + rest);
    const fails = failList.filter(item=> item !== null);
    if (fails.length > 0) {
        // 存在壓縮失敗的專案(展示失敗的專案名),詢問是否把壓縮失敗的繼續壓縮 y/n
        // 選擇否之後,詢問是否生成錯誤日誌
        inquirer.prompt({
            type: 'confirm',
            name: 'compressAgain',
            message: '存在壓縮失敗的圖片,是否將失敗的圖片繼續壓縮?',
            default: true
        }).then(res=> {
            if (res) {
                compressArray(failList, filePath);
            } else {
			   // 詢問是否生成錯誤日誌
            }
        })
    } else {
        // 壓縮完成
        console.log('======圖片已全部壓縮完成======');
    }
}

module.exports = {
    compress
}
複製程式碼

libs/subCommand.js

const inquirer = require('inquirer');
const {compress} = require('./compress.js');
const {checkApiKey, getKeys, addKeyToFile, list} = require('./util.js');

module.exports.compress = compress;
module.exports.init = ()=> {
    inquirer.prompt({
        type: 'input',
        name: 'apiKey',
        message: '請輸入api-key:',
        validate: (apiKey)=> {
            // console.log('\n正在檢測,請稍候...');
            process.stdout.write('\n正在檢測,請稍候...');
            return new Promise(async (resolve)=> {
                const res = await checkApiKey(apiKey);
                resolve(res);
            });
        }
    }).then(async res=> {
        await addKeyToFile(res.apiKey);
        console.log('apikey 已完成初始化,壓縮工具可以使用了');
    })
}

module.exports.addKey = async key=> {
    await checkApiKey(key);
    const keys = await getKeys();
    if (keys.includes(key)) {
        console.log('該api-key已存在檔案內');
        return;
    }
    const content = keys.length === 0 ? '' : keys.join(' ') + ' ';
    await addKeyToFile(key, content);
    list();
}

module.exports.deleteKey = async key=> {
    const keys = await getKeys();
    const index = keys.indexOf(key);
    if (index < 0) {
        console.log('該api-key不存在');
        return;
    }
    keys.splice(index, 1);
    console.log(keys);
    const content = keys.length === 0 ? '' : keys.join(' ');
    await addKeyToFile('', content);
    list();
}

module.exports.emptyKey = async key=> {
    inquirer.prompt({
        type: 'confirm',
        name: 'emptyConfirm',
        message: '確認清空所有已儲存的api-key?',
        default: true
    }).then(res=> {
        if (res.emptyConfirm) {
            addKeyToFile('');
        } else {
            console.log('已取消');
        }
    })
}

module.exports.list = list;
複製程式碼

libs/util.js

const fs = require('fs');
const path = require('path');
const tinify = require('tinify');
const KEY_FILE_PATH = path.join(__dirname, './data/key');

// 睡眠
const sleep = (ms)=> {
    return new Promise(function(resolve) {
        setTimeout(()=> {
            resolve(true);
        }, ms);
    });
}
// 判定apikey是否有效
const checkApiKey = async apiKey=> {
    return new Promise(async resolve=> {
        let res = true;
        res = /^\w{32}$/.test(apiKey);
        if (res === false) {
            console.log('api-key格式不對');
            resolve(res);
            return;
        }
        res = await checkKeyValidate(apiKey);
        resolve(res);
    })
}
// 檢查api-key是否存在
const checkKeyValidate = apiKey=> {
    return new Promise(async (resolve)=> {
        tinify.key = apiKey;
        tinify.validate(function(err) {
            if (err) {
                console.log('該api-key不是有效值');
                resolve(false);
            }
        });
        let count = 500;
        Object.defineProperty(tinify, 'compressionCount', {
            get: ()=> {
                return count;
            },
            set: newValue => {
                count = newValue;
                resolve(true);
            },
            enumerable : true,
            configurable : true
        });
    });
};

// 獲取檔案內的key,以陣列的形式返回
const getKeys = ()=> {
    const keys =  fs.readFileSync(KEY_FILE_PATH, 'utf-8').split(' ');
    return keys[0] === '' ? [] : keys;
}

// 把api-key寫入到檔案裡
const addKeyToFile = (apiKey, content = '')=> {
    return new Promise(async resolve=> {
        const writerStream = fs.createWriteStream(KEY_FILE_PATH);
        // 使用 utf8 編碼寫入資料
        writerStream.write(content + apiKey,'UTF8');

        // 標記檔案末尾
        writerStream.end();

        // 處理流事件 --> data, end, and error
        writerStream.on('finish', function() {
            console.log('=====已更新=====');
            resolve(true);
        });

        writerStream.on('error', function(err){
            console.log(err.stack);
            console.log('寫入失敗。');
            resolve(false);
        });
    })
}

// 顯示檔案內的api-key
const list = ()=> {
    const keys = getKeys();
    if (keys.length === 0) {
        console.log('沒有儲存api-key');
    } else {
        keys.forEach((key)=> {
            console.log(key);
        });
    }
};
module.exports = {
    sleep,
    checkApiKey,
    getKeys,
    addKeyToFile,
    list
}
複製程式碼

相關文章