從零開始的electron開發-更新-全量更新

更新-全量更新

更新

electron的更新通常來講有兩種方式,全量和增量,顧名思義全量就是下載咱們打包好的exe文件或者zip文件,進行全面替換。咱們以前說過electron就是用瀏覽器打開咱們的頁面,不少時候咱們的更新可能只會修改渲染進程,那麼咱們把咱們的渲染進程的文件給替換了不久更新嗎,也就是說增量其實是替換打包好的html,js等文件。那麼更新的方式以下:html

  • 主進程修改:全量更新
  • 渲染進程修改:全量或增量更新

增量更新放到下一期來講,本篇是講全量更新的事,本文旨在對全量更新進行詳細的說明,包括主進程的更新流程和渲染進程展現等以及更新問題如何排查。vue

更新流程

咱們這裏先說明一下咱們的處理流程:react

  • 咱們的渲染進程請求接口,接口返回了更新信息,拿到更新信息進行版本對比以及更新邏輯判斷。
  • 渲染進程全量更新經過,把信息傳遞給主進程
  • 主進程拿到信息實現更新,把更新信息推送給渲染進程
  • 渲染進程進行更新狀態以及進度的顯示,完成更新

electron-updater

全量更新咱們使用的是electron-updater這個包,進入咱們的項目:nginx

npm i -D electron-updater

electron-updater更新只須要把一個url傳入,而後它會自動查找${url}/xxx.yml文件,根據這個yml文件檢測更新及下載,咱們能夠經過其各類事件拿到對應的更新狀態,下載完成以後調用autoUpdater.quitAndInstall()重啓當前的應用而且安裝更新
這個url下是放靜態資源的,簡單來講就是這個連接相似訪問一個目錄,這個目錄下須要有更新的文件以及檢測更新的文件。
須要哪些東西呢:git

  • win: latest.yml,exe文件
  • mac:latest-mac.yml,zip文件

總的來講咱們就是要一個url實現更新,先讓渲染進程把這個url傳過來吧,咱們先模擬一下更新請求。github

更新請求

模擬更新請求

通常來講,更新都是調用後端接口,上面說了更新須要一個url,那麼咱們這裏就用json文件模擬請求以及模擬更新文件的地址web

npm i -g  http-server
新建一個目錄server
http-server ./ -p 4000   // 啓一個4000端口的服務http://127.0.0.1:4000
server下新建一個index.json來當接口返回數據
{
  "code": 200,
  "success": true,
  "data": {
    "forceUpdate": false, // 是否強制更新
    "fullUpdate": true, // 是否全量更新
    "upDateUrl": "http://127.0.0.1:4000/", // electron-updater傳入的url
    "restart": true,  // 增量更新是否重啓
    "message": "我要升級成0.0.2", // 更新說明
    "version": "0.0.2" // 版本號
  }
}
瀏覽器打開http://localhost:4000/,看是否有index.json。

咱們在渲染進程用請求獲取index.json的數據(這裏是我本身作的api請求封裝)
const api = proxy.$api
api('http://localhost:4000/index.json', {}, { method: 'get' }).then(res => {
  console.log(res)
})

打高版本包

而後咱們打一個高版本包,這裏是打dev包,修改.env.devVUE_APP_VERSION爲0.0.2vue-cli

npm run build:dev:win64
mac同理,將打包好的文件放入server目錄
win:latest.yml,0.0.2的exe
mac:latest-mac.yml,0.0.2的zip(注意是zip不是dmg)

跨域處理

不出意料的話會報一個跨域的錯誤,畢竟咱們的端口號不一樣,在electron中咱們的權限要比瀏覽器中大得多,
咱們能夠把同源策略關閉,就不會出現跨域的問題了,能夠獲得返回了。npm

BrowserWindow的
webPreferences: {
  .....
  webSecurity: false
}

補充:固然還有其餘跨域的處理方式,想了解的話能夠看一下我對跨域的說明連接json

渲染進程檢測更新

檢測更新

