Node.js 使用命令行工具检查更新


Posted in Javascript onJune 08, 2017

随着 Node.js 的“走红”,使用 Node.js 开发命令行工具越来越简单。一个成熟的命令行工具应该从一开始就要考虑好之后的版本更新如何“优雅”的告知用户。最好的方法当然是当用户在终端执行命令时,将相关信息提示给用户。

这篇文章将给出一个易用、高效、可定制的方法。源码在这里: GITHUB ,欢迎大家顺手点赞。接下来我将讲解其实现思路。

使用

我们先简单看看这个 npm 包的使用方法:

const updater = require('pkg-updater');
const pkg = require('./package.json'); // 命令行工具自己的 package 信息

updater({'pkg': pkg}) .then(() => { /* 在这里启动命令行工具 */ });

updater({
 'pkg': pkg, 
 // 自定义 registry
 'registry': 'http://xxx.registry.com',
 // 自定义请求的 dist-tag,默认是 latest
 'tag': 'next',
 // 自定义检查间隔,默认是 1h
 'checkInterval': 24 * 60 * 60 * 1000,
 // 自定义更新提示信息
 'updateMessage': 'package update from <%=current%> to <%=latest%>.',
 // 自定义强制更新的版本更新级别,默认是 major
 'level': 'minor'
}).then(() => { /* 在这里启动命令行工具 */ });

updater({
 'pkg': pkg,
 // 完全自定义版本更新时的逻辑
 'onVersionChange': function* (opts) {

 }
}).then(() => { /* 在这里启动命令行工具 */ });

效果如图:

Node.js 使用命令行工具检查更新

Node.js 使用命令行工具检查更新

实现

使用方法很简单,我们一起来看看其实现方法。

需求

我们先来梳理下需求,一个命令行检查更新器应该至少提供如下功能:

能从远程获取最新版本

能根据检查结果进行提示

在版本不兼容时可以直接退出,强制用户升级程序

获取版本

获取最新版本这个功能看起来很简单,就是发送一个请求从“某处”获取信息。但是有一些问题需要我们考虑:

从哪里获取版本信息?

获取版本信息的策略是怎样的?(什么时候获取?获取的信息如何处理?)

从哪里获取版本信息

我们的命令行工具一般都是使用 npm 进行分发,最简便的方法就是直接通过 registry 获取。通过请求 https://registry.npmjs.org/{name}/{dist-tag} 就可以得到 package 对应 tag 的版本信息。结果类似下面这样:

// https://registry.npmjs.org/co/latest
{
 "name": "co",
 "version": "4.5.0"
}

在实际实现时,我们应该允许调用者自定义 registry 地址、请求的 dist-tag 等,这样可以有更多的定制性。

获取版本信息的策略

首先想到的方法是用户每次执行命令时都去获取一次版本信息,这样的获取策略应该是最简单和实时的。

但是这个策略其实并不合适:

每次执行命令都要去发请求进行检查,如果网络延迟,会阻塞命令执行,影响用户体验

工具的版本更新其实并不会很频繁,没有必要进行实时检查

网络请求的影响因素很多,不能保证每次都成功,应该提供本地缓存机制来存储请求成功的结果,避免版本信息的不可用

综合上面的几点,我们设计如下的获取策略:

将发送网络请求获取版本信息的逻辑放在一个独立的后台进程去执行,保证不阻塞主命令执行

请求成功后将版本信息、检查时间缓存到用户机器

每次执行命令时,只是读取本地缓存下来的版本信息,不去发送网络请求

根据缓存下来的检查时间和当前时间,在一个间隔之内不去额外创建后台检查进程

将上面的策略翻译成代码大概就是下面这样:

// 读取本地缓存的检查结果
const checkInfo = yield updater.readCheckInfo(opts);
const lastCheck = checkInfo.lastCheck;
const lastVersion = checkInfo.lastVersion;

// 根据版本信息提示用户
// ...

// 在时间间隔内,直接返回
if (Date.now() - lastCheck < opts.checkInterval) {
 return;
}

