自從Node.js出現,它的好基友npm(node package manager)也是咱們平常開發中必不可少的東西。npm讓js實現了模塊化,使得複用其餘人寫好的模塊(搬磚)變得更加方便,也讓咱們能夠分享一些本身的做品給你們使用(造輪子),今天這裏我就給你們分享一個用命令行壓縮圖片的工具,它的用法大體是這樣的:vue
// 全局安裝後,在圖片目錄下,運行這行
$ tinyhere
複製代碼
這樣就把文件夾內的圖片進行壓縮。這裏壓縮採用的是 tinypng 提供的接口,壓縮率大體上是50%,基本能夠壓一半的大小。之前在寫項目的時候,測試驗收完成後老是要本身手動去壓一次圖片,後來想把這個枯燥重複的事自動化去完成(懶),可是公司腳手架又沒有集成這個東西,就想本身寫一個輪子作出來用用就行了。它的名字叫作tinyhere
,你們能夠去安裝使用試一下node
$ npm i tinyhere -g
複製代碼
若是要寫一個模塊發佈到npm,那麼首先要了解一下npm的用法。git
給這個模塊建一個文件夾,而後在目錄內運行npm init
來初始化它的package.json,就是這個包的描述github
// 我的比較喜歡後面帶--yes,它會生成一個帶默認參數的package.json
$ npm init (--yes)
複製代碼
package.json詳情:npm
{
"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…json
初始化完成以後,你就能夠着手寫這個包了,當你以爲你寫好了以後,就能夠發佈到npm上面api
npm login
npm publish
+ pkgname@1.0.0 // 成功
複製代碼
這時,你在npm上面搜你的包名,你寫在package.json 的信息都會被解析,而後你的包的頁面介紹內容就是你的README.md數組
包初始化好了以後,咱們就能夠開始寫這個包了bash
對於這個壓縮工具來講,要用到的素材只有兩個,tinypng接口要用到的 api-key,須要壓縮的圖片,因此我對這兩個素材須要用到的一些操做進行了如下分析:async
個人初衷是想把這個命令寫的儘可能簡單,讓我能夠聯想到壓縮圖片=簡單,因此我待定了整個包只有一個單詞就能跑,是這樣:
$ tinyhere
複製代碼
其餘的操做都放在子命令和可選項上。
而後開始劃分項目結構
大體上是這樣,把全局命令執行的 tinyhere
放在bin目錄下,而後subCommand負責提供操做函數,而後把可複用的函數(好比讀寫操做)抽離出來放在util上,比較複雜的功能單獨抽離成一個文件,好比compress,而後導出一個函數給subCommand。至於存放用戶的api-key,就存放在data下面的key裏。
tinyhere的執行文件就負責解析用戶的輸入,而後執行subCommand給出的對應函數。
壓縮圖片的這個包的過程是這樣的:
一、解析當前目錄內的全部圖片文件,這裏應該根據二進制流及文件頭獲取文件類型mime-type,而後讀取文件二進制的頭信息,獲取其真實的文件類型,來判斷它是否真的是圖片文件,而不是那些僅僅是後綴名改爲.png的假貨
二、 若是用戶有要求把壓縮的圖片存放到指定目錄,那就須要生成一個文件夾來存放它們。那麼,首先要判斷這個路徑是否合法,而後再去生成這個目錄
三、判斷用戶的api-key的剩餘次數是否足夠此次的圖片壓縮,若是這個key不夠,就換到下一個key,知道遍歷文件內全部的key找到有可用的key爲止。
四、圖片和key都有了,這時能夠進行壓縮了。用一個數組把壓縮失敗的存起來,而後每次壓縮完成都輸出提示,在全部圖片都處理完成後,若是存在壓縮失敗的,就詢問是否把壓縮失敗的圖繼續壓縮
五、這樣,一次壓縮就處理完成了。壓縮過的圖片會覆蓋原有的圖片,或者是存放到指定的路徑裏
ps:$ tinyhere deep
>>> 把目錄內的全部圖片都進行壓縮(含子目錄)。這個命令和上述的主命令的流程有點不一樣,目前有點頭緒,尚未開發完成,考慮到文件系統是樹形結構,我目前的想法是經過深度遍歷,把存在圖片的文件夾看成一個單位,而後遞歸執行壓縮。
其餘:
這裏吐槽一下tinypng 的接口寫的真的爛。。在查詢key的合法性的 validate 函數只接受報錯的回調,可是成功卻沒有任何動做。我真是服了,以前是作延時來判斷用戶的key的合法性,最後實在是受不了這個bug同樣的寫法了,決定用Object.defineProperty來監聽它的使用次數的變化。若是它的setter被調用則說明它是一個合法的key了
在這裏,我想跟你們說,若是你作了一個你以爲很酷的東西,也想給更多的人去使用,來讓它變得更好,選擇發佈在NPM上面就是一個很是好的途徑,看了上面的內容你會發現分享其實真的不難,你也有機會讓世界看到屬於你的風采!
若是你們以爲我有哪裏寫錯了,寫得很差,有其它什麼建議(誇獎),很是歡迎你們補充。但願能讓你們交流意見,相互學習,一塊兒進步! 我是一名 19 的應屆新人,以上就是今天的分享,新手上路中,後續不按期周更(或者是月更哈哈),我會努力讓本身變得更優秀、寫出更好的文章,文章中有不對之處,煩請各位大神斧正。若是你以爲這篇文章對你有所幫助,請記得點贊或者品論留言哦~。
歡迎你們提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
}
複製代碼