當前咱們的版本號的話,是在.env裏面設置的,不知道的可看搭建篇環境變量說明。開發時不用檢測版本更新,因此有個判斷。
isClick是自動檢測的話沒有message提示,手動有。

<template>
  <div class="update">
    <div class="version">當前版本爲:{{ config.VUE_APP_VERSION }}</div>
    <a-button type="primary" @click="upDateClick(true)">檢測更新</a-button>
  </div>
</template>

<script>
import cfg from '@/config'
import update from '@/utils/update'
import { defineComponent, getCurrentInstance } from 'vue'

export default defineComponent({
  setup() {
    const state = reactive({
      visible: false,
      upDateData: {},
      upDateProgress: {
        show: false,
        percent: 0
      }
    })
    const { proxy } = getCurrentInstance()
    const config = cfg
    const api = proxy.$api
     function upDateClick(isClick) {
      api('http://localhost:4000/index.json', {}, { method: 'get' }).then(res => {
        console.log(res)
        if (cfg.NODE_ENV !== 'development') {
          update(config.VUE_APP_VERSION, res).then(() => {
            state.upDateData = res
            if (res.fullUpdate) {
              updateNotice(isClick)
            } else {
              console.log()
            }
          }).catch(err => {
            if (err.code === 0) {
              isClick && message.success('已爲最新版本')
            }
          })
        } else {
          message.success('請在打包環境下更新')
        }
      })
    }
    return {
      config,
      upDateClick
    }
  }
})
</script>

版本對比

這裏用當前版本與接口版本進行對比,向主進程發送通知(更新message彈窗)。

/utils/update.js

export default (version, data) => {
  return new Promise((resolve, reject) => {
    const isUpdate = data ? versionCompare(data.version, version) : 0
    switch (isUpdate) {
      case 0:
        console.log('不更新')
        reject({ code: isUpdate })
        break;
      case 1:
        if (data.fullUpdate) {
          console.log('全量更新')
          resolve()
        } else {
          console.log('增量更新')
        }
        break
      case -1:
        console.log('降級')
        reject({ code: isUpdate })
        break
    }
  })
}


// 1爲當前版本比更新版本低,0爲版本一致,-1爲當前版本比更新版本高
function versionCompare(stra, strb) {
  const straArr = stra.split('.')
  const strbArr = strb.split('.')
  const maxLen = Math.max(straArr.length, strbArr.length)
  let result
  let sa
  let sb
  for (let i = 0; i < maxLen; i++) {
    sa = ~~straArr[i]
    sb = ~~strbArr[i]
    if (sa > sb) {
      result = 1
    } else if (sa < sb) {
      result = -1
    } else {
      result = 0
    }
    if (result !== 0) {
      return result
    }
  }
  return result
}

更新彈窗

檢測到更新後咱們顯示更新彈窗,根據forceUpdate判斷若是是強制更新的話咱們把彈窗的關閉除去,只能點肯定。若是不是的話,那麼點擊取消後,咱們記錄一個更新版本號的localStorage,下次再檢測更新時若是有值的話,更新彈窗將不會出現

<a-modal
  v-model:visible="visible"
  title="更新"
  @ok="upDateOk"
  @cancel="upDateCancel"
  :closable="!upDateData.forceUpdate"
  :maskClosable="false"
  :keyboard="!upDateData.forceUpdate"
  :destroyOnClose="true"
>
  <p v-html="upDateData.message"></p>
  <template #footer v-if="upDateData.forceUpdate">
    <div class="footer">
      <a-button type="primary" @click="upDateOk">肯定</a-button>
    </div>
  </template>
</a-modal>

調用updateNotice(isClick),這裏是按鈕點擊檢測更新,固然你也能夠程序自動檢測更新,放在App.vue中主動調用updateNotice(false)

