從零開始的electron開發-更新-增量更新(一)

更新-增量更新(一)

上一期咱們完成了electron的全量更新,本期咱們介紹的是如何只修改部分文件以實現增量更新的幾種方案。javascript

asar

咱們將electron軟件進行安裝後右鍵打開文件所在位置進入resources目錄(mac顯示包內容),能夠看到app.asar這個文件,這個文件是electron程序的主業務文件,這東西可不是什麼加密文件,其實是一種壓縮文件,咱們能夠用npm包解壓這個文件css

npm install -g asar
asar extract app.asar ./   (和app.asar同級目錄下執行,注:安裝在c盤下的同窗若是解壓不了的話,用管理員身份運行cmd進入再解壓)

解壓後發現,實際上就是dist_electron/bundled裏面的東西,其實咱們若是隻修改了渲染進程裏面的東西的話,並不須要進行徹底的打包更新,只要對js,html,css進行替換,那咱們的頁面也會更新,那麼咱們只須要更新幾M的文件,並不須要讓用戶再下載一個完整的新包,增量更新的優勢也在於此。 html

可是呢提及來容易,實際操做起來呢仍是有必定的問題的,若是你設置的打包asar:true的話,那麼在軟件啓動的時候進行app.asar替換會發現替換不了(win下),正在被軟件使用。那麼這個方案確定用普通的替換是走不通了,下面我介紹幾種方案供你們查考。vue

7z-Asar7z

這裏呢我還提供一個7z的插件,讓7z也能打開asar,連接,若是你的7z是安裝在c盤,把Asar.64.dll(64位系統)放入C:\Program Files\7-Zip\Formats\裏,Formats沒有的話,本身新建一個。
asarjava

方案一,asar:false

這是一種比較常見的方式,好比vscode就是採用的此方案,在進行打包是修改打包配置(vue.config.js中的builderOptions)asar:false,那麼打包的時候resources下就不會產生app.asar,而是一個app文件夾,而這個文件夾呢是能夠直接進行替換的,故不存在替換不了的問題。 node

簡單來講就是,設置asar:false,打包,進入打包的綠色安裝包dist_electron/win-ia32-unpacked/resources(win32),將app文件壓縮成app.zip放到服務器,渲染進程檢測增量更新通知主進程,主進程下載app.zip,解壓替換。git

  • 優勢: 簡單粗暴。
  • 缺點:安裝及全量更新時安裝較慢,主進程暴露在外面,替換時都是總體下載替換。

此方案的步驟和方案二差很少,具體作法可參考方案二的方式。github

方案二,app.asar.unpacked + app.asar

app.asar.unpacked這個東西仍是比較常見的,因爲app.asar的限制性,好比文件內只是可讀,一些node命令不能使用等,咱們常常會把一些第三方的數據庫或者dll等文件會用到這個,簡單來講就是把原本應該放在app.asar中的文件放入到與app.asar同級的app.asar.unpacked目錄中(其實和方案一的app文件夾相似),從而解除app.asar的限制性。web

看到這裏是否是就有新思路了,既然app.asar不能動,咱們能夠把變更的文件給扔到app.asar.unpacked裏,主進程及一些不變的東西仍是放在app.asar,增量更新時替換app.asar.unpacked就好了。vue-cli

  • 優勢: 能夠將主進程js等文件保留在app.asar,只對渲染進程文件進行替換。
  • 缺點:因爲主進程js沒有動,那麼主進程注入的環境變量版本號也不會改變,也就是說更新後在主進程使用環境變量獲取的版本號不是更新後的版本號(能夠從渲染進程拿)。

實現步驟:

1. 設置app.asar.unpacked

首先設置一下咱們想要替換的那些文件,打包時會先生成dist_electron/bundled這個文件夾,而後再用electron-builder把這個文件夾打包成咱們的electron文件。

vue.config.js的builderOptions

