背景:最近采用Electron改造公司的ERP客户端,由于界面功能较多,最终打包的文件有130M,更关键的问题是系统的更新比较频繁基本每月都会有一次版本的迭代更新。Electron虽然提供系统自动更新的功能,但采用的是整个系统文件全部替换的方式,如果更新文件较大,频率高,且用户体量较大的情况下采用Electron自带的系统更新功能显然不是很合适。
思路:Electron分为主进程和渲染进程,主进程的功能主要是使用BrowserWindow 实例创建页面,主进程管理所有的web页面和它们对应的渲染进程。可以这么理解无论我们的系统多么复杂、庞大,实际都是由单个独立的web页面组成的,每个页面对应着渲染进程。我们系统的功能的更新主要是针对渲染进程,本质上就是更新功能对应的web页面及其页面包含的图片、样式及实现业务功能的js文件(这些文件都是可以从服务端远程下载到本地运行的)。从主进程和渲染进程的用途着手,主进程主要负责调度各个渲染进程打开、关闭、销毁,渲染进程负责各个功能的业务逻辑的实现,主进程和渲染进程之间通过ipcMain消息的方式进行相互通讯。我们的前提是主进程不更新,所以需要把主进程的功能简化或转移到渲染进程中实现,主进程通过ipcMain消息的方式接受渲染进程发送的消息去实现页面的打开、关闭、系统退出等功能。
实现步骤
- 搭建远程服务端系统
提供系统当前最新的版本号及当前版本号需要更新的文件(html页面、css文件、图片、js文件等) - 编写主进程
- 系统启动时,获取本地系统的版本信息,项目使用了electron-json-storage存储本地的版本信息。
- 访问远程服务器,获取当前最新版本记录,同本地版本进行对比,不一致采用强制更新,启动更新页面显示更新进度及更新日志,系统自动下载更新的文件,保存并替换到本地的文件。
- 启动系统入口页面、安全登录验证页面。
- 处理ipcMain消息,实现页面的打开、关闭、系统退出等功能。
部分代码:
- 服务端功能简单,用nodejs实现文件下载,获取当前版本号功能
var express = require('express');
var app = express();
app.use(express.static('resource'));
var server = app.listen(92, function () {
var host = server.address().address
var port = server.address().port
console.log("应用实例,访问地址为 http://%s:%s", host, port)
})
app.get('/', function (req, res) {
var config = require('./config');
var json = JSON.stringify(config)
res.setHeader("Content-Type", "application/json");
res.write(json);
res.end();
})
配置文件:
var config = {
version: '1.0.0.3', // 版本号
file:['js/background-bundle.js','page/login.html'],
detail:['更新登陆页面']
};
module.exports = config;
- Electron主线程代码:
app.on('ready', async function () {
getserverinfo()
})
//获取服务端的版本和本地本部对比,不一致启动热更新
function getserverinfo() {
settingDB.getItem('version', (result) => {
let curversion = ''
if (result.version) {
curversion = result.version
}
commonfun.getHttpData(global.backgroundparam.autoUpdateUrl, (data) => {
let serverconfig = JSON.parse(data)
global.backgroundparam.serverurl = serverconfig.serverurl
//curversion = '1.0.0'
if (curversion != serverconfig.version) {
//启动自动更新页面
let updateconfig = {}
if (curversion == '')
curversion = '1.0.0'
Object.assign(updateconfig, {localversion: curversion}, serverconfig)
hotupdatepage.setup(updateconfig)
settingDB.setItem('version', {version: serverconfig.version})
} else {
//启动安全验证界面
securityvalid.setup()
}
}, () => {
dialog.showMessageBox(null,
{
type: 'error', buttons: ['确定'], title: '系统启动失败', message: '系统启动失败,请联系管理员!'
})
app.exit()
})
})
}
//根据配置打开新页面
ipcMain.on('openpage', ({sender}, pageconfig) => {
let {x, y, width, height} = pageconfig
let pagewindow = new BrowserWindow({
titleBarStyle: 'hiddenInset',
autoHideMenuBar: true,
fullscreenable: false,
frame: false,
x,
y,
width,
height,
defaultEncoding: 'UTF-8',
webPreferences: {
nodeIntegration: true,
webviewTag: true
}
})
let page = {
id: (Math.random() * 1000 | 0) + Date.now(),
browserwin: pagewindow,
name: pageconfig.name
}
pages.push(page)
pagewindow.loadURL(`file://${global.backgroundparam.rootpath}/page/${pageconfig.path}`)
//pagewindow.webContents.openDevTools()
pagewindow.show()
pagewindow.webContents.on('did-finish-load', () => {
pagewindow.webContents.send('receivepageID', page.id)
})
})
//根据ID关闭页面
ipcMain.on('closepagebyself', ({sender}, arg) => {
for (var i = 0; i < pages.length; i++) {
if (pages[i].id === arg) {
pages[i].browserwin.close()
pages.splice(i, 1)
return
}
}
})
//根据名称关闭页面
ipcMain.on('closepagebypagename', ({sender}, arg) => {
for (var i = 0; i < pages.length; i++) {
if (pages[i].name == arg) {
pages[i].browserwin.close()
pages.splice(i, 1)
return
}
}
})
//中转主页面消息命令
ipcMain.on('mainpage:command', ({sender}, commandtype, paramobj) => {
let mainpagewin = getMainPageWin()
if (mainpagewin != null) {
mainpagewin.webContents.send('command', commandtype, paramobj)
}
})
//退出系统
ipcMain.on('existapp', function () {
app.quit()
})
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') {
app.quit()
}
})
//获取操作主页面
function getMainPageWin() {
for (var i = 0; i < pages.length; i++) {
if (pages[i].name == 'mainpage') {
return pages[i].browserwin
}
}
return null
}