目录
- 项目简介
- 源码分析
- 动手实现
- 总结
1、项目简介
update-notifier是yeoman团队出品的,之所以选择这个项目,主要是大佬推荐,其次是,项目体积只有15.3kB,功能也不复杂,在github上目前有1.6k star,比较适合当作入门源码。
update-notifier用于检查包的当前版本是否是最新版本,如果不是,则提示更新。
这个项目的使用场景一般是脚手架应用,可以在npm主页上看到有哪些项目用到了update-notifier:
我去考证了一下,实际上npm只在早期版本用了update-notifier,现在的最新版已经没有用到了,究其原因,我猜可能是因为这个项目还有很多问题没有解决,不知道是团队已经打算放弃了还是啥,反正现在还开着20来个issue,上一次更新是8个月前了。
没关系,反正我们就是要看它的源码,只不过不能一味吸收,也要分辨出其中已过时的东西,辩证地看。
话不多说,开撕源码。
2、源码分析
2.1 package.json
package.json里蕴含了包的很多信息。一般npm项目都可以直接从这个文件开始入手。
update-notifier的package.json主要关注下面这几个信息:
- files:可选配置项,文件数组,指明了当项目作为依赖包被安装时要包含哪些文件或文件夹
- dependencies:依赖包
- devDependencies:开发环境的依赖包
files
- index.js:默认入口文件
- check.js
dependencies
- boxen:一个能在控制台打印出方框的工具
- chalk:给控制台的字体添加颜色
- configstore:一个加载配置的工具,会在用户配置目录下生成对应的json文件,并保存在$XDG_CONFIG_HOME 或 ~/.config.目录下,如C:\Users\用户名\.config\configstore\
- has-yarn:检查是否有安装工具yarn
- import-lazy:懒加载引入依赖包
- is-ci:检测当前环境是否为 CI 服务器(持续集成服务器)
- is-installed-globally: 检查依赖包是否是npm全局安装的
- is-npm:判断是否是作为npm或yarn的脚本命令来运行
- is-yarn-global:检查包是否通过yarn全局安装的
- latest-version:获取依赖包的最新版本信息
- pupa:模板字符串工具
- semver:语义化版本工具
- semver-diff:版本比较工具
- xdg-basedir:linux平台下的,获取 XDG 基本目录路径的工具
devDependencies
- ava:测试工具
- clear-module:清除模块缓存
- fixture-stdout:截获控制台的输入
- mock-require:模拟引入node.js模块,已弃用
- strip-ansi:去掉字符串中的ascii转义字符
- xo:基于eslint的强制代码格式规范工具
2.2 文档
我们先看一下update-notifier文档里的API
有哪些:
- notifier = updateNotifier(options),实例化对象
- options:配置选项
- pkg:包信息
- name:包名
- version:包的版本
- name:包名
- updateCheckInterval:更新时间间隔
- shouldNotifyInNpmScript:允许在脚本运行时通知
- distTag:定义最新版本指向的是哪个版本,默认是’latest’
- pkg:包信息
- options:配置选项
- notifier.fetchInfo():获取版本更新信息的方法,包括最新版本号latest、当前版本号current、当前包的类型type、包名name
- notifier.notify(options?):输出更新提示的方法,
- options:可选配置对象
- defer:当为true时,会等进程退出后再提示
- message:更新提示的信息,可配置的字段有包名packageName、当前版本currentVersion、最新版本latestVersion、更新命令updateCommand(比如npm或yarn)
- isGlobal:提示的时候,是否提示使用
-g
参数npm全局安装(文档中漏了个isYarnGlobal,是yarn的全局安装提示) - boxenOptions:提示文本的边框样式
- options:可选配置对象
- –no-update-notifier:node运行时参数,当加上这个参数时不会提示更新
- NODE_ENV:设置process.env.NODE_ENV为
test
时不会提示更新 - 当前环境为CI服务器时不会提示更新。
2.3 例子example
先下载源码,再npm i
或yarn
安装依赖包,源码中有个example.js
文件,运行node example
,你就会发现什么:
什么都没发生。
运行了个寂寞的我再看了一下example.js里的注释,于是看到了这么一句:
// You have to run this file two times the first time
// This is because it never reports updates on the first run
// If you want to test your own usage, ensure you set an older version
翻译一下就是:
你必须运行两次,因为第一次运行肯定是看不到更新的;
如果你想测试你自己的包,确保你的包版本号不是最新的,这样才能看到控制台打印出更新信息。
早说嘛!
一二三四再来一次:
哎,果然有了。
这是为什么?
这个问题先拿笔记本记下,待会儿分析源码的时候再说。
可以看到example.js里引入了updateNotifier
后,调用了实例化方法;这里检查了一个叫public-ip
的包,当前版本是0.9.2
,运行后提示我们更新到4.0.4
。
我们也可以检查一下其他包,比如把name的值改成vue
,再运行两下,就会看到提示我们更新vue版本:
2.4 index.js
package.json中如果没有配置main属性,则默认index.js是项目的入口文件。所以打开index.js:
这是CommonJS规范导出模块的写法,module.exports
导出了UpdateNotifier
类和实例化方法。
看看UpdateNotifier
类里有啥:
- constructor:构造函数
- check:检查是否需要更新
- fetchInfo:异步方法,获取最新版本的信息
- notify:在控制台输出提示的方法
这里只大致看了下方法名、入参、出参,再结合说明文档,半看半猜每个方法的作用,不一定是正确的;
不过错了也没关系,接下来我们会深入看每一个方法的实现细节,如果猜错了最后看完再回来改下就行。
上一节我们通过example知道,使用的是updateNotifier实例化方法,所以我们看看实例化方法具体做了啥:
module.exports = options => {
const updateNotifier = new UpdateNotifier(options);
updateNotifier.check();
return updateNotifier;
};
这三行代码总结起来就是:
new
一个UpdateNotifier
对象,再调用check
方法,最后返回实例化对象。
来,一句句拆解。
2.4.1 构造函数constructor
当new一个对象的时候发生了啥?
- 创建一个船新的空JS对象(即
{}
); - 然后给这个空对象添加
__proto__
属性,并将该属性链接至构造函数的原型对象,这句话意思如下:
const B=new A();
B.__prototype__===A.prototype;// true`
- 再将构造函数的this作用域指向这个对象;
- 最后,执行构造函数constructor,当构造函数没有返回值时,返回这个new出来的对象;如果构造函数有返回值,则返回构造函数的返回值。
懂了,所以当我们调用实例化方法的时候,就会执行构造函数。
update-notifier的构造函数里做了下面这些事:
- 将入参pkg对象中的name和version,保存到this.options中;
- 创建configstore实例对象this.config,并保存当前更新时间;
configstore会在用户配置路径~/.config/configstore/
下生成一个update-notifier-[包名].json文件。这个文件内容如下:
- 如果你手动把其中的optOut值改为true,程序就不会再提示更新;
- lastUpdateCheck是最近一次更新的时间戳;
- update对象是版本更新信息。
当然,除此之外,构造函数还做了:
- 必要的参数校验,比如name和version的非空校验;
- 可选配置项的初始化默认值,如更新间隔updateCheckInterval的默认初始化为1天
- 检查环境配置,看当前的NODE_ENV是否为test、运行时参数是否有–no-update-notifier、是否为CI服务器,如果有一项值为是,则不提示更新。
2.4.2 check
首先根据配置项中的参数判断是否需要更新,如果需要更新,再接着往下:
this.update = this.config.get('update');
这里会去取上述json文件中的update对象,如果有update对象,会先把json文件中的update删除,然后开启一个子进程去执行check.js文件中的脚本:
// 开启子进程去执行check.js
spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
detached: true,
stdio: 'ignore'
}).unref(); // unref可以让父进程不用等待子进程退出再退出
check.js只做了一件事:
调用fetchInfo()
异步方法,如果当前版本不是最新版本,就会更新本地json文件中的update对象:
const update = await updateNotifier.fetchInfo();
if (update.type && update.type !== 'latest') {
updateNotifier.config.set('update', update);
}
🍉注意,这里fetchInfo
是异步的,所以执行完check
后,没办法立即更新json文件中的update对象,这就导致了,example.js中调用notify()
方法时,第一次的时候取不到update对象的值(理论上,只要你手速够快,不止第一次取不到,在fetchInfo回调之前你都是取不到的)。
看完check()
,我们再来看看notify()
方法做了啥。
2.4.3 notify
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
return this;
}
开头的这个if
判断,决定了是否要提示更新,以下几种情况,都不会输出提示:
- !process.stdout.isTTY:没有输出终端
- suppressForNpm:如果是作为npm或yarn脚本运行时但配置了不允许提示
- !this.update:没有获取到update对象
- !semver().gt(this.update.latest, this.update.current):当前版本号>=最新版本号
看出问题来没?如果本地json文件中没有update对象,那么上一步check
方法中的this.update
就取不到值,这里就不会提示更新。
并且,这里的更新提示信息,实际是上一次请求的结果,而不是实际上的最新版本。
比如我们手动把json文件中的latest
值改为6.6.666
,然后执行node example
,你就会发现打印出来的是6.6.666:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M2wMYrja-1633144246700)(https://files.mdnice.com/user/14530/800ff4b1-a0d7-42e0-81c1-9e207b1882e5.png)]
现在你明白了为啥第一次运行的时候什么都没有了吧?
第一次运行的时候,子进程的回调还没结束,json文件中的update对象还没有值,因此
check()
方法中的this.update
取不到值,接着notify()
判断this.update
时值为undefined
,所以直接就return this
了。
我给源码加上了注释,欢迎勘误,仅供参考:
https://github.com/youzouzou/update-notifier
3、动手实现
总结下update-notifier的运行流程:
核心功能总结:
拉取最新版本信息,保存到本地json文件,以便下一次运行的时候取出信息,与当前版本号进行对比。如果当前版本号小于最新版本,就在控制台输出提示信息。
核心功能用到的包实际主要就三个:last-version
、semver
、configstore
,其他都是一些样式或者辅助判断环境的工具包。
模仿这个核心思路,我写了一个精简版本,只实现了核心功能:
用last-version
异步拉取最新版本信息,在回调函数中用semver
比较当前版本是否小于最新版本,若是,则直接在控制台输出更新提示。
我没有用configstore存储上一次的请求结果,也没有开子进程,而是等回调结束后再直接输出更新信息,所以每一次运行都会去拉取实际的最新版本信息。
实现代码如下:
const latestVersion = require('latest-version'); // 用于获取最新版本的 npm 包
const semver = require('semver'); // 语义化版本工具
class MyUpdateNotifier {
constructor(options) {
this.check(options)
}
async check(options) {
const { name, version } = options;
if (!name) {
console.error("未获取到包名");
return;
}
if (!version) {
console.error("未获取到当前版本");
return;
}
const latest = await latestVersion(name); // 获取到最新版本号
console.log(name + "最新版本号", latest)
if (semver.gt(latest, version)) { // 最新版本号是否大于当前版本
console.log("请把" + name + "从" + version + "更新到最新的" + latest)
}
}
}
module.exports = MyUpdateNotifier;
测试一下:
成功!✅
Demo地址:https://github.com/youzouzou/my-update-notifier
运行的时候会发现,有略明显的卡顿,这是因为我没有开子进程,而update-notifier虽然更新信息不一定是实际最新的,但却不会阻塞父进程的运行,用起来会比较丝滑。
4、总结
之前虽然也零散地看了些源码,但是并没有看完,也没有写分析笔记,这算是我第一次完整读完一个项目的源码。
这次的阅读体验还是很愉快的,update-notifier项目文档完善,代码逻辑清晰,难度也不大,看完不仅了解了实现原理,也涨了很多姿势,比如各种实用工具包,node子进程用法等等。