function updateNotice(isClick) {
  if (LgetItem(UPDATE_LIST) && LgetItem(UPDATE_LIST)[state.upDateData.version] && !isClick) { // 用戶手動點擊檢測的話出現彈窗
    return
  }
  state.visible = true
}
function upDateOk() {
  state.visible = false
  window.ipcRenderer.invoke('win-update', {...state.upDateData})
}
function upDateCancel() {
  if (!LgetItem(UPDATE_LIST)) {
    LsetItem(UPDATE_LIST, {})
  }
  LsetItem(UPDATE_LIST, { ...LgetItem(UPDATE_LIST), [state.upDateData.version]: true })
}

主進程處理更新

咱們向主進程推送了更新信息win-update

ipcMain.js接收渲染進程的更新信息

import checkUpdate from './checkUpdate'

ipcMain.handle('win-update', (_, data) => {
  checkUpdate(data)
})

上面說了electron-updater須要一個url,咱們這裏實現一下其更新邏輯,electron-updater有各類更新狀態,咱們把這些狀態推送renderer-updateMsg給渲染進程,讓其有對應的提示或者進度,autoUpdater.quitAndInstall()被調用後會自動安裝重啓。

新建checkUpdate.js
import { autoUpdater } from 'electron-updater'
import log from '../config/log.js'
import global from '../config/global'
autoUpdater.logger = log
/**
 * -1 檢查更新失敗 0 正在檢查更新 1 檢測到新版本,準備下載 2 未檢測到新版本 3 下載中 4 下載完成
 **/
function Message(type, data) {
  const sendData = {
    type,
    data
  }
  global.sharedObject.win.webContents.send('renderer-updateMsg', sendData)
}

// 當更新發生錯誤的時候觸發。
autoUpdater.on('error', (err) => {
  log.info('更新出現錯誤')
  log.info(err.message)
  if (err.message.includes('sha512 checksum mismatch')) {
    Message(-1, 'sha512校驗失敗')
  }
})

// 當開始檢查更新的時候觸發
autoUpdater.on('checking-for-update', () => {
  log.info('開始檢查更新')
  Message(0)
})

// 發現可更新數據時
autoUpdater.on('update-available', () => {
  log.info('有更新')
  Message(1)
})

// 沒有可更新數據時
autoUpdater.on('update-not-available', () => {
  log.info('沒有更新')
  Message(2)
})

// 下載監聽
autoUpdater.on('download-progress', (progressObj) => {
  Message(3, progressObj)
})

// 下載完成
autoUpdater.on('update-downloaded', () => {
  log.info('下載完成')
  Message(4)
  setTimeout(() => { // 重啓更新提示1秒後在進行重啓安裝
    global.willQuitApp = true
    autoUpdater.quitAndInstall()
  }, 1000)
})

export default function (data) {
  log.info('Update', data)
  autoUpdater.setFeedURL(data.upDateUrl)
  autoUpdater.checkForUpdates().catch(err => {
    log.info('網絡鏈接問題', err)
    Message(5, err.code)
  })
}

渲染進程顯示更新狀態

在上面的更新彈窗中新增更新進度,其實就是接收主進程的更新狀態作個展現:

<a-modal
  v-model:visible="upDateProgress.show"
  title="下載中"
  :closable="false"
  :maskClosable="false"
  :keyboard="false"
  :destroyOnClose="true"
  :footer="null"
>
  <a-progress :percent="upDateProgress.percent" status="active" />
</a-modal>

const message = proxy.$message
onMounted(() => {
  window.ipcRenderer.on('renderer-updateMsg', (_, data) => {
    switch (data.type) {
      case -1:
        message.error(data.data)
        break
      case 0:
        message.info('正在檢查更新')
        break
      case 1:
        message.destroy()
        message.success('已檢查到新版本,開始下載')
        state.upDateProgress.show = true
        break
      case 2:
        message.destroy()
        message.success('無新版本')
        break
      case 3:
        state.upDateProgress.percent = data.data.percent.toFixed(1)
        break
      case 4:
        state.upDateProgress.show = false
        message.loading('重啓更新中...', 1)
        break
      case 5:
        message.destroy()
        message.warning(data.data)
        state.upDateProgress.show = false
        break
      default:
        break
    }
  })
})
onUnmounted(() => {
  window.ipcRenderer.removeListener('renderer-updateMsg')
})

