摘要:在發布自己造的輪子前言自從出現,它的好基友也是我們日常開發中必不可少的東西。
在NPM發布自己造的輪子 1、前言
自從Node.js出現,它的好基友npm(node package manager)也是我們日常開發中必不可少的東西。npm讓js實現了模塊化,使得復用其他人寫好的模塊(搬磚)變得更加方便,也讓我們可以分享一些自己的作品給大家使用(造輪子),今天這里我就給大家分享一個用命令行壓縮圖片的工具,它的用法大致是這樣的:
// 全局安裝后,在圖片目錄下,運行這行 $ tinyhere
這樣就把文件夾內的圖片進行壓縮。這里壓縮采用的是 tinypng 提供的接口,壓縮率大致上是50%,基本可以壓一半的大小。以前在寫項目的時候,測試驗收完成后總是要自己手動去壓一次圖片,后來想把這個枯燥重復的事自動化去完成(懶),但是公司腳手架又沒有集成這個東西,就想自己寫一個輪子做出來用用就好了。它的名字叫做tinyhere,大家可以去安裝使用試一下
$ npm i tinyhere -g2、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 login npm 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/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", "壓縮后的圖片存放到指定路徑(使用相對路徑)") .option("-a, --add ", "添加api-key") .option("--delete ", "刪除指定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(" 正在檢測,請稍候..."); process.stdout.write(" 正在檢測,請稍候..."); 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 }
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/109039.html
摘要:原文地址為了探究按需加載的本質,選擇了對先前造的輪子進行實驗。下文就來揭開面紗,并動手改造項目,最終目標是用第二種寫法實現按需加載,減小打包體積。下面給出種可以按需加載的方案。 原文地址 為了探究按需加載的本質,選擇了對先前造的輪子 diana 進行實驗。 實驗一:全量引用 import * as _ from diana 打包體積結果如下: showImg(http://oqhtsc...
摘要:起因工作也差不多滿一年了,對于基本的業務開發有了一些自己的想法剛開始工作的前個月,每天都可以接觸到新東西,接觸新業務個月之后業務開發熟悉了對于自己的技術成長就感覺受到了局限如果一直沒有作出改變,那么等于是個月的經驗要用一年我的學習方式就是多 起因 工作也差不多滿一年了,對于基本的業務開發有了一些自己的想法 剛開始工作的前3個月,每天都可以接觸到新東西,接觸新業務 3個月之后業務開發熟...
摘要:為此,玉伯當時還特意發了一條微博,說是應該給和也樹一塊墓碑了。這里,閏土所說的過時,并不是指它現在就不能用了,而是說出現了明顯更加先進的理念或者標準,這會導致未來它的使用場景大為減少,整體趨勢已經步入衰落。 showImg(https://segmentfault.com/img/bVYQLf?w=700&h=392); 前言 都已經2017年的11月份了,我們項目還打算用seajs?...
摘要:最近學習,學習過程中使用官方推薦的庫,感覺官方庫不太好用,基礎的沒問題。介紹這個輪子其實是很早以前就造好的,主要參考的數據庫操作方式。將設置表名設置查詢字段聯表等操作進行鏈式操作,給人一種語義化操作數據庫的感覺。 最近學習eggjs,學習過程中使用官方推薦的MySQL庫,感覺官方庫不太好用,基礎的CURD沒問題。但是復雜點的操作就不行了,雖然官方還有一個egg-sequelize,但是...
閱讀 2763·2021-09-24 09:47
閱讀 4382·2021-08-27 13:10
閱讀 3032·2019-08-30 15:44
閱讀 1302·2019-08-29 12:56
閱讀 2604·2019-08-28 18:07
閱讀 2627·2019-08-26 14:05
閱讀 2587·2019-08-26 13:41
閱讀 1278·2019-08-26 13:33