溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

在NPM發(fā)布自己造的輪子的方法步驟

發(fā)布時(shí)間:2020-09-10 14:44:32 來源:腳本之家 閱讀:119 作者:Croc_wend 欄目:web開發(fā)

1、前言

自從Node.js出現(xiàn),它的好基友npm(node package manager)也是我們?nèi)粘i_發(fā)中必不可少的東西。npm讓js實(shí)現(xiàn)了模塊化,使得復(fù)用其他人寫好的模塊(搬磚)變得更加方便,也讓我們可以分享一些自己的作品給大家使用(造輪子),今天這里我就給大家分享一個(gè)用命令行壓縮圖片的工具,它的用法大致是這樣的:

// 全局安裝后,在圖片目錄下,運(yùn)行這行
$ tinyhere

這樣就把文件夾內(nèi)的圖片進(jìn)行壓縮。這里壓縮采用的是 tinypng 提供的接口,壓縮率大致上是50%,基本可以壓一半的大小。以前在寫項(xiàng)目的時(shí)候,測試驗(yàn)收完成后總是要自己手動(dòng)去壓一次圖片,后來想把這個(gè)枯燥重復(fù)的事自動(dòng)化去完成(懶),但是公司腳手架又沒有集成這個(gè)東西,就想自己寫一個(gè)輪子做出來用用就好了。它的名字叫做tinyhere,大家可以去安裝使用試一下

$ npm i tinyhere -g

2、npm簡介

如果要寫一個(gè)模塊發(fā)布到npm,那么首先要了解一下npm的用法。

給這個(gè)模塊建一個(gè)文件夾,然后在目錄內(nèi)運(yùn)行npm init來初始化它的package.json,就是這個(gè)包的描述

// 個(gè)人比較喜歡后面帶--yes,它會生成一個(gè)帶默認(rèn)參數(shù)的package.json
$ npm init (--yes)

package.json詳情:

{
 "name": "pkgname", // 包名,默認(rèn)文件夾的名字
 "version": "1.0.0",
 "description": "my package",
 "main": "index.js", // 如果只是用來全局安裝的話,可以不寫
 "bin": "cli", // 如果是命令行使用的話,必須要這個(gè),名字就是命令名
 "scripts": {
  "test": "echo \"Error: no test specified\" && exit 1" // npm run test對應(yīng)的test
 },
 "keywords": ['cli', 'images', 'compress'],
 "author": "croc-wend",
 "license": "MIT",
 ...
}

更多配置信息可以參考一下vue的package.json的https://github.com/vuejs/vue/blob/dev/package.json

初始化完成之后,你就可以著手寫這個(gè)包了,當(dāng)你覺得你寫好了之后,就可以發(fā)布到npm上面

npm login
npm publish
+ pkgname@1.0.0 // 成功

這時(shí),你在npm上面搜你的包名,你寫在package.json 的信息都會被解析,然后你的包的頁面介紹內(nèi)容就是你的README.md

3、寫這個(gè)包

包初始化好了之后,我們就可以開始寫這個(gè)包了

對于這個(gè)壓縮工具來說,要用到的素材只有兩個(gè),tinypng接口要用到的 api-key,需要壓縮的圖片,所以我對這兩個(gè)素材需要用到的一些操作進(jìn)行了以下分析:

在NPM發(fā)布自己造的輪子的方法步驟

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

$ tinyhere

其他的操作都放在子命令和可選項(xiàng)上。

然后開始劃分項(xiàng)目結(jié)構(gòu)

在NPM發(fā)布自己造的輪子的方法步驟

大致上是這樣,把全局命令執(zhí)行的 tinyhere 放在bin目錄下,然后subCommand負(fù)責(zé)提供操作函數(shù),然后把可復(fù)用的函數(shù)(比如讀寫操作)抽離出來放在util上,比較復(fù)雜的功能單獨(dú)抽離成一個(gè)文件,比如compress,然后導(dǎo)出一個(gè)函數(shù)給subCommand。至于存放用戶的api-key,就存放在data下面的key里。

