🤖 作者簡介:水煮白菜王,一位前端勸退師 👻
👀 文章專欄: 前端專欄 ,記錄一下平時在博客寫作中,總結出的一些開發技巧和知識歸納總結?。
感謝支持💕💕💕
目錄
- 實現流程分析
- 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是一個不可或缺的工具,它幫助用戶及時了解并應用最新的組件庫更新,從而提高項目的穩定性和安全性。
如果你覺得這篇文章對你有幫助,請點贊 👍、收藏 👏 并關注我!👀