【Electron】酷家樂客戶端開發實踐分享 — 軟件自動更新

做者:鍾離,酷家樂PC客戶端負責人
原文地址:https://webfe.kujiale.com/electron-autoupdate/
酷家樂客戶端:下載地址 https://www.kujiale.com/activity/136
文章背景:在酷家樂客戶端在V12改版成功後,咱們積累了許多的寶貴的經驗和最佳實踐。前端社區裏關於Electron知識相對較少,所以但願將這些內容以系列文章的形式分享出來。
系列文章:css

更新原理

在講客戶端更新方案以前,咱們先了解一下web和客戶端更新的原理html

web應用

在web應用的世界裏,咱們一般會更新web服務器上的前端代碼(模板、HTML,也多是js、css),來發布新的功能。在此以後用戶再訪問咱們的web服務器,拿到的已是更新事後的前端代碼了。前端

web應用更新如此方便,得益於它中心化存儲的方式:ios

  1. web應用的前端代碼,通常集中儲存在服務器或雲服務上
  2. 瀏覽器每次都會都會去服務器拉取最新的資源,用戶本機實際上沒有持久化儲存web應用的代碼
瀏覽器緩存也算是在用戶本機存儲了前端代碼,可是在web應用須要更新的時候,確定是會禁用緩存的,不然此次發佈對有緩存的用戶無效。

客戶端

和web應用的中心化儲存不一樣,客戶端的代碼其實是一種分佈式存儲,每一個用戶電腦上都有一份完整的代碼文件,有點像gitgit

用戶在電腦上安裝客戶端,實際上會將客戶端代碼文件持久儲存到本機。例如在MacOS上,代碼文件存放在/Applications目錄下。github

客戶端內嵌web頁面的更新方式,和上面講到的web應用更新是同樣的,再也不贅述(參考移動APP內嵌的H5頁面更新)

結論

web應用的更新,其實是更新服務端代碼文件web

客戶端的更新,其實是更新用戶電腦上代碼文件shell

具體實現

Electron官網有關於更新的教程 Updating Applications,可是都不能知足業務需求:npm

  1. update.electron.org,代碼必須託管在github上,pass
  2. electron-builder,windows下只支持NSIS,並且須要搭建HTTP服務。更新程序UI和交互定製也不是很友好
  3. 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();

三、更新本機文件

當用戶點擊了更新按鈕以後,那麼意味着咱們能夠開始進行最後一步了。

最後的這一步驟,咱們分兩步進行:

  1. 獲取到最新的安裝程序(.dmg or .exe),由於最新的代碼文件就在安裝程序中
  2. 替換掉用戶本機上的代碼文件

這一步驟,能夠交給用戶來作,也能夠由咱們幫用戶來作。咱們來看看這兩種狀況下,分別是如何實現的。

交給用戶來更新

首先,咱們須要更新網站客戶端下載頁上的安裝程序資源至最新。而後,用戶點擊更新按鈕以後,直接用本機默認瀏覽器打開下載頁,讓用戶本身下載、安裝,安裝程序正常執行完畢以後,自己就能夠覆蓋本機代碼文件。

  1. 用戶經過瀏覽器,在下載頁獲取到了最新的安裝程序。
  2. 用戶手動打開了安裝程序,並執行完畢安裝程序。

此法用戶體驗不是很好,可是優勢也很明顯:節省了不少開發成本,直接複用了web頁面來作更新。

若是採用這種策略,那麼代碼會很是簡單:

// 點擊更新按鈕
function handleUpdate() {
    shell.openExternal('https://www.kujiale.com/activity/136'); // 打開一個下載頁,剩下的交給用戶
    
}
咱們幫用戶更新

固然,爲了追求更好的用戶體驗,直接在更新窗口的代碼中實現功能是更好的。

第一步,下載最新的安裝程序,而且給出下載進度展現。下載進度功能推薦使用request-progress來作。固然,也可使用NodeJs原生的httpstream模塊來實現下載進度展現,這裏不詳細講解。

下載到的安裝程序,能夠暫時存放到用戶電腦的臨時文件夾中

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')))
}

進度展現示例圖:下載

第二步,將安裝程序中的代碼文件更新到用戶本機上,此時有兩種方案:

  1. 直接打開安裝程序,用戶跟隨安裝程序指引稍做點擊便可完成安裝。
  2. 解壓安裝程序中的內容,並將內容更新到用戶本機

在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

相關文章
相關標籤/搜索