1. 场景
用于将当前 package.json
中的包与包管理工具库(如 npm
)进行比对,若存在可以更新的包,则进行提示。
2. 使用
1)安装
$ npm install update-notifier
2)使用
const updateNotifier = require('update-notifier');
const pkg = require('./package.json');
const notifier = updateNotifier({pkg});
notifier.notify();
console.log(notifier.update);
/*
{
latest: '1.0.1',
current: '1.0.0',
type: 'patch', // Possible values: latest, major, minor, patch, prerelease, build
name: 'pageres'
}
*/
3. 源码
1)阅读 readme.md
源码地址:https://github.com/yeoman/update-notifier
从 readme
中可了解到如下内容
-
可以运行根目录下的
example.js
进行调试 -
为了用户体验,
update-notifier
并不会每次运行都检查,而是会在 一定时间间隔后进行检查 -
update-notifier
检查会开一个子进程来进行,尽管调用process.exit
退出进程,也不会影响到检查
2)克隆源码仓库
# 克隆
git clone https://github.com/yeoman/update-notifier.git
# 安装依赖
cd update-notifier
npm install
3)运行调试
1)通过 package.json
可以看到,调试命令如下
npm run test
2)查看调试结果
首次运行时不会打印信息,只有在第二次开始才会提示更新信息。
这是因为 首次启动时找不到旧版本进行对比,因此会先将首次的信息持久化地存储起来,第二次执行时将上次存储的信息,与本次运行的结果进行对比。
4)源码解读
example.js
'use strict';
const updateNotifier = require('.');
// 将一个包信息传入
// 首次运行会将该包 0.9.2 版本存储起来
// 第二次运行会拿到 0.9.2 与现有库中最新的版本对比
updateNotifier({
pkg: {
name: 'public-ip',
version: '0.9.2'
},
// 检查的时间间隔
updateCheckInterval: 0
}).notify();
check.js
/* eslint-disable unicorn/no-process-exit */
'use strict';
let updateNotifier = require('.');
const options = JSON.parse(process.argv[2]);
updateNotifier = new updateNotifier.UpdateNotifier(options);
(async () => {
// 若运行超时,则退出进程
setTimeout(process.exit, 1000 * 30);
// 获取更新检查得到的包版本信息
const update = await updateNotifier.fetchInfo();
// 将当前时间作为最后一次检查更新时间
updateNotifier.config.set('lastUpdateCheck', Date.now());
// 如果当前包不是最新版本,则抛出提示
if (update.type && update.type !== 'latest') {
updateNotifier.config.set('update', update);
}
process.exit();
})().catch(error => {
console.error(error);
process.exit(1);
});
index.js
注:index.js 源码较多,以下会分成多个代码块进行解读
总代码流程个人理解为:
- 首次运行,提取出当前 package.json 中存储的所有包的版本信息,生成文件并进行持久化存储,记录下当前的时间
- 下次运行时,将当前时间与上一次生成文件的时间 比对,若超过检查间隔,则进行 udpate check。检查比对 npm 库,若发现包版本有可以更新的,则在控制台进行提示
跟随流程来阅读源码
1)引入工具包
'use strict';
const {spawn} = require('child_process');
const path = require('path');
const {format} = require('util');
// 懒加载模块:只有在调用到对应包的时候,再进行引入
const importLazy = require('import-lazy')(require);
const configstore = importLazy('configstore');
const chalk = importLazy('chalk');
const semver = importLazy('semver');
const semverDiff = importLazy('semver-diff');
const latestVersion = importLazy('latest-version');
const isNpm = importLazy('is-npm');
const isInstalledGlobally = importLazy('is-installed-globally');
const isYarnGlobal = importLazy('is-yarn-global');
const hasYarn = importLazy('has-yarn');
const boxen = importLazy('boxen');
const xdgBasedir = importLazy('xdg-basedir');
const isCi = importLazy('is-ci');
const pupa = importLazy('pupa');
const ONE_DAY = 1000 * 60 * 60 * 24;
2)声明 UpdateNotifier 类,后续无特殊说明,作用域均在类中
class UpdateNotifier {
...
}
3)声明类的构造函数
class UpdateNotifier {
constructor(options = {}) {
this.options = options;
options.pkg = options.pkg || {};
options.distTag = options.distTag || 'latest';
// 读取包名和版本号信息
options.pkg = {
name: options.pkg.name || options.packageName,
version: options.pkg.version || options.packageVersion
};
if (!options.pkg.name || !options.pkg.version) {
throw new Error('pkg.name and pkg.version required');
}
this.packageName = options.pkg.name;
this.packageVersion = options.pkg.version;
// 更新检查间隔,默认值:1天
this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
process.env.NODE_ENV === 'test' ||
// process.argv:Node.js 进程时传入的命令行参数
process.argv.includes('--no-update-notifier') ||
// CI持续集成环境
isCi();
this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
if (!this.disabled) {
// 生成文件存储版本信息
try {
const ConfigStore = configstore();
this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
optOut: false,
// 生成文件时,将当前时间记录起来,方便下次比对是否超出检查的时间间隔
lastUpdateCheck: Date.now()
});
} catch {
const message =
chalk().yellow(format(' %s update check failed ', options.pkg.name)) +
format('\n Try running with %s or get access ', chalk().cyan('sudo')) +
'\n to the local update config store via \n' +
chalk().cyan(format(' sudo chown -R $USER:$(id -gn $USER) %s ', xdgBasedir().config));
process.on('exit', () => {
console.error(boxen()(message, {align: 'center'}));
});
}
}
}
...
}
4)声明 check 方法
class UpdateNotifier {
...
check() {
if (
!this.config ||
this.config.get('optOut') ||
this.disabled
) {
return;
}
// 读取此次检查获取的版本信息
this.update = this.config.get('update');
if (this.update) {
// 使用最新的版本,而非缓存版本
this.update.current = this.packageVersion;
// 清除缓存中的信息
this.config.delete('update');
}
// Only check for updates on a set interval
// 仅在检查时间间隔内进行检查,如:设置1周内不重复检查
if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
return;
}
// 使用子进程来执行任务
spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
detached: true,
stdio: 'ignore'
}).unref();
}
}
5)获取版本信息
class UpdateNotifier {
...
// 获取版本信息
async fetchInfo() {
const {distTag} = this.options;
const latest = await latestVersion()(this.packageName, {version: distTag});
return {
latest,
current: this.packageVersion,
// 判断包是否为最新版本,联系 check.js 中 update.type === 'latest'
type: semverDiff()(this.packageVersion, latest) || distTag,
name: this.packageName
};
}
}
6)发出通知提醒
class UpdateNotifier {
...
notify(options) {
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
if (!process.stdout.isTTY || suppressForNpm || !this.update || !semver().gt(this.update.latest, this.update.current)) {
return this;
}
options = {
isGlobal: isInstalledGlobally(),
isYarnGlobal: isYarnGlobal()(),
...options
};
// 消息提醒模板
let installCommand;
if (options.isYarnGlobal) {
installCommand = `yarn global add ${this.packageName}`;
} else if (options.isGlobal) {
installCommand = `npm i -g ${this.packageName}`;
} else if (hasYarn()()) {
installCommand = `yarn add ${this.packageName}`;
} else {
installCommand = `npm i ${this.packageName}`;
}
// 消息提醒模板
const defaultTemplate = 'Update available ' +
chalk().dim('{currentVersion}') +
chalk().reset(' → ') +
chalk().green('{latestVersion}') +
' \nRun ' + chalk().cyan('{updateCommand}') + ' to update';
const template = options.message || defaultTemplate;
// 消息提醒边框样式
options.boxenOptions = options.boxenOptions || {
padding: 1,
margin: 1,
align: 'center',
borderColor: 'yellow',
borderStyle: 'round'
};
// 拼接提醒消息
const message = boxen()(
pupa()(template, {
packageName: this.packageName,
currentVersion: this.update.current,
latestVersion: this.update.latest,
updateCommand: installCommand
}),
options.boxenOptions
);
if (options.defer === false) {
console.error(message);
} else {
process.on('exit', () => {
console.error(message);
});
process.on('SIGINT', () => {
console.error('');
process.exit();
});
}
return this;
}
}