在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 相关文章推荐
jQuery入门知识简介
Mar 04 Javascript
js图片跟随鼠标移动代码
Nov 26 Javascript
老生常谈遮罩层 滚动条的问题
Apr 29 Javascript
js实现碰撞检测特效代码分享
Oct 16 Javascript
BootStrap tooltip提示框使用小结
Oct 26 Javascript
JavaScript动态检验密码强度的实现方法
Nov 09 Javascript
js放大镜放大购物图片效果
Jan 18 Javascript
把JavaScript代码改成ES6语法不完全指南(分享)
Sep 10 Javascript
ES6 系列之 Generator 的自动执行的方法示例
Oct 19 Javascript
JavaScript隐式类型转换代码实例
May 29 Javascript
实现一个Vue自定义指令懒加载的方法示例
Jun 04 Javascript
JavaScript如何判断对象有某属性
Jul 03 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
ThinkPHP3.2.3实现分页的方法详解
2016/06/03 PHP
SAE实时日志接口SDK用法示例
2016/10/09 PHP
PHP基于phpqrcode类生成二维码的方法详解
2018/03/14 PHP
PHP实现百度人脸识别
2019/05/06 PHP
javascript indexOf函数使用说明
2008/07/03 Javascript
JavaScript常用对象的方法和属性小结
2012/01/24 Javascript
使用Math.floor与Math.random取随机整数的方法详解
2013/05/07 Javascript
编写简单的jQuery提示插件
2014/12/21 Javascript
js对象的复制继承实例
2015/01/10 Javascript
javascript与Python快速排序实例对比
2015/08/10 Javascript
jquery实现具有嵌套功能的选项卡
2016/02/12 Javascript
AngularJS基础 ng-include 指令示例讲解
2016/08/01 Javascript
angularJs提交文本框数据到后台的方法
2018/10/08 Javascript
layui.use模块外部使用其内部定义的js封装函数方法
2019/09/16 Javascript
微信小程序工具函数封装
2019/10/28 Javascript
js实现删除json中指定的元素
2020/09/22 Javascript
微信小程序自定义支持图片的弹窗
2020/12/21 Javascript
Python封装shell命令实例分析
2015/05/05 Python
小白如何入门Python? 制作一个网站为例
2018/03/06 Python
python实现根据文件关键字进行切分为多个文件的示例
2018/12/10 Python
详解Python下载图片并保存本地的两种方式
2019/05/15 Python
在Pycharm中使用GitHub的方法步骤
2019/06/13 Python
python hash每次调用结果不同的原因
2019/11/21 Python
python用opencv完成图像分割并进行目标物的提取
2020/05/25 Python
Python 捕获代码中所有异常的方法
2020/08/03 Python
利用CSS3制作简单的3d半透明立方体图片展示
2017/03/25 HTML / CSS
phpquery中文手册
2021/03/18 PHP
实习生自我鉴定范文
2013/12/05 职场文书
学习张林森心得体会
2014/09/10 职场文书
房屋维修协议书范本
2014/09/25 职场文书
学校组织向国旗敬礼活动方案(中小学适用)
2014/09/27 职场文书
初中家长评语和期望
2014/12/26 职场文书
爱护环境卫生倡议书
2015/04/29 职场文书
交通事故被告代理词
2015/05/23 职场文书
职场中的你,辞职信写对了吗?
2019/06/26 职场文书
Python绘制分类图的方法
2021/04/20 Python