上一期咱們完成了electron的全量更新,本期咱們介紹的是如何只修改部分文件以實現增量更新的幾種方案。javascript
咱們將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的插件,讓7z也能打開asar,連接,若是你的7z是安裝在c盤,把Asar.64.dll
(64位系統)放入C:\Program Files\7-Zip\Formats\
裏,Formats
沒有的話,本身新建一個。
java
這是一種比較常見的方式,好比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
的限制性,好比文件內只是可讀,一些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
app.asar
,只對渲染進程文件進行替換。實現步驟:
首先設置一下咱們想要替換的那些文件,打包時會先生成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
裏面的除了icons
,background.js
等文件放入app.asar
,其他的都放入app.asar.unpacked
,打包看看,看看app.asar.unpacked
裏面是不是咱們想要的東西。
如今咱們有了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
,這個就是咱們的增量包了。
在窗口啓動篇咱們說過,咱們渲染進程的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://
協議加載本地文件的其改動也差很少,就是改變一下加載地址,準備工做完成了,咱們開始渲染進程增量更新的邏輯。
這裏呢就很少說什麼了,和上一期全量更新同樣,不瞭解能夠去看看上一期內容,用http-server模擬接口返回,修改.env.dev
爲0.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" } }
這裏的頁面邏輯和上一期全量更新差很少,咱們檢測到更新用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>
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
沒有
前面咱們說了,此方案有個缺點就是主進程中的環境變量不會改變,那麼咱們在主進程中經過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的包試試。
固然增量更新還有其餘的方式實現,一期講完太多了,其餘方案咱們放到下一期繼續。
本系列更新只有利用週末和下班時間整理,比較多的內容的話更新會比較慢,但願能對你有所幫助,請多多star或點贊收藏支持一下