本文首發於個人博客,轉載請註明出處:https://kohpoll.github.io/blo...javascript
隨着 Node.js 的「走紅」,使用 Node.js 開發命令行工具愈來愈簡單。一個成熟的命令行工具應該從一開始就要考慮好以後的版本更新如何「優雅」的告知用戶。最好的方法固然是當用戶在終端執行命令時,將相關信息提示給用戶。java
這篇文章將給出一個易用、高效、可定製的方法。源碼在這裏:https://github.com/kohpoll/pk...,歡迎你們順手點贊。接下來我將講解其實現思路。git
咱們先簡單看看這個 npm 包的使用方法:github
const updater = require('pkg-updater'); const pkg = require('./package.json'); // 命令行工具本身的 package 信息 updater({'pkg': pkg}) .then(() => { /* 在這裏啓動命令行工具 */ }); updater({ 'pkg': pkg, // 自定義 registry 'registry': 'http://xxx.registry.com', // 自定義請求的 dist-tag,默認是 latest 'tag': 'next', // 自定義檢查間隔,默認是 1h 'checkInterval': 24 * 60 * 60 * 1000, // 自定義更新提示信息 'updateMessage': 'package update from <%=current%> to <%=latest%>.', // 自定義強制更新的版本更新級別,默認是 major 'level': 'minor' }).then(() => { /* 在這裏啓動命令行工具 */ }); updater({ 'pkg': pkg, // 徹底自定義版本更新時的邏輯 'onVersionChange': function* (opts) { } }).then(() => { /* 在這裏啓動命令行工具 */ });
效果如圖:npm
使用方法很簡單,咱們一塊兒來看看其實現方法。json
咱們先來梳理下需求,一個命令行檢查更新器應該至少提供以下功能:緩存
能從遠程獲取最新版本網絡
能根據檢查結果進行提示工具
在版本不兼容時能夠直接退出,強制用戶升級程序ui
獲取最新版本這個功能看起來很簡單,就是發送一個請求從「某處」獲取信息。可是有一些問題須要咱們考慮:
從哪裏獲取版本信息?
獲取版本信息的策略是怎樣的?(何時獲取?獲取的信息如何處理?)
咱們的命令行工具通常都是使用 npm 進行分發,最簡便的方法就是直接經過 registry 獲取。經過請求 https://registry.npmjs.org/{name}/{dist-tag}
就能夠獲得 package 對應 tag 的版本信息。結果相似下面這樣:
// https://registry.npmjs.org/co/latest { "name": "co", "version": "4.5.0" }
在實際實現時,咱們應該容許調用者自定義 registry 地址、請求的 dist-tag 等,這樣能夠有更多的定製性。
首先想到的方法是用戶每次執行命令時都去獲取一次版本信息,這樣的獲取策略應該是最簡單和實時的。
可是這個策略其實並不合適:
每次執行命令都要去發請求進行檢查,若是網絡延遲,會阻塞命令執行,影響用戶體驗
工具的版本更新其實並不會很頻繁,沒有必要進行實時檢查
網絡請求的影響因素不少,不能保證每次都成功,應該提供本地緩存機制來存儲請求成功的結果,避免版本信息的不可用
綜合上面的幾點,咱們設計以下的獲取策略:
將發送網絡請求獲取版本信息的邏輯放在一個獨立的後臺進程去執行,保證不阻塞主命令執行
請求成功後將版本信息、檢查時間緩存到用戶機器
每次執行命令時,只是讀取本地緩存下來的版本信息,不去發送網絡請求
根據緩存下來的檢查時間和當前時間,在一個間隔以內不去額外建立後臺檢查進程
將上面的策略翻譯成代碼大概就是下面這樣:
// 讀取本地緩存的檢查結果 const checkInfo = yield updater.readCheckInfo(opts); const lastCheck = checkInfo.lastCheck; const lastVersion = checkInfo.lastVersion; // 根據版本信息提示用戶 // ... // 在時間間隔內,直接返回 if (Date.now() - lastCheck < opts.checkInterval) { return; } // 建立後臺檢查進程 try { require('child_process').spawn( process.execPath, [require('path').join(__dirname, '_check.js'), JSON.stringify({ 'pkg': opts.pkg, // package 信息 'tag': opts.tag, // 檢查的 dist-tag 'logFile': opts.logFile, // 緩存文件路徑 'registry': opts.registry // registry 地址 })], {'stdio': ['ignore', 'ignore', 'ignore'], 'detached': true} ).unref(); } catch(e) {}
後臺進程執行的 _check.js
文件也很簡單,以下所示:
const opts = JSON.parse(process.argv[2]); let lastVersion = ''; try { // 發送請求獲取最新版本 const url = normalizeUrl(opts.registry + '/' + opts.pkg.name + '/' + (opts.tag || 'latest')); const res = yield got.get(url, { 'json': true, 'timeout': 60 * 1000 }); if (res && res.body && res.body.version) { lastVersion = res.body.version; } } catch(e) {} // 若是獲取失敗了,最新版本就是當前版本(package.version) if (!lastVersion) { lastVersion = opts.pkg.version; } let data = yield util.readJson(opts.logFile); if (!data[opts.pkg.name]) { data[opts.pkg.name] = {}; } data[opts.pkg.name].lastVersion = lastVersion; // 最新版本 data[opts.pkg.name].lastCheck = Date.now(); // 檢查時間 // 寫入緩存 yield util.writeJson(opts.logFile, data);
當版本更新了,咱們應該在終端提示用戶。這裏有兩個問題:
提示文案的問題
提示文案顯示間隔的問題(一直顯示?每隔一段時間顯示?)
這裏咱們採起的策略是:
提供默認提示文案,清晰的說明當前版本、最新版本、更新方法,容許調用者自定義提示文案
只要有更新就一直顯示提示文案,由於咱們但願用戶常常的進行更新
實現代碼大概以下:
// 比對版本 const type = updater.diffType(opts.pkg.version, lastVersion, opts.level); if (type) { // 根據模板渲染提示信息 const str = updater.template(opts.updateMessage || updater.defaultOpts.updateMessage)({ 'colors': updater.colors, 'name': opts.pkg.name, 'current': opts.pkg.version, 'latest': opts.lastVersion, 'command': 'npm i -g ' + opts.pkg.name }); // 進行提示 console.log( updater.boxen(str, { 'padding': 1, 'margin': 1, 'borderStyle': 'classic' }) ); }
對於 npm 模塊來講,版本 a.b.c 的更新通常有三種狀況:
patch:c 位,小版本更新,通常是 bug 修復
minor:b 位,中版本更新,通常增長新功能、bug 修復
major,a 位,大版本更新,通常是不兼容的升級
咱們但願當遠程版本的更新若是是 major 形式,命令行工具將直接退出,強制用戶進行升級後才能使用。這能夠保證咱們推送一個大版本後,全部的用戶都可以立刻更新掉,而不是繼續使用老版本,形成版本碎片的問題。
實現代碼大體以下:
// 比對版本 const type = updater.diffType(opts.pkg.version, lastVersion, opts.level); if (type) { // 根據模板渲染提示信息 const str = updater.template(opts.updateMessage || updater.defaultOpts.updateMessage)({ 'colors': updater.colors, 'name': opts.pkg.name, 'current': opts.pkg.version, 'latest': opts.lastVersion, 'command': 'npm i -g ' + opts.pkg.name }); // 進行提示 console.log( updater.boxen(str, { 'padding': 1, 'margin': 1, 'borderStyle': 'classic' }) ); // 不兼容的更新,直接讓進程退出 if (type == 'incompatible') { process.exit(1); } }
命令行檢查更新看似簡單,其實仔細思考,仍是有不少細節。但願這篇文章對你有所啓發。