自從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的https://github.com/vuejs/vue/blob/dev/package.json json
初始化完成以後,你就能夠着手寫這個包了,當你以爲你寫好了以後,就能夠發佈到npm上面api
npm login npm publish + pkgname@1.0.0 // 成功
這時,你在npm上面搜你的包名,你寫在package.json 的信息都會被解析,而後你的包的頁面介紹內容就是你的README.md數組
包初始化好了以後,咱們就能夠開始寫這個包了async
對於這個壓縮工具來講,要用到的素材只有兩個,tinypng接口要用到的 api-key,須要壓縮的圖片,因此我對這兩個素材須要用到的一些操做進行了如下分析:模塊化
個人初衷是想把這個命令寫的儘可能簡單,讓我能夠聯想到壓縮圖片=簡單,因此我待定了整個包只有一個單詞就能跑,是這樣:
$ 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/ti...
https://www.npmjs.com/package...
最後貼上部分代碼,內容過長,能夠跳過哦
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 }