自从Node.js出现,它的好基友npm(node package manager)也是我们日常开发中必不可少的东西。npm让js实现了模块化,使得复用其他人写好的模块(搬砖)变得更加方便,也让我们可以分享一些自己的作品给大家使用(造轮子),今天这里我就给大家分享一个用命令行压缩图片的工具,它的用法大致是这样的:
// 全局安装后,在图片目录下,运行这行
$ tinyhere
这样就把文件夹内的图片进行压缩。这里压缩采用的是 tinypng 提供的接口,压缩率大致上是50%,基本可以压一半的大小。以前在写项目的时候,测试验收完成后总是要自己手动去压一次图片,后来想把这个枯燥重复的事自动化去完成(懒),但是公司脚手架又没有集成这个东西,就想自己写一个轮子做出来用用就好了。它的名字叫做tinyhere,大家可以去安装使用试一下
$ npm i tinyhere -g
如果要写一个模块发布到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
包初始化好了之后,我们就可以开始写这个包了
对于这个压缩工具来说,要用到的素材只有两个,tinypng接口要用到的 api-key,需要压缩的图片,所以我对这两个素材需要用到的一些操作进行了以下分析:
我的初衷是想把这个命令写的尽量简单,让我可以联想到压缩图片=简单,所以我待定了整个包只有一个单词就能跑,是这样:
$ tinyhere
其他的操作都放在子命令和可选项上。
然后开始划分项目结构
大致上是这样,把全局命令执行的 tinyhere 放在bin目录下,然后subCommand负责提供操作函数,然后把可复用的函数(比如读写操作)抽离出来放在util上,比较复杂的功能单独抽离成一个文件,比如compress,然后导出一个函数给subCommand。至于存放用户的api-key,就存放在data下面的key里。
tinyhere的执行文件就负责解析用户的输入,然后执行subCommand给出的对应函数。
压缩图片的这个包的过程是这样的:
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了
在这里,我想跟大家说,如果你做了一个你觉得很酷的东西,也想给更多的人去使用,来让它变得更好,选择发布在NPM上面就是一个非常好的途径,看了上面的内容你会发现分享其实真的不难,你也有机会让世界看到属于你的风采!
如果大家觉得我有哪里写错了,写得不好,有其它什么建议(夸奖),非常欢迎大家补充。希望能让大家交流意见,相互学习,一起进步! 我是一名 19 的应届新人,以上就是今天的分享,新手上路中,后续不定期周更(或者是月更哈哈),我会努力让自己变得更优秀、写出更好的文章,文章中有不对之处,烦请各位大神斧正。如果你觉得这篇文章对你有所帮助,请记得点赞或者品论留言哦~。
欢迎大家提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 <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
}
作者:Croc_wend
https://segmentfault.com/a/1190000018439337
下面通过三种方法来搭建公司私有npm仓库,每种方式都有自己的优势。启动并配置服务、设置注册地址、登录cnpm、包上传到私有仓库、查看预览包、通过verdaccio搭建....
webpack:解析js文件,无法解析的文件需要借助loader,npm插件发布(vue&webpack&单页面):npm init =>package.json,创建.vue文件 =>插件界面及功能,index.js =>入口文件
NPM是随同NodeJS一起安装的包管理和分发工具,它很方便让JavaScript开发者下载、安装、上传以及管理已经安装的包。这篇文章整理NPM常用的一些命令
直接通过&&连接多条命令,在npm run start的时候,发现只停留在第一个命令执行监听,后面的命令都没有执行。只能通过打开多个窗口分别执行多条命令,那么有没有办法实现一条npm命令执行多条监听呢?
npm和bower太像了,就像一对孪生兄弟…… npm的文件是package.json,包安装的目录是node_modules。 bower的文件是bower.json,包安装的目录是bower_components。使用命令也基本一致
这篇文章主要介绍了vue项目中Npm run build 根据环境传递参数方法来打包不同域名,使用npm run build --xxx,根据传递参数xxx来判定不同的环境,给出不同的域名配置,具体内容详情大家参考下:config文件夹下dev.env.js中修改代码、prod.env.js中修改代码 HOST为截取到的参数
使用node.js和npm,在安装模块的时候报错npm WARN saveError ENOENT: no such file or directory, open ...的解决办法。这个原因就是因为项目没有进行初始化,缺少package.json文件造成的。需要package.json才能npm install。 可以npm init初始化生成一个package.json。
抽空写了一个 textarea,打算发布到 npm 的时候却遇到了问题,之前用 vue-cli 2.x 的时候,打包配置项非常透明,可以很容易的修改,但升级到 vue-cli 3.x 之后,反而一脸懵逼
如果您曾在 Node 或 JavaScript 前端开发中投入过时间和精力,那么您就知道 npm 中有数以十万计的模块可供您选择。挑选模块可能很难,但您只需要一些方法点来解决它。当您正在为如何抉择浪费时间,或者甚至不知道从哪里开始时,请使用本指南来帮助您。
关于 npm run eject 报错的问题,昨天新开React的项目, 刚刚一上手就碰到问题,解决方法(前提你有git).出错的原因:应该是git没有安装好,或者代码没有提交上git
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!