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的https://github.com/vuejs/vue/blob/dev/package.json
初始化完成之后,你就可以著手寫這個包了,當你覺得你寫好了之后,就可以發布到npm上面
npm loginnpm publish+ pkgname@1.0.0 // 成功
這時,你在npm上面搜你的包名,你寫在package.json 的信息都會被解析,然后你的包的頁面介紹內容就是你的README.md
3、寫這個包
包初始化好了之后,我們就可以開始寫這個包了
對于這個壓縮工具來說,要用到的素材只有兩個,tinypng接口要用到的 api-key,需要壓縮的圖片,所以我對這兩個素材需要用到的一些操作進行了以下分析:
我的初衷是想把這個命令寫的盡量簡單,讓我可以聯想到壓縮圖片=簡單,所以我待定了整個包只有一個單詞就能跑,是這樣:
$ tinyhere
其他的操作都放在子命令和可選項上。
然后開始劃分項目結構
大致上是這樣,把全局命令執行的 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 nodeconst 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-keyconst 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}
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持VeVb武林網。
新聞熱點
疑難解答