tinyhere的執(zhí)行文件就負(fù)責(zé)解析用戶的輸入,然后執(zhí)行subCommand給出的對應(yīng)函數(shù)。

4、過程解析

壓縮圖片的這個(gè)包的過程是這樣的:

1、解析當(dāng)前目錄內(nèi)的所有圖片文件,這里應(yīng)該根據(jù)二進(jìn)制流及文件頭獲取文件類型mime-type,然后讀取文件二進(jìn)制的頭信息,獲取其真實(shí)的文件類型,來判斷它是否真的是圖片文件,而不是那些僅僅是后綴名改成.png的假貨

2、 如果用戶有要求把壓縮的圖片存放到指定目錄,那就需要生成一個(gè)文件夾來存放它們。那么,首先要判斷這個(gè)路徑是否合法,然后再去生成這個(gè)目錄

3、判斷用戶的api-key的剩余次數(shù)是否足夠這次的圖片壓縮,如果這個(gè)key不夠,就換到下一個(gè)key,知道遍歷文件內(nèi)所有的key找到有可用的key為止。

4、圖片和key都有了,這時(shí)可以進(jìn)行壓縮了。用一個(gè)數(shù)組把壓縮失敗的存起來,然后每次壓縮完成都輸出提示,在所有圖片都處理完成后,如果存在壓縮失敗的,就詢問是否把壓縮失敗的圖繼續(xù)壓縮

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

ps:$ tinyhere deep >>> 把目錄內(nèi)的所有圖片都進(jìn)行壓縮(含子目錄)。這個(gè)命令和上述的主命令的流程有點(diǎn)不同,目前有點(diǎn)頭緒,還沒有開發(fā)完成,考慮到文件系統(tǒng)是樹形結(jié)構(gòu),我目前的想法是通過深度遍歷,把存在圖片的文件夾當(dāng)作一個(gè)單位,然后遞歸執(zhí)行壓縮。

其他:

這里吐槽一下tinypng 的接口寫的真的爛。。在查詢key的合法性的 validate 函數(shù)只接受報(bào)錯(cuò)的回調(diào),但是成功卻沒有任何動(dòng)作。我真是服了,之前是做延時(shí)來判斷用戶的key的合法性,最后實(shí)在是受不了這個(gè)bug一樣的寫法了,決定用Object.defineProperty來監(jiān)聽它的使用次數(shù)的變化。如果它的setter被調(diào)用則說明它是一個(gè)合法的key了

5、小結(jié)

在這里,我想跟大家說,如果你做了一個(gè)你覺得很酷的東西,也想給更多的人去使用,來讓它變得更好,選擇發(fā)布在NPM上面就是一個(gè)非常好的途徑,看了上面的內(nèi)容你會發(fā)現(xiàn)分享其實(shí)真的不難,你也有機(jī)會讓世界看到屬于你的風(fēng)采!

如果大家覺得我有哪里寫錯(cuò)了,寫得不好,有其它什么建議(夸獎(jiǎng)),非常歡迎大家補(bǔ)充。希望能讓大家交流意見,相互學(xué)習(xí),一起進(jìn)步! 我是一名 19 的應(yīng)屆新人,以上就是今天的分享,新手上路中,后續(xù)不定期周更(或者是月更哈哈),我會努力讓自己變得更優(yōu)秀、寫出更好的文章,文章中有不對之處,煩請各位大神斧正。如果你覺得這篇文章對你有所幫助,請記得點(diǎn)贊或者品論留言哦~。

6、寫在最后

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

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

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

最后貼上部分代碼,內(nèi)容過長,可以跳過哦

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('把該目錄內(nèi)的所有圖片(含子目錄)的圖片都進(jìn)行壓縮')
.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');