extraResources: [{
  from: "dist_electron/bundled",
  to: "app.asar.unpacked",
  filter: [
    "!**/icons",
    "!**/preload.js",
    "!**/node_modules",
    "!**/background.js"
    ]
  }],
  files: [
    "**/icons/*",
    "**/preload.js",
    "**/node_modules/**/*",
    "**/background.js"
  ],

extraResources呢是設置app.asar.unpacked裏面的東西,files是設置app.asar裏的東西,這裏的意思是咱們把dist_electron/bundled裏面的除了iconsbackground.js等文件放入app.asar,其他的都放入app.asar.unpacked,打包看看,看看app.asar.unpacked裏面是不是咱們想要的東西。

2. 構建增量zip

如今咱們有了app.asar.unpacked,可是咱們不可能每次都進入免安裝包裏面手動壓縮app.asar.unpacked,太麻煩了,咱們這裏利用打包完成的鉤子,自動構建增量包。
adm-zip是處理zip包,fs-extra是fs的拓展,處理文件

npm i adm-zip
npm i fs-extra

electron-builder提供裏打包完成的鉤子afterPack

vue.config.js的builderOptions添加

afterPack: './afterPack.js',

./afterPack.js:
const path = require('path')
const AdmZip = require('adm-zip')

exports.default = async function(context) {
  let targetPath
  if(context.packager.platform.nodeName === 'darwin') {
    targetPath = path.join(context.appOutDir, `${context.packager.appInfo.productName}.app/Contents/Resources`)
  } else {
    targetPath = path.join(context.appOutDir, './resources')
  }
  const unpacked = path.join(targetPath, './app.asar.unpacked')
  var zip = new AdmZip()
  zip.addLocalFolder(unpacked)
  zip.writeZip(path.join(context.outDir, 'unpacked.zip'))
}

mac和win的resources有所區別,如今咱們再打包看看,dist_electron目錄下會生成一個unpacked.zip,這個就是咱們的增量包了。

3. 加載策略修改

在窗口啓動篇咱們說過,咱們渲染進程的html加載是經過app://協議加載的,這個協議呢之前是以app.asar爲根目錄的,這裏把的渲染進程的文件給移出app.asar了,app://協議就找不到咱們的渲染進程html,因此咱們這裏須要修改一下,把app.asar.unpacked做爲根目錄。

主進程main/index.js下找到
// import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' 咱們找到這個文件拷貝一份出來,
// 修改readFile(path.join(__dirname, pathName),這裏能夠看出這個協議讀取的是__dirname(`app.asar`)下的文件,咱們經過傳入一個path替換掉原來的__dirname

建立createProtocol.js

import { protocol } from 'electron'
import * as path from 'path'
import { readFile } from 'fs'
import { URL } from 'url'

export default (scheme, serverPath = __dirname) => {
  protocol.registerBufferProtocol(
    scheme,
    (request, respond) => {
      let pathName = new URL(request.url).pathname
      pathName = decodeURI(pathName) // Needed in case URL contains spaces
      readFile(path.join(serverPath, pathName), (error, data) => {
        if (error) {
          console.error(
            `Failed to read ${pathName} on ${scheme} protocol`,
            error
          )
        }
        const extension = path.extname(pathName).toLowerCase()
        let mimeType = ''

        if (extension === '.js') {
          mimeType = 'text/javascript'
        } else if (extension === '.html') {
          mimeType = 'text/html'
        } else if (extension === '.css') {
          mimeType = 'text/css'
        } else if (extension === '.svg' || extension === '.svgz') {
          mimeType = 'image/svg+xml'
        } else if (extension === '.json') {
          mimeType = 'application/json'
        } else if (extension === '.wasm') {
          mimeType = 'application/wasm'
        }

        respond({ mimeType, data })
      })
    },
    (error) => {
      if (error) {
        console.error(`Failed to register ${scheme} protocol`, error)
      }
    }
  )
}

主進程引入咱們修改的createProtocol.js