// 创建后台检查进程
try {
 require('child_process').spawn(
 process.execPath,
 [require('path').join(__dirname, '_check.js'), JSON.stringify({
  'pkg': opts.pkg, // package 信息
  'tag': opts.tag, // 检查的 dist-tag
  'logFile': opts.logFile, // 缓存文件路径
  'registry': opts.registry // registry 地址
 })],
 {'stdio': ['ignore', 'ignore', 'ignore'], 'detached': true}
 ).unref();
} catch(e) {}

后台进程执行的 _check.js 文件也很简单,如下所示:

const opts = JSON.parse(process.argv[2]);

let lastVersion = '';
try {
 // 发送请求获取最新版本
 const url = normalizeUrl(opts.registry + '/' + opts.pkg.name + '/' + (opts.tag || 'latest'));
 const res = yield got.get(url, {
 'json': true,
 'timeout': 60 * 1000
 });
 if (res && res.body && res.body.version) {
 lastVersion = res.body.version;
 }
} catch(e) {}

// 如果获取失败了,最新版本就是当前版本(package.version)
if (!lastVersion) {
 lastVersion = opts.pkg.version;
}

let data = yield util.readJson(opts.logFile);
if (!data[opts.pkg.name]) {
 data[opts.pkg.name] = {};
}
data[opts.pkg.name].lastVersion = lastVersion; // 最新版本
data[opts.pkg.name].lastCheck = Date.now(); // 检查时间

// 写入缓存
yield util.writeJson(opts.logFile, data);

提示

当版本更新了,我们应该在终端提示用户。这里有两个问题:

提示文案的问题

提示文案显示间隔的问题(一直显示?每隔一段时间显示?)

这里我们采取的策略是:

提供默认提示文案,清晰的说明当前版本、最新版本、更新方法,允许调用者自定义提示文案

只要有更新就一直显示提示文案,因为我们希望用户经常的进行更新

实现代码大概如下:

// 比对版本
const type = updater.diffType(opts.pkg.version, lastVersion, opts.level);
if (type) {
 // 根据模板渲染提示信息
 const str = updater.template(opts.updateMessage || updater.defaultOpts.updateMessage)({
 'colors': updater.colors,
 'name': opts.pkg.name,
 'current': opts.pkg.version,
 'latest': opts.lastVersion,
 'command': 'npm i -g ' + opts.pkg.name
 });
 // 进行提示
 console.log(
 updater.boxen(str, {
  'padding': 1,
  'margin': 1,
  'borderStyle': 'classic'
 })
 );
}

强制升级

对于 npm 模块来说,版本 a.b.c 的更新一般有三种情况:

patch:c 位,小版本更新,一般是 bug 修复

minor:b 位,中版本更新,一般增加新功能、bug 修复

major,a 位,大版本更新,一般是不兼容的升级

我们希望当远程版本的更新如果是 major 形式,命令行工具将直接退出,强制用户进行升级后才能使用。这可以保证我们推送一个大版本后,所有的用户都能够马上更新掉,而不是继续使用老版本,造成版本碎片的问题。

实现代码大致如下:

// 比对版本
const type = updater.diffType(opts.pkg.version, lastVersion, opts.level);
if (type) {
 // 根据模板渲染提示信息
 const str = updater.template(opts.updateMessage || updater.defaultOpts.updateMessage)({
 'colors': updater.colors,
 'name': opts.pkg.name,
 'current': opts.pkg.version,
 'latest': opts.lastVersion,
 'command': 'npm i -g ' + opts.pkg.name
 });
 // 进行提示
 console.log(
 updater.boxen(str, {
  'padding': 1,
  'margin': 1,
  'borderStyle': 'classic'
 })
 );

 // 不兼容的更新,直接让进程退出
 if (type == 'incompatible') {
 process.exit(1);
 }
}

总结

命令行检查更新看似简单,其实仔细思考,还是有很多细节。希望这篇文章对你有所启发。

