在NPM发布自己造的轮子的方法步骤


Posted in Javascript onMarch 09, 2019

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 login
npm publish
+ pkgname@1.0.0 // 成功

这时,你在npm上面搜你的包名,你写在package.json 的信息都会被解析,然后你的包的页面介绍内容就是你的README.md

3、写这个包

包初始化好了之后,我们就可以开始写这个包了

对于这个压缩工具来说,要用到的素材只有两个,tinypng接口要用到的 api-key,需要压缩的图片,所以我对这两个素材需要用到的一些操作进行了以下分析:

在NPM发布自己造的轮子的方法步骤

我的初衷是想把这个命令写的尽量简单,让我可以联想到压缩图片=简单,所以我待定了整个包只有一个单词就能跑,是这样:

$ tinyhere

其他的操作都放在子命令和可选项上。

然后开始划分项目结构

在NPM发布自己造的轮子的方法步骤

大致上是这样,把全局命令执行的 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 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
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript实现仿银行密码输入框效果的代码
Dec 13 Javascript
javascript数组的扩展实现代码集合
Jun 01 Javascript
IE Firefox 使用自定义标签的区别
Oct 15 Javascript
javascript中数组的冒泡排序使用示例
Dec 18 Javascript
jquery ajax跨域解决方法(json方式)
Feb 04 Javascript
javascript中String对象的slice()方法分析
Dec 20 Javascript
深入理解Node.js的HTTP模块
Oct 12 Javascript
微信小程序 视图层(xx.xml)和逻辑层(xx.js)详细介绍
Oct 13 Javascript
利用node.js实现自动生成前端项目组件的方法详解
Jul 12 Javascript
使用vue-router切换页面时,获取上一页url以及当前页面url的方法
May 06 Javascript
js常用正则表达式集锦
May 17 Javascript
vue中监听返回键问题
Aug 28 Javascript
使用pm2部署node生产环境的方法步骤
Mar 09 #Javascript
Koa日志中间件封装开发详解
Mar 09 #Javascript
详解vue2.6插槽更新v-slot用法总结
Mar 09 #Javascript
Node.js Stream ondata触发时机与顺序的探索
Mar 08 #Javascript
详解JSON和JSONP劫持以及解决方法
Mar 08 #Javascript
Node.js Event Loop各阶段讲解
Mar 08 #Javascript
vue基础之data存储数据及v-for循环用法示例
Mar 08 #Javascript
You might like
PHP编码规范-php coding standard
2007/03/16 PHP
深入解析PHP的引用计数机制
2013/06/14 PHP
PHP实现读取一个1G的文件大小
2013/08/24 PHP
PHP实现支持SSL连接的SMTP邮件发送类
2015/03/05 PHP
PHP实现链式操作的核心思想
2015/06/23 PHP
实现PHP框架系列文章(6)mysql数据库方法
2016/03/04 PHP
jQuery 选择器理解
2010/03/16 Javascript
理解Javascript_02_理解undefined和null
2010/10/11 Javascript
jquery实现未经美化的简洁TAB菜单效果
2015/08/28 Javascript
JavaScript中创建对象的模式汇总
2016/04/19 Javascript
Bootstrap CSS组件之大屏幕展播
2016/12/17 Javascript
谈谈Vue.js——vue-resource全攻略
2017/01/16 Javascript
bootstrap PrintThis打印插件使用详解
2017/02/20 Javascript
JS中touchstart事件与click事件冲突的解决方法
2018/03/12 Javascript
vue项目或网页上实现文字转换成语音播放功能
2020/06/09 Javascript
vue router-link 默认a标签去除下划线的实现
2020/11/06 Javascript
[42:06]2019国际邀请赛全明星赛 8.23
2019/09/05 DOTA
Python获取Linux系统下的本机IP地址代码分享
2014/11/07 Python
python中的__slots__使用示例
2015/02/26 Python
编写Python CGI脚本的教程
2015/06/29 Python
Python数据集切分实例
2018/12/08 Python
详解10个可以快速用Python进行数据分析的小技巧
2019/06/24 Python
python基于pdfminer库提取pdf文字代码实例
2019/08/15 Python
tensorflow指定CPU与GPU运算的方法实现
2020/04/21 Python
Python fileinput模块如何逐行读取多个文件
2020/10/05 Python
css3实现文字扫光渐变动画效果的示例
2017/11/07 HTML / CSS
美国知名日用品连锁超市:Dollar General(多来店)
2017/01/14 全球购物
欧洲顶级的童装奢侈品购物网站:Bambini Fashion(面向全球)
2018/04/24 全球购物
印尼综合在线预订网站:Tiket.com(机票、酒店、火车、租车和娱乐)
2018/10/11 全球购物
"火柴棍式"程序员面试题
2014/03/16 面试题
荷叶圆圆教学反思
2014/02/01 职场文书
2014年人事专员工作总结
2014/11/19 职场文书
2015秋季开学典礼致辞
2015/07/16 职场文书
2015年公路路政个人工作总结
2015/07/24 职场文书
2016党性教育学习心得体会
2016/01/21 职场文书
员工保密协议范本,您一定得收藏!很有用!
2019/08/08 职场文书