import createProtocol from './services/createProtocol'
const resources = process.resourcesPath

將原來的createProtocol('app')修改成
createProtocol('app', path.join(resources, './app.asar.unpacked'))

如今就能夠經過app://協議載入app.asar.unpacked下的文件了,打個包試試看看頁面可否正常加載,固然若是你是直接用file://協議加載本地文件的其改動也差很少,就是改變一下加載地址,準備工做完成了,咱們開始渲染進程增量更新的邏輯。

4. 模擬接口

這裏呢就很少說什麼了,和上一期全量更新同樣,不瞭解能夠去看看上一期內容,用http-server模擬接口返回,修改.env.dev0.0.2,打包生成unpacked.zip,放入server目錄下

{
  "code": 200,
  "success": true,
  "data": {
    "forceUpdate": false,
    "fullUpdate": false,
    "upDateUrl": "http://127.0.0.1:4000/unpacked.zip",
    "restart": false,
    "message": "我要升級成0.0.2",
    "version": "0.0.2"
  }
}

5. 渲染進程增量更新

這裏的頁面邏輯和上一期全量更新差很少,咱們檢測到更新用win-increment向主進程發送更新信息:

<template>
  <div class="increment">
    <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 { proxy } = getCurrentInstance()
    const config = cfg
    const api = proxy.$api
    const message = proxy.$message
    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(() => {
            if (!res.fullUpdate) {
              window.ipcRenderer.invoke('win-increment', res)
            }
          }).catch(err => {
            if (err.code === 0) {
              isClick && message.success('已爲最新版本')
            }
          })
        } else {
          message.success('請在打包環境下更新')
        }
      })
    }
    return {
      config,
      upDateClick
    }
  }
})
</script>

6. 主進程處理

ipcMain.js添加
import increment from '../utils/increment'

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

增量更新處理increment.js,經過upDateUrl下載增量包,下載完成以後,咱們先把原來的app.asar.unpacked重命名備份,若是出錯的話能夠還原,而後將下載的解壓,處理完成以後咱們能夠用reloadIgnoringCache從新加載頁面便可,固然你也能夠用app.relaunch()重啓應用

import downloadFile from './downloadFile'
import global from '../config/global'
import { app } from 'electron'
const path = require('path')
const fse = require('fs-extra')
const AdmZip = require('adm-zip')

export default (data) => {
  const resourcesPath = process.resourcesPath
  const unpackedPath = path.join(resourcesPath, './app.asar.unpacked')
  downloadFile({ url: data.upDateUrl, targetPath: resourcesPath }).then(async (filePath) => {
    backups(unpackedPath)
    const zip = new AdmZip(filePath)
    zip.extractAllToAsync(unpackedPath, true, (err) => {
      if (err) {
        console.error(err)
        reduction(unpackedPath)
        return
      }
      fse.removeSync(filePath)
      if (data.restart) {
        reLoad(true)
      } else {
        reLoad(false)
      }
    })
  }).catch(err => {
    console.log(err)
  })
}

function backups(targetPath) {
  if (fse.pathExistsSync(targetPath + '.back')) { // 刪除舊備份
    fse.removeSync(targetPath + '.back')
  }
  if (fse.pathExistsSync(targetPath)) {
    fse.moveSync(targetPath, targetPath + '.back') // 備份目錄
  }
}

function reduction(targetPath) {
  if (fse.pathExistsSync(targetPath + '.back')) {
    fse.moveSync(targetPath + '.back', targetPath)
  }
  reLoad(false)
}

function reLoad(close) {
  if (close) {
    app.relaunch()
    app.exit(0)
  } else {
    global.sharedObject.win.webContents.reloadIgnoringCache()
  }
}

封裝的下載文件downloadFile.js

const request = require('request')
const fs = require('fs')
const fse = require('fs-extra')
const path = require('path')