Javascript 相关文章推荐
JS对象转换为Jquery对象示例
Jan 26 Javascript
JSON中双引号的轮回使用过程中一定要小心
Mar 05 Javascript
用js的document.write输出的广告无阻塞加载的方法
Jun 05 Javascript
javascript面向对象之定义成员方法实例分析
Jan 13 Javascript
浅谈angular.js中实现双向绑定的方法$watch $digest $apply
Oct 14 Javascript
基于Bootstrap+jQuery.validate实现表单验证
May 30 Javascript
微信小程序 教程之数据绑定
Oct 18 Javascript
Node.js中process模块常用的属性和方法
Dec 13 Javascript
教你5分钟学会用requirejs(必看篇)
Jul 25 Javascript
ES6 如何改变JS内置行为的代理与反射
Feb 11 Javascript
微信小游戏中three.js离屏画布的示例代码
Oct 12 Javascript
vue使用require.context实现动态注册路由
Dec 25 Vue.js
在vue.js中抽出公共代码的方法示例
Jun 08 #Javascript
Ionic3 UI组件之autocomplete详解
Jun 08 #Javascript
jQuery+ajax实现局部刷新的两种方法
Jun 08 #jQuery
gulp解决跨域的配置文件问题
Jun 08 #Javascript
angular 用拦截器统一处理http请求和响应的方法
Jun 08 #Javascript
jQuery 添加样式属性的优先级别方法(推荐)
Jun 08 #jQuery
Ionic项目中Native Camera的使用方法
Jun 07 #Javascript
You might like
PHP中foreach循环中使用引用要注意的地方
2011/01/02 PHP
PHP文件大小格式化函数合集
2014/03/10 PHP
PHP递归遍历文件夹去除注释并压缩php源代码的方法示例
2018/05/23 PHP
JQuery为textarea添加maxlength属性的代码
2010/04/07 Javascript
ion content 滚动到底部会遮住一部分视图的快速解决方法
2016/09/06 Javascript
js日期相关函数dateAdd,dateDiff,dateFormat等介绍
2016/09/24 Javascript
Jquery和Js获得元素标签名称的方法总结
2016/10/08 Javascript
基于JS实现9种不同的面包屑和分布式多步骤导航效果
2017/02/21 Javascript
微信小程序封装http访问网络库实例代码
2017/05/24 Javascript
微信小程序实现动态显示和隐藏某个控件功能示例
2018/12/14 Javascript
[05:46]DOTA2英雄梦之声_第18期_陈
2014/06/20 DOTA
Python中用函数作为返回值和实现闭包的教程
2015/04/27 Python
代码讲解Python对Windows服务进行监控
2018/02/11 Python
Python针对给定列表中元素进行翻转操作的方法分析
2018/04/27 Python
基于Django的乐观锁与悲观锁解决订单并发问题详解
2019/07/31 Python
在Django下创建项目以及设置settings.py教程
2019/12/03 Python
iPython pylab模式启动方式
2020/04/24 Python
DjangoWeb使用Datatable进行后端分页的实现
2020/05/18 Python
Python Map 函数的使用
2020/08/28 Python
使用before和:after伪类制作css3圆形按钮
2014/04/08 HTML / CSS
美国著名珠宝品牌之一:Jared The Galleria Of Jewelry
2016/10/01 全球购物
美国汽车性能部件和赛车零件网站:Vivid Racing
2018/03/27 全球购物
随机分配座位,共50个学生,使学号相邻的同学座位不能相邻
2014/01/18 面试题
大学生求职简历的自我评价
2013/10/14 职场文书
环境工程毕业生自荐信
2013/11/17 职场文书
感恩老师的演讲稿
2014/05/06 职场文书
求职信格式要求
2014/05/23 职场文书
2014年学校党建工作汇报材料
2014/11/02 职场文书
钳工实训报告总结
2014/11/04 职场文书
2014年财务个人工作总结
2014/12/08 职场文书
保护校园环境倡议书
2015/04/28 职场文书
导游词之峨眉山
2019/12/16 职场文书
css 边框添加四个角的实现代码
2021/10/16 HTML / CSS
Mysql排序的特性详情
2021/11/01 MySQL
阿里云 Windows server 2019 配置FTP
2022/04/28 Servers
Python中np.random.randint()参数详解及用法实例
2022/09/23 Python