资讯专栏INFORMATION COLUMN

在NPM发布自己造的轮子

binaryTree / 1264人阅读

摘要:在发布自己造的轮子前言自从出现,它的好基友也是我们日常开发中必不可少的东西。

在NPM发布自己造的轮子 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,需要压缩的图片,所以我对这两个素材需要用到的一些操作进行了以下分析:

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

$ 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/ti...

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

最后贴上部分代码,内容过长,可以跳过哦

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 ", "压缩后的图片存放到指定路径(使用相对路径)")
.option("-a, --add ", "添加api-key")
.option("--delete ", "删除指定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("
正在检测,请稍候...");
            process.stdout.write("
正在检测,请稍候...");
            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
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109039.html

相关文章

  • 结合自己造的轮子实践按需加载

    摘要:原文地址为了探究按需加载的本质,选择了对先前造的轮子进行实验。下文就来揭开面纱,并动手改造项目,最终目标是用第二种写法实现按需加载,减小打包体积。下面给出种可以按需加载的方案。 原文地址 为了探究按需加载的本质,选择了对先前造的轮子 diana 进行实验。 实验一:全量引用 import * as _ from diana 打包体积结果如下: showImg(http://oqhtsc...

    Alfred 评论0 收藏0
  • 一年前端造的轮子是什么样子?

    摘要:起因工作也差不多满一年了,对于基本的业务开发有了一些自己的想法刚开始工作的前个月,每天都可以接触到新东西,接触新业务个月之后业务开发熟悉了对于自己的技术成长就感觉受到了局限如果一直没有作出改变,那么等于是个月的经验要用一年我的学习方式就是多 起因 工作也差不多满一年了,对于基本的业务开发有了一些自己的想法 刚开始工作的前3个月,每天都可以接触到新东西,接触新业务 3个月之后业务开发熟...

    szysky 评论0 收藏0
  • 那些年造的轮子,我们该为谁树墓碑?

    摘要:为此,玉伯当时还特意发了一条微博,说是应该给和也树一块墓碑了。这里,闰土所说的过时,并不是指它现在就不能用了,而是说出现了明显更加先进的理念或者标准,这会导致未来它的使用场景大为减少,整体趋势已经步入衰落。 showImg(https://segmentfault.com/img/bVYQLf?w=700&h=392); 前言 都已经2017年的11月份了,我们项目还打算用seajs?...

    vspiders 评论0 收藏0
  • 轮子 - EGGJS的MySQL操作库

    摘要:最近学习,学习过程中使用官方推荐的库,感觉官方库不太好用,基础的没问题。介绍这个轮子其实是很早以前就造好的,主要参考的数据库操作方式。将设置表名设置查询字段联表等操作进行链式操作,给人一种语义化操作数据库的感觉。 最近学习eggjs,学习过程中使用官方推荐的MySQL库,感觉官方库不太好用,基础的CURD没问题。但是复杂点的操作就不行了,虽然官方还有一个egg-sequelize,但是...

    Alex 评论0 收藏0

发表评论

0条评论

binaryTree

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<