function download(url, targetPath, cb = () => { }) {
  let status
  const req = request({
    method: 'GET',
    uri: encodeURI(url)
  })
  try {
    const stream = fs.createWriteStream(targetPath)
    let len = 0
    let cur = 0
    req.pipe(stream)
    req.on('response', (data) => {
      len = parseInt(data.headers['content-length'])
    })
    req.on('data', (chunk) => {
      cur += chunk.length
      const progress = (100 * cur / len).toFixed(2)
      status = 'progressing'
      cb(status, progress)
    })
    req.on('end', function () {
      if (req.response.statusCode === 200) {
        if (len === cur) {
          console.log(targetPath + ' Download complete ')
          status = 'completed'
          cb(status, 100)
        } else {
          stream.end()
          removeFile(targetPath)
          status = 'error'
          cb(status, '網絡波動,下載文件不全')
        }
      } else {
        stream.end()
        removeFile(targetPath)
        status = 'error'
        cb(status, req.response.statusMessage)
      }
    })
    req.on('error', (e) => {
      stream.end()
      removeFile(targetPath)
      if (len !== cur) {
        status = 'error'
        cb(status, '網絡波動,下載失敗')
      } else {
        status = 'error'
        cb(status, e)
      }
    })
  } catch (error) {
    console.log(error)
  }
}

function removeFile(targetPath) {
  try {
    fse.removeSync(targetPath)
  } catch (error) {
    console.log(error)
  }
}

export default async function downloadFile({ url, targetPath, folder = './' }, cb = () => { }) {
  if (!targetPath || !url) {
    throw new Error('targetPath or url is nofind')
  }
  try {
    await fse.ensureDirSync(path.join(targetPath, folder))
  } catch (error) {
    throw new Error(error)
  }
  return new Promise((resolve, reject) => {
    const name = url.split('/').pop()
    const filePath = path.join(targetPath, folder, name)
    download(url, filePath, (status, result) => {
      if (status === 'completed') {
        resolve(filePath)
      }
      if (status === 'error') {
        reject(result)
      }
      if (status === 'progressing') {
        cb && cb(result)
      }
    })
  })
}

增量更新的基本邏輯就完成了,若是你是採用方案一的話,也能夠參考一下流程,點擊渲染進程的檢測更新,看看版本變成0.0.2沒有
app.asar.unpacked
asar

方案缺陷處理

前面咱們說了,此方案有個缺點就是主進程中的環境變量不會改變,那麼咱們在主進程中經過process.env.VUE_APP_VERSION獲取版本號拿到的仍是以前的版本號。
咱們的渲染進程是從新打包的,故其環境變量都是準確的,此時咱們能夠在頁面加載時,從渲染進程把配置信息發送給主進程。

renderer的App.vue:
import cfg from '@/config'
window.ipcRenderer.invoke('win-envConfig', cfg)

global.js:
global.envConfig = {}

main的ipcMain.js:
import global from '../config/global'
ipcMain.handle('win-envConfig', (_, data) => {
  global.envConfig = data
})

再也不使用process.env.VUE_APP_VERSION獲取版本號信息,使用global.config.VUE_APP_VERSION獲取,從新打個0.0.2的包試試。

補充說明

  • 這裏呢只是簡單的完成了增量更新的邏輯,若是你想要一個下載進度呀,能夠本身實現一下
  • 通常來講這類增量更新包在上傳時會將地址保存到數據庫中,能夠作一下安全處理,好比在保存時附加文件的md5或sha呀,而後在增量更新下載完成後本地校驗是否一致再進行解壓,保證文件準確性。
  • 固然還有解壓失敗處理,假如咱們的增量更新包損壞了,雖然咱們有備份,可是重啓仍是會拉取更新包進行更新,若是使用了重啓更新的話,就陷入了死循環了,這裏能夠作一個版本更新重啓記錄,超過多少次後,就再也不對這個版本的包進行處理了。

固然增量更新還有其餘的方式實現,一期講完太多了,其餘方案咱們放到下一期繼續。

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

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

相關文章
相關標籤/搜索