• 贵州省社科规划办与贵州日报社合作推出“文化贵州”专栏 2019-03-20
  • 就算不为了世界杯,俄罗斯也有那么多时髦好去处值得你飞去 2019-03-20
  • 山西十一选五任四遗漏:在NPM发布自己造的轮子的方法步骤

    山西体彩11选5直选遗漏 www.caxru.com  更新时间:2019年03月09日 10:16:36   作者:Croc_wend   我要评论

    这篇文章主要介绍了在NPM发布自己造的轮子的方法步骤,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

    1、前言

    自从Node.js出现,它的好基友npm(node package manager)也是我们日??⒅斜夭豢缮俚亩?。npm让js实现了??榛?,使得复用其他人写好的??椋ò嶙┍涞酶臃奖?,也让我们可以分享一些自己的作品给大家使用(造轮子),今天这里我就给大家分享一个用命令行压缩图片的工具,它的用法大致是这样的:

    // 全局安装后,在图片目录下,运行这行
    $ tinyhere

    这样就把文件夹内的图片进行压缩。这里压缩采用的是 tinypng 提供的接口,压缩率大致上是50%,基本可以压一半的大小。以前在写项目的时候,测试验收完成后总是要自己手动去压一次图片,后来想把这个枯燥重复的事自动化去完成(懒),但是公司脚手架又没有集成这个东西,就想自己写一个轮子做出来用用就好了。它的名字叫做tinyhere,大家可以去安装使用试一下

    $ npm i tinyhere -g

    2、npm简介

    如果要写一个??榉⒉嫉絥pm,那么首先要了解一下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
    + [email protected] // 成功

    这时,你在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

    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
    }

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

    相关文章

    • node.js中module.exports与exports用法上的区别

      node.js中module.exports与exports用法上的区别

      Node.js 引入了??椋∕odule)概念,一个??榭梢酝ü齧odule.exports 或 exports 将函数、变量等导出,以使其它 JavaScript 脚本通过require() 函数引入并使用。那么node.js中module.exports与exports有什么区别呢?下面小编给大家解答下
      2016-09-09
    • nodeJS微信分享

      nodeJS微信分享

      这篇文章主要为大家详细介绍了nodeJS微信分享的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
      2017-12-12
    • node错误处理与日志记录的实现

      node错误处理与日志记录的实现

      这篇文章主要介绍了node错误处理与日志记录的实现,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2018-12-12
    • 基于nodejs实现微信支付功能

      基于nodejs实现微信支付功能

      这篇文章主要为大家详细介绍了基于nodejs实现微信支付功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
      2017-12-12
    • node使用Koa2搭建web项目的方法

      node使用Koa2搭建web项目的方法

      本篇文章主要介绍了node使用Koa2搭建web项目的方法,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
      2017-10-10
    • connect中间件session、cookie的使用方法分享

      connect中间件session、cookie的使用方法分享

      今天大象哥用了下connect的session和cookie,感觉还挺好用的,分享一下(里面坑挺多的,文档写的太模糊了,费了哥不少时间)。
      2014-06-06
    • node.js中的path.extname方法使用说明

      node.js中的path.extname方法使用说明

      这篇文章主要介绍了node.js中的path.extname方法使用说明,本文介绍了path.extname的方法说明、语法、使用实例和实现源码,需要的朋友可以参考下
      2014-12-12
    • nodejs学习笔记之路由

      nodejs学习笔记之路由

      因为只是用于本地服务器用于自己测试用,所以不需要太完善的路由功能,所以也就不去使用express框架,而是自己实现一个简易路由,可以针对自己的需求来定制路由功能。
      2017-03-03
    • Node.js编写组件的三种实现方式

      Node.js编写组件的三种实现方式

      这篇文章主要介绍了Node.js编写组件的三种实现方式,包括纯js实现、v8 API实现(同步&异步)、借助swig框架实现,感兴趣的小伙伴们可以参考一下
      2016-02-02
    • async/await优雅的错误处理方法总结

      async/await优雅的错误处理方法总结

      这篇文章主要给大家介绍了关于async/await优雅的错误处理方法的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
      2019-01-01

    最新评论

  • 贵州省社科规划办与贵州日报社合作推出“文化贵州”专栏 2019-03-20
  • 就算不为了世界杯,俄罗斯也有那么多时髦好去处值得你飞去 2019-03-20