// 對當(dāng)前目錄內(nèi)的圖片進(jìn)行壓縮
const compress = (newPath = '')=> {
  const imageList = readDir();
  if (imageList.length === 0) {
    console.log('當(dāng)前目錄內(nèi)無可用于壓縮的圖片');
    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剩余次數(shù)是否大于500
 * @param {*} count 本次需要壓縮的圖片數(shù)目
 */
const checkCompressionCount = (count = 0)=> {
  return (500 - tinify.compressionCount - count) >> 0;
}

/**
 * 找到可用的api-key
 * @param {*} imageLength 本次需要壓縮的圖片數(shù)目
 */
const findValidateKey = async imageLength=> { // bug高發(fā)處
  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張限制,如果要繼續(xù)使用請?zhí)砑有碌腶pi-key');
  process.exit();
}

// 獲取當(dāng)前目錄的所有png/jpg文件
const readDir = ()=> {
  const filePath = process.cwd()
  const arr = fs.readdirSync(filePath).filter(item=> {
    // 這里應(yīng)該根據(jù)二進(jìn)制流及文件頭獲取文件類型mime-type,然后讀取文件二進(jìn)制的頭信息,獲取其真實(shí)的文件類型,對與通過后綴名獲得的文件類型進(jìn)行比較。
    if (/(\.png|\.jpg|\.jpeg)$/.test(item)) { // 求不要出現(xiàn)奇奇怪怪的文件名。。
      const fileInfo = fs.readFileSync(item);
      const info = imageinfo(fileInfo);
      return /png|jpg|jpeg/.test(info.mimeType);
    }
    return false;
  });
  return arr;
};

/**
 * 對數(shù)組內(nèi)的圖片名進(jìn)行壓縮
 * @param {*} imageList 存放圖片名的數(shù)組
 * @param {*} newPath 壓縮后的圖片的存放地址
 */
const compressArray = (imageList, newPath)=> {
  const failList = [];
  imageList.forEach(item=> {
    compressImg(item, imageList.length, failList, newPath);
  });
}

/**
 * 壓縮給定名稱的圖片
 * @param {*} name 文件名
 * @param {*} fullLen 全部文件數(shù)量
 * @param {*} failsList 壓縮失敗的數(shù)組
 * @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);
     // 標(biāo)記文件末尾
     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}`);
}

/**
 * 完成調(diào)用的回調(diào)
 * @param {*} failList 存儲壓縮失敗圖片名的數(shù)組
 * @param {*} filePath 用來存放的新地址
 */
const finishcb = (failList, filePath)=> {
  const rest = 500 - tinify.compressionCount;
  console.log('本月剩余次數(shù):' + rest);
  const fails = failList.filter(item=> item !== null);
  if (fails.length > 0) {
    // 存在壓縮失敗的項(xiàng)目(展示失敗的項(xiàng)目名),詢問是否把壓縮失敗的繼續(xù)壓縮 y/n
    // 選擇否之后,詢問是否生成錯(cuò)誤日志
    inquirer.prompt({
      type: 'confirm',
      name: 'compressAgain',
      message: '存在壓縮失敗的圖片,是否將失敗的圖片繼續(xù)壓縮?',
      default: true
    }).then(res=> {
      if (res) {
        compressArray(failList, filePath);
      } else {
        // 詢問是否生成錯(cuò)誤日志
      }
    })
  } 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已存在文件內(nèi)');
    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: '確認(rèn)清空所有已存儲的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
    });
  });
};

// 獲取文件內(nèi)的key,以數(shù)組的形式返回
const getKeys = ()=> {
  const keys = fs.readFileSync(KEY_FILE_PATH, 'utf-8').split(' ');
  return keys[0] === '' ? [] : keys;
}

// 把a(bǔ)pi-key寫入到文件里
const addKeyToFile = (apiKey, content = '')=> {
  return new Promise(async resolve=> {
    const writerStream = fs.createWriteStream(KEY_FILE_PATH);
    // 使用 utf8 編碼寫入數(shù)據(jù)
    writerStream.write(content + apiKey,'UTF8');

    // 標(biāo)記文件末尾
    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);
    });
  })
}

// 顯示文件內(nèi)的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
}

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI