做者:鍾離,酷家樂PC客戶端負責人
原文地址:https://webfe.kujiale.com/electron-autoupdate/
酷家樂客戶端:下載地址 https://www.kujiale.com/activity/136
文章背景:在酷家樂客戶端在V12改版成功後,咱們積累了許多的寶貴的經驗和最佳實踐。前端社區裏關於Electron知識相對較少,所以但願將這些內容以系列文章的形式分享出來。
系列文章:css
在講客戶端更新方案以前,咱們先了解一下web和客戶端更新的原理html
在web應用的世界裏,咱們一般會更新web服務器上的前端代碼(模板、HTML,也多是js、css),來發布新的功能。在此以後用戶再訪問咱們的web服務器,拿到的已是更新事後的前端代碼了。前端
web應用更新如此方便,得益於它中心化存儲的方式:ios
瀏覽器緩存也算是在用戶本機存儲了前端代碼,可是在web應用須要更新的時候,確定是會禁用緩存的,不然此次發佈對有緩存的用戶無效。
和web應用的中心化儲存不一樣,客戶端的代碼其實是一種分佈式存儲,每一個用戶電腦上都有一份完整的代碼文件,有點像git
。git
用戶在電腦上安裝客戶端,實際上會將客戶端代碼文件持久儲存到本機。例如在MacOS上,代碼文件存放在/Applications
目錄下。github
客戶端內嵌web頁面的更新方式,和上面講到的web應用更新是同樣的,再也不贅述(參考移動APP內嵌的H5頁面更新)
web應用的更新,其實是更新服務端代碼文件web
客戶端的更新,其實是更新用戶電腦上代碼文件shell
Electron官網有關於更新的教程 Updating Applications,可是都不能知足業務需求:npm
update.electron.org
,代碼必須託管在github上,passelectron-builder
,windows下只支持NSIS,並且須要搭建HTTP服務。更新程序UI和交互定製也不是很友好Deploying an Update Server
,這個方案須要部署一個update server,也比較麻煩所以,咱們使用的是本身實現的一套更新流程。json
檢查更新是總體流程的第一個步驟。若是有更新,後續的更新邏輯纔會執行。一般咱們會在軟件啓動時檢查更新。
檢查更新的策略,其實是將本地客戶端的版本與遠程版本進行一次對比,而後根據版本對比的結果來給出不一樣的更新展現。
相比於本身搭建一個update server,維護一個遠程的JSON數據成本是很低的。這個遠程數據能夠是一個後端接口或者cdn上的json文件,而且能夠在須要更新的時候,及時更新遠程數據的內容
這個遠程JSON數據裏面通常會存放版本號、更新內容介紹以及發佈時間:
const updateData = axios.get('https://some-update.json'); console.log(updateData); /* { version: '1.0.0', changeLogs: ['來個開發祭天','新增了🐂🍺的功能'], time: '2019-06-06', } */
在Electron中獲取本地版本是很是簡單的 app.getVersion
const localVersion = app.getVersion(); // 0.0.1
一般,遠程版本號大於本地版本時,即認定爲有更新。在有更新的狀況下,咱們還能夠根據版本號裏的major、minor、patch版本變更,來制定不一樣的更新策略。
// 遠程版本 > 本地版本 const shouldUpdate = semver.gt(removeVersion, localVersion); // 例子:major版本號變化時,給出強的更新提示。不然給出正常更新提示 const isMajorUpdate = semver.diff(removeVersion, localVersion) === 'major'; if (!shouldUpdate) return; // 無更新,不走後續 if (isMajorUpdate) { console.log('給出強勢更新') } else { console.log('給出普通的更新提示') }
對於版本號的操做使用 semver
檢查到軟件更新以後,須要給出更新提示來提醒到用戶。此時,咱們會使用一個窗口承載更新提示的內容,後面統稱爲更新窗口。
更新窗口內部代碼示例:
const updateData = axios.get('some-update.json') // 檢查更新的邏輯,省略 if (!shouldUpdate) return; // 無更新 // 執行到這裏,確定有更新了。拿到更新數據,渲染窗口內容 ReactDom.render(<App updateData={updateData} onUpdate={() => console.log('用戶點擊了更新')} />, '#app'); // 有更新,主動展現窗口(更新窗口默認是隱藏的) currentWindow.show();
當用戶點擊了更新按鈕以後,那麼意味着咱們能夠開始進行最後一步了。
最後的這一步驟,咱們分兩步進行:
這一步驟,能夠交給用戶來作,也能夠由咱們幫用戶來作。咱們來看看這兩種狀況下,分別是如何實現的。
首先,咱們須要更新網站客戶端下載頁上的安裝程序資源至最新。而後,用戶點擊更新按鈕以後,直接用本機默認瀏覽器打開下載頁,讓用戶本身下載、安裝,安裝程序正常執行完畢以後,自己就能夠覆蓋本機代碼文件。
此法用戶體驗不是很好,可是優勢也很明顯:節省了不少開發成本,直接複用了web頁面來作更新。
若是採用這種策略,那麼代碼會很是簡單:
// 點擊更新按鈕 function handleUpdate() { shell.openExternal('https://www.kujiale.com/activity/136'); // 打開一個下載頁,剩下的交給用戶 }
固然,爲了追求更好的用戶體驗,直接在更新窗口的代碼中實現功能是更好的。
第一步,下載最新的安裝程序,而且給出下載進度展現。下載進度功能推薦使用request-progress來作。固然,也可使用NodeJs原生的http
和stream
模塊來實現下載進度展現,這裏不詳細講解。
下載到的安裝程序,能夠暫時存放到用戶電腦的臨時文件夾中
const fs = require('fs'); const request = require('request'); const progress = require('request-progress'); // 點擊更新按鈕 function handleUpdate(){ // 根據版本號拼接安裝程序地址 const downloadUrl = `https://someupdate/${updateData.version}/installer.dmg`; // 用request下載 progress(request(downloadUrl)) .on('progress', (state) => { // 進度 console.log(state) }) // 寫入到臨時文件夾 .pipe(fs.createWriteStream(path.join(app.getPath('temp'), 'installer.dmg'))) }
進度展現示例圖:
第二步,將安裝程序中的代碼文件更新到用戶本機上,此時有兩種方案:
在windows下,安裝程序裏面是有業務邏輯的:操做註冊表、卸載程序、快捷方式等等,所以咱們選擇第一種方案。
const { shell, app } = require('electron'); shell.openItem('your installer exe path'); // 打開下載好的安裝程序 app.quit(); // 退出當前客戶端
在MacOS下,咱們所發行的dmg文件其實沒有業務邏輯,所以可使用方案二,直接把.app
目錄解壓出來,而後拷貝到/Applications
目錄便可。在MacOS下,解壓dmg文件可使用hdiutil。
const cp = require('child_progress'); const path = require('path'); const fs = require('fs-extra'); // 下載完畢以後的dmg文件,文件內的.app目錄名爲Test const installerPath = '/your_installer.dmg'; // 使用hdiutil來解壓dmg文件內部資源,解壓後的資源目錄爲/Volumes/your_installer cproc.execSync(`hdiutil attach ${installerPath} -nobrowse`, { stdio: ['ignore', 'ignore', 'ignore'] }); // 刪掉原有的.app目錄 fs.removeSync('/Applications/Test.app'); // 把Volumes目錄下的.app目錄拷貝到/Applications中,更新完畢 fs.copySync('/Volums/your_installer/Test.app', '/Applications'); // 重啓應用 app.relaunch(); app.quit();
歡迎你們在評論區討論,技術交流 & 內推 -> zhongli@qunhemail.com