1. 前言
1.1 环境
- 操作系统: macOS 11.5.2
- 浏览器: Chrome 94.0.4606.81
- node 14.18.1
-
1.2 阅读该文章可以get以下知识点
update-notifier
- 检测npm包是否更新,比如组件库更新或者其他npm包更新,在控制台提醒
2. update-notifier源码解析
地址: update-notifier2.1 index.js 引入的npm包
```javascript // spawn 创建一个异步子进程 const {spawn} = require(‘child_process’); const path = require(‘path’); // 转换函数 const {format} = require(‘util’); // Import a module lazily 引入模块懒加载 const importLazy = require(‘import-lazy’)(require);
// Easily load and persist config without having to think about where and how
// 配置的持久化存储
const configstore = importLazy(‘configstore’);
// 控制台输出,带有颜色,vue-next使用了这个库
const chalk = importLazy(‘chalk’);
// 版本控制,vue-next使用了这个库
const semver = importLazy(‘semver’);
// 版本对比
const semverDiff = importLazy(‘semver-diff’);
// 最新版本
const latestVersion = importLazy(‘latest-version’);
// 是不是npm包
const isNpm = importLazy(‘is-npm’);
// 是不是全局安装
const isInstalledGlobally = importLazy(‘is-installed-globally’);
// 是不是yarn全局安装
const isYarnGlobal = importLazy(‘is-yarn-global’);
// 有没有yarn
const hasYarn = importLazy(‘has-yarn’);
// Create boxes in the terminal 创建box在终端上
const boxen = importLazy(‘boxen’);
// Get XDG Base Directory paths mac断点是获取路径/user/username下面的文件
const xdgBasedir = importLazy(‘xdg-basedir’);
// Returns true
if the current environment is a Continuous Integration server 判断当前环境是ci服务吗
const isCi = importLazy(‘is-ci’);
// Simple micro templating 模板语法
const pupa = importLazy(‘pupa’);
xdg-basedir的文档
```javascript
// sindresorhus是指用户的文件名
const xdgBasedir = require('xdg-basedir');
xdgBasedir.data;
//=> '/home/sindresorhus/.local/share'
xdgBasedir.config;
//=> '/home/sindresorhus/.config'
xdgBasedir.dataDirs
//=> ['/home/sindresorhus/.local/share', '/usr/local/share/', '/usr/share/']
2.2 UpdateNotifier 整体结构
class UpdateNotifier {
constructor(options = {}) {
}
check() {}
async fetchInfo() {}
notify(options) {}
}
module.exports = options => {
// new一个实例,执行constructor的内容
const updateNotifier = new UpdateNotifier(options);
// 执行check函数
updateNotifier.check();
return updateNotifier;
};
module.exports.UpdateNotifier = UpdateNotifier;
2.3 UpdateNotifier constructor
constructor(options = {}) {
this.options = options;
options.pkg = options.pkg || {};
options.distTag = options.distTag || 'latest';
// Reduce pkg to the essential keys. with fallback to deprecated options
// TODO: Remove deprecated options at some point far into the future
// 设置名称和版本,这里应该是做了兼容性判断,先找pkg.name,不存在在找option.packageName
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;
// 设置定时器的时长,如果option.updateCheckInterval存在就用这个,不存在设置为一天
this.updateCheckInterval = typeof options.updateCheckInterval === 'number' ? options.updateCheckInterval : ONE_DAY;
// 禁用状态,只要一个为true,就禁用,env.NO_UPDATE_NOTIFIER存在||NODE_ENV=test||参数包含--no-update-notifier|| 是不是ci
this.disabled = 'NO_UPDATE_NOTIFIER' in process.env ||
process.env.NODE_ENV === 'test' ||
process.argv.includes('--no-update-notifier') ||
isCi();
// 在npm脚本中需要通知
this.shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;
if (!this.disabled) {
try {
// 创建配置store,存入到~/.config/configStore中
const ConfigStore = configstore();
this.config = new ConfigStore(`update-notifier-${this.packageName}`, {
optOut: false,
// Init with the current time so the first check is only
// after the set interval, so not to bother users right away
lastUpdateCheck: Date.now()
});
} catch {
// Expecting error code EACCES or EPERM
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));
// 监听程序退出,输出一个box里面的信息
process.on('exit', () => {
console.error(boxen()(message, {align: 'center'}));
});
}
}
}
2.4 UpdateNotifier check 检测包的更新信息
check() {
// config不存在||config.optOut为true||disabled,直接返回,不校验
if (
!this.config ||
this.config.get('optOut') ||
this.disabled
) {
return;
}
// 初始化的时候是空的
this.update = this.config.get('update');
// update表示需要更新的信息
if (this.update) {
// Use the real latest version instead of the cached one,如果当前版本和包版本一致,直接删除
this.update.current = this.packageVersion;
// Clear cached information
this.config.delete('update');
}
// Only check for updates on a set interval
// 判断时间有效期,在更新范围内,不需要往下走
if (Date.now() - this.config.get('lastUpdateCheck') < this.updateCheckInterval) {
return;
}
// Spawn a detached process, passing the options as an environment property
// 子进程执行check.js文件,并将option转成字符串传入
spawn(process.execPath, [path.join(__dirname, 'check.js'), JSON.stringify(this.options)], {
detached: true,
stdio: 'ignore'
// unref 子进程让父进程退出
}).unref();
}
// check.js
/* eslint-disable unicorn/no-process-exit */
'use strict';
let updateNotifier = require('.');
// 字符串转对象
const options = JSON.parse(process.argv[2]);
// UpdateNotifiers 实例化
updateNotifier = new updateNotifier.UpdateNotifier(options);
// 异步函数
(async () => {
// Exit process when offline, 超时直接关闭进程
setTimeout(process.exit, 1000 * 30);
// 执行fetchInfo
const update = await updateNotifier.fetchInfo();
// Only update the last update check time on success
// 更新最近check时间
updateNotifier.config.set('lastUpdateCheck', Date.now());
// 如果有type并且不是最新,更新update数据
if (update.type && update.type !== 'latest') {
updateNotifier.config.set('update', update);
}
// Call process exit explicitly to terminate the child process,
// otherwise the child process will run forever, according to the Node.js docs
// 结束
process.exit();
})().catch(error => {
console.error(error);
process.exit(1);
});
很有意思的写法
// 自执行异步函数,然后通过catch捕获异常
(async () => {})().catch(error => {
console.error(error);
process.exit(1);
});
2.5 UpdateNotifier fetchInfo 获取包的最新版本信息
async fetchInfo() {
// 获取配置
const {distTag} = this.options;
// 获取最新的版本
const latest = await latestVersion()(this.packageName, {version: distTag});
// 返回信息,最后会在check函数中存入到configStore本地缓存
return {
latest,
current: this.packageVersion,
// 版本类型
type: semverDiff()(this.packageVersion, latest) || distTag,
name: this.packageName
};
}
2.6 UpdateNotifier notify 通知包更新
notify(options) {
// shouldNotifyInNpmScript是配置项,默认undefined,true的时候再去判断是npm或者yarn,如果是这两个就不通知
const suppressForNpm = !this.shouldNotifyInNpmScript && isNpm().isNpmOrYarn;
// 一些不通知的条件,只要一个成立就return,isTTY(系统默认就是true,设置为false不通知),suppressForNpm,update(没有更新信息), semver(版本对比,最新没大于当前版本)
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;
// boxen的配置项,用来在命令行现在盒子的样式
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;
}
2.7 整体调用顺序
- new UpdateNotifier() 初始化
- 实例对象check,检测更新信息,变更本地configStore缓存
- 实例对象调用notify,打印到控制台,通知更新
2.8 如何使用
```javascript // Notify using the built-in convenience method notifier.notify();
// notifier.update
contains some useful info about the update
console.log(notifier.update);
/*
{
latest: ‘1.0.1’,
current: ‘1.0.0’,
type: ‘patch’, // Possible values: latest, major, minor, patch, prerelease, build
name: ‘pageres’
}
const notifier = updateNotifier({ pkg, updateCheckInterval: 1000 60 60 24 7 // 1 week });
if (notifier.update) {
console.log(Update available: ${notifier.update.latest}
);
}
```
4. 总结
通过阅读update-notifier源码学到了不少东西