在NPM发布自己造的*的方法步骤
1、前言
自从node.js出现,它的好基友npm(node package manager)也是我们日常开发中必不可少的东西。npm让js实现了模块化,使得复用其他人写好的模块(搬砖)变得更加方便,也让我们可以分享一些自己的作品给大家使用(造*),今天这里我就给大家分享一个用命令行压缩图片的工具,它的用法大致是这样的:
// 全局安装后,在图片目录下,运行这行 $ tinyhere
这样就把文件夹内的图片进行压缩。这里压缩采用的是 提供的接口,压缩率大致上是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的
初始化完成之后,你就可以着手写这个包了,当你觉得你写好了之后,就可以发布到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/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 }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。