開發完成後咱們打個0.0.1版本的dev包(上面咱們放入server文件夾的是0.0.2的dev包),而後檢測更新,更新重啓後看打印的config是不是0.0.2。

補充

經過上面的步驟,咱們的一個全量更新的流程就算完成了,這裏對一些常見問題進行補充。

請求緩存

咱們的請求是一個json文件,咱們可能會修改其中的參數,有時候發現返回仍是沒變,請將控制檯的NetworkDisable cache勾上(請求緩存常見處理方式)

下載緩存

在開發時咱們可能會作開發調試,好比看下載進度呀什麼的,先打了一個高版本的放在遠程地址,本地重複安裝低版本的看更新效果,可是在第一次更新完成後,後面的更新都是瞬間完成了,看不到進度,這裏是因爲electron-updater在更新時會檢測本地是否下載過這個高版本,有的話直接用本地的進行安裝,咱們能夠把這個緩存文件刪除掉。AppData這個文件夾呢多是處於隱藏的,後面挺多地方會用到這個的,能夠在頂端查看中勾選隱藏的項目讓其顯示,具體的話百度吧。

electronvuedev這個是你設置的name(本框架在vue.config.js中)
win:C:\Users\Administrator(你的用戶)\AppData\Local\electronvuedev-updater
mac:~/Library/Application Support/Caches/electronvuedev-updater

更新錯誤檢測

有時候咱們更新時會遇到問題,好比檢測出更新,可是下載卻不動,或是更新完成後並無安裝重啓,這裏先說一下比較常見的幾個可能會引起的問題

  • mac端下載進度不動:mac端打包會出現三個文件,dmg安裝包,dmg的zip壓縮包以及yml件,比win多一個zip壓縮包,mac更新下載的是zip文件,不少同窗覺得和win的exe同樣放(win是exe和yml),把dmg文件放到更新地址下,沒有zip文件,這會致使檢測到更新,但下載進度卻不動。
  • 更新下載後沒有重啓,打開軟件版本也沒有變:以前咱們在作窗口關閉時說過,能夠在某些事件裏用e.preventDefault()阻止窗口的關閉,好比咱們在第二期裏使用了:
win.on('close', (e) => {
  console.log('close', global.willQuitApp)
  if (!global.willQuitApp) {
    win.webContents.send('renderer-close-tips', { isMac })
    e.preventDefault()
  }
})

針對於本項目,咱們在調用重啓更新時,因爲咱們設置了這個可能會引發窗口關閉的失敗,這裏咱們在關閉前設置willQuitApp,讓其正常關閉:

global.willQuitApp = true
autoUpdater.quitAndInstall()
  • 其餘緣由排查:固然不可能只有上面幾種緣由的,那麼咱們如何進行排查呢:

    • 後端錯誤收集:autoUpdater的error事件,能夠在這個事件中把錯誤信息傳遞給後端接口。
    • 添加錯誤日誌:若是隻能本身調試的話,能夠用electron-log這個包把autoUpdater的error收集到本地日誌中:
    npm i electron-log
    
    新建log.js
    import log from 'electron-log'
    
    log.transports.file.level = 'silly'
    log.transports.console.level = false // 禁用console輸出
    
    export default log
    
    在咱們的checkUpdate.js更新中使用
    import log from '../config/log.js'
    autoUpdater.logger = log
    
    autoUpdater.on('error', (err) => {
      log.info('更新出現錯誤')
      log.info(err.message)
    })

    當更新出現問題後查看對應的log文件

    win:C:\Users\Administrator(你的用戶)\AppData\Roaming\<app name>\logs
    mac: ~/Library/Logs/<app name>

本系列更新只有利用週末和下班時間整理,比較多的內容的話更新會比較慢,但願能對你有所幫助,請多多star或點贊收藏支持一下

本文地址:連接
本文github地址:連接

相關文章
相關標籤/搜索