🤖 作者简介:水煮白菜王,一位前端劝退师 👻
👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧和知识归纳总结✍。
感谢支持💕💕💕
目录
- 实现流程分析
- update-notifier的实现
- 内部使用的第三方库
- 实现
- 初始化
- check
- check.js
- fetchInfo
- notify
- 最后
实现流程分析
update-notifier的实现
update-notifier是在给定的时间间隔内,检查package.json 中的 name 和 version 和当前 npm registry 上是否有新的更新可用,并在有更新时通知用户。它基于 ConfigStore 来保存和读取配置信息,使用了一些第三方库来获取最新版本的信息,并使用 boxen 来创建一个带有边框的通知框。\
内部使用的第三方库
- process:Node.js 的 process 模块,提供了对进程的操作和控制。
- spawn:child_process 模块的 spawn 方法,用于启动一个子进程并执行命令。
- fileURLToPath:url 模块的 fileURLToPath 方法,用于将文件的 URL 转换为本地路径。
- path:Node.js 的 path 模块,提供了处理文件路径的工具函数。
- format:util 模块的 format 方法,用于格式化字符串。
- ConfigStore:一个用于保存配置信息的库。
- chalk:一个用于在终端中添加颜色和样式的库。
- semver:用于对语义化版本进行比较和操作的库。
- semverDiff:用于计算两个版本之间的差异的库。
- latestVersion:用于获取指定包名的最新版本号的库。
- isNpmOrYarn:一个用于检测当前项目是使用npm还是yarn的库。
- isInstalledGlobally:一个用于检测当前包是否全局安装的库。
- boxen:用于在终端创建带有边框的框的库。
- xdgConfig:一个用于获取 XDG 配置目录路径的库。
- isInCi:用于检测当前是否在 CI 环境下运行的库。
- pupa:用于填充模板字符串的库。
UpdateNotifier 类有一些公共属性和方法,用于检查和通知更新。其中重要的属性和方法包括:
- config:一个 ConfigStore 实例,用于保存和读取更新通知的配置信息。
- update:一个保存最新版本信息的对象,包括当前版本、最新版本和版本差异。
- check():用于检查是否有新的更新可用。
- fetchInfo():用于获取最新版本的信息。
- notify():用于通知用户有新的更新可用。
伪代码
const { exec } = require('child_process');// 检查指定组件库的更新
function checkSpecificPackageUpdate(packageName, currentVersion) {exec(`npm view ${packageName} version`, (error, stdout, stderr) => {if (error) {console.error(`Error fetching version for ${packageName}: ${error.message}`);return;}if (stderr) {console.error(`Stderr for ${packageName}: ${stderr}`);return;}const latestVersion = stdout.trim();if (latestVersion !== currentVersion) {console.log(`Update available for ${packageName}: ${currentVersion} -> ${latestVersion}`);} else {console.log(`${packageName} is up to date.`);}});
}// 指定的组件库及其版本
const packagesToCheck = [{ name: 'express', version: '4.17.1' },{ name: 'lodash', version: '4.17.20' }
];// 遍历并检查每个指定的组件库
packagesToCheck.forEach(pkg => {checkSpecificPackageUpdate(pkg.name, pkg.version);
});
实现
class UpdateNotifier {// 初始化操作constructor(options = {}) {}// 检查check() {}// 获取版本信息fetchInfo() {}// 提示notify(options) {}
}
初始化
在UpdateNotifier的构造函数中,初始化操作包括解析传入的选项、设置默认值(如包名和版本号)、确定更新检查的时间间隔以及判断是否需要禁用更新通知。此外,还会创建一个持久化存储来保存配置信息,如用户选择退出更新通知等。
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 兼容以前版本处理options.pkg = {name: options.pkg.name ?? options.packageName,version: options.pkg.version ?? options.packageVersion,};// 必须传项 pkg.name(需要校验的包), pkg.version(需要校验包的版本)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;// 禁用提示// 1. 在环境变量中禁用 NO_UPDATE_NOTIFIER// 2. 在环境变量中禁用 NODE_ENV 为 test// 3. 在命令行中禁用 node example.js --no-update-notifier// 4. 在CI中禁用this.#isDisabled ="NO_UPDATE_NOTIFIER" in process.env ||process.env.NODE_ENV === "test" ||process.argv.includes("--no-update-notifier") ||isInCi;// 是否在npm脚本中通知this._shouldNotifyInNpmScript = options.shouldNotifyInNpmScript;// 如果没有禁用提示 继续执行if (!this.#isDisabled) {try {// 持久化存储数据 存储一些默认值如 {optOut: false, lastUpdateCheck: Date.now()} 存储位置 ~/.config/update-notifierthis.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 awaylastUpdateCheck: Date.now(),});} catch {// Expecting error code EACCES or EPERMconst 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 ", xdgConfig),);process.on("exit", () => {console.error(boxen(message, { textAlignment: "center" }));});}}
}
check
check()方法用于判断当前是否需要执行更新检查。它首先检查是否满足某些条件(例如是否设置了禁用标志或是否达到预定的检查时间间隔)。如果满足条件,则会启动一个独立的子进程来执行实际的更新检查操作,避免影响主应用的性
check() {// 判断是否禁用// 1. 是否存在config属性// 2. 是否有optOut字段// 3. 是否禁用提示if (!this.config || this.config.get("optOut") || this.#isDisabled) {return;}// 第一次是undefinedthis.update = this.config.get("update");// 如果存在update 更新当前的最新版本currentif (this.update) {// Use the real latest version instead of the cached onethis.update.current = this.#packageVersion;// Clear cached information 清除上一次缓存信息this.config.delete("update");}// Only check for updates on a set interval// 如果当前检测时间和上一次检测的时间间隔小于 updateCheckInterval, 则不进行校验, 防止校验次数过多, 降低用户体验if (Date.now() - this.config.get("lastUpdateCheck") <this.#updateCheckInterval) {return;}// Spawn a detached process, passing the options as an environment property// 启动一个独立的子进程中执行 check.js 文件,并将 this.options 对象作为参数传递给 check.js。子进程的标准输入、输出和错误流都会被忽略,子进程与父进程的关联也会被解除。// 目的是更新configStore中的update数据spawn(process.execPath,[path.join(__dirname, "check.js"), JSON.stringify(this.#options)],{detached: true,stdio: "ignore",},).unref();}
check.js
check.js是一个单独的脚本文件,通过子进程的方式被调用来执行具体的更新检查任务。它接收来自父进程传递过来的配置选项,并尝试获取指定组件库的最新版本信息。一旦成功获取到更新信息,就会更新本地配置存储中的相关数据,并记录最后一次检查的时间。
/* eslint-disable unicorn/no-process-exit */
import process from "node:process";
import UpdateNotifier from "./update-notifier.js";
// 传入this.#options的配置
const updateNotifier = new UpdateNotifier(JSON.parse(process.argv[2]));try {// Exit process when offline// 30s退出setTimeout(process.exit, 1000 * 30);// 获取最新版本信息const update = await updateNotifier.fetchInfo();// Only update the last update check time on success// 更新最后一次检查时间updateNotifier.config.set("lastUpdateCheck", Date.now());// 更新最新版本信息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 docsprocess.exit();
} catch (error) {console.error(error);process.exit(1);
}
fetchInfo
fetchInfo()方法负责从npm注册表中获取指定组件库的最新版本信息,并将其与当前安装的版本进行比较。此方法返回的对象包含了最新的版本号、当前版本号以及两者之间的差异类型。
async fetchInfo() {const { distTag } = this.#options;const latest = await latestVersion(this._packageName, { version: distTag });return {latest,current: this.#packageVersion,type: semverDiff(this.#packageVersion, latest) ?? distTag,name: this._packageName,};}
notify
notify()方法根据一系列条件判断是否向用户显示更新通知。这些条件包括当前环境是否支持TTY输出、是否处于npm脚本运行环境中、是否有可用的更新信息以及新版本是否确实比当前版本更高。如果所有条件都满足,则使用chalk和boxen构建并展示一个美观的通知框,指导用户如何更新到最新版本。
notify(options) {// 如果是npm或yarn且不在npm脚本中通知,则不通知const suppressForNpm = !this._shouldNotifyInNpmScript && isNpmOrYarn;// 是否提示// 1. 判断TTY是否可用// 2. 判断是不是在npm脚本中// 3. 判断是否有没有update信息// 4. 判断是否大于最新版本if (!process.stdout.isTTY ||suppressForNpm ||!this.update ||!semver.gt(this.update.latest, this.update.current)) {return this;}// 是否是全局安装options = {isGlobal: isInstalledGlobally,...options,};// 根据判断是否是全局安装 设置不同命令const installCommand = options.isGlobal? `npm i -g ${this._packageName}`: `npm i ${this._packageName}`;// 提示信息 chalk是一个命令行颜色库const defaultTemplate ="Update available " +chalk.dim("{currentVersion}") +chalk.reset(" → ") +chalk.green("{latestVersion}") +" \nRun " +chalk.cyan("{updateCommand}") +" to update";// 如果有传入message则使用传入的, 否则使用默认const template = options.message || defaultTemplate;// 如果没有传入boxenOptions则使用默认 这个库来构建各种不同的方框包裹的的提示信息options.boxenOptions ??= {padding: 1,margin: 1,textAlignment: "center",borderColor: "yellow",borderStyle: "round",};// 渲染提示信息 pupa将数据渲染到模板中的占位符const message = boxen(pupa(template, {packageName: this._packageName,currentVersion: this.update.current,latestVersion: this.update.latest,updateCommand: installCommand,}),options.boxenOptions,);// 如果options.defer属性的值为false,则在控制台输出错误信息。否则,注册一个"exit"事件监听器,该监听器在进程退出时输出错误信息;另外,还注册一个"SIGINT"事件监听器,该监听器在接收到SIGINT信号(通常由用户按下Ctrl+C触发)时输出错误信息,并退出进程。if (options.defer === false) {console.error(message);} else {process.on("exit", () => {console.error(message);});process.on("SIGINT", () => {console.error("");process.exit();});}return this;}
最后
update-notifier库通过其精心设计的流程和方法,提供了一个简便而有效的途径来追踪项目依赖的更新情况。它不仅关注用户体验,通过设定合理的检查间隔避免了频繁打扰用户,还通过环境检测和灵活的通知条件设置,确保通知仅在合适的时机出现。利用如chalk用于美化终端输出、semver用于版本号比较等多种第三方库的支持,使得整个更新通知过程既直观又友好。
从初始化配置开始,到执行检查,直到最后的通知展示,每一个环节都体现了对细节的关注以及对用户需求的深刻理解。对于个人开发者或团队合作来说,update-notifier是一个不可或缺的工具,它帮助用户及时了解并应用最新的组件库更新,从而提高项目的稳定性和安全性。
如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