前言
很久以前写过一篇electron的自动更新实例,在这个里面发现无论我更改了任何东西,都必须要下载全部的更新包,安装后才能实现更新。如果我们只是更改了一部分渲染进程的代码甚至只是更新了几张图片,就需要下载全部的安装包,这样的操作对于用户来说其实是有点太不友好了。于是就有了热更新的想法,在我们只有静态资源更新或者渲染进程发生变化的时候,就可以只更新渲染进程就可以实现更新,而不必下载全部的安装包;这样的更新会更加的友好更加的便捷。
实现壹、
如果你只是想用electron的壳,而不是想用electron中带有的node环境,拿就直接使用网站的更新就好了,
win.mainWin.loadURL(winURL) // winURL 你网站的URL地址,
这样的话就是一旦你网站发生变化,你的程序就自动发生变化,但是在网页版中无法使用electron自带的node环境,这样的话有些方法就会无法使用,例如文件系统等等;一般我们在开发的时候很少使用这种方式,而是使用内嵌式的开发,即页面写在electron框架内部的,这个种形式的热更新才是我们所需要重点关注的对象。
实现贰、
在electron-vue的框架内部,热更新是不支持的,或者说electron-builder是不支持热更新的;所以我们要用其他方法来实现热更新,下面是我实现热更新的原理;
注:以下所有操作都是在配置asar为false的基础上进行的
在这个流程图中可以发现有以下几个问题:
- 渲染进程如何和主进程分开打包
- 如何压缩渲染进程的文件成zip格式
- 如何判断当前版本是热更新版本需要更新的版本;或者说热更新版本存放在哪个位置
- 在主进程中如何解压渲染进程的zip文件,删除原有的渲染进程的文件夹
- 更新完成后如何重启
ok 我们现在来一个一个解决这些疑问
① 渲染进程如何和主进程分开打包
在electron-vue中的所有webpack配置都在.electron-vue文件夹中,处理这个问题主要还是要修改webpack配置。
在阅读过electron-vue所有webpack配置后,发现在webpack.renderer.config.js文件中配置渲染进程的打包后输出文件的位置与主进程的输出路径是一致,为了实现热更新,所以就改!!
...
// webpack.renderer.config.js文件中的配置修改为
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron/bundle')
},
...
执行yarn build 查看打包后的文件夹展示是否为
bundle为渲染进程为文件夹
static 额外的静态文件
main.js 入口文件,或者称为主进程文件
这样我们就解决了第一个问题,即渲染进程和主进程分开打包;如果我们需要热更新就可以只更新bundle文件夹就可以了
② 如何压缩渲染进程的文件成zip格式
因为我们使用的electron-vue,本身就自带node环境(以后不会重复这句话,都烦了不懂自己翻翻electron官方文档看看),所以我们可以使用node自带的文件系统;在文件压缩成zip的工具中我使用的是jszip;
## 引入需要的包
yarn add -D jszip
在.electron-vue文件夹中新建一个build-zip.js 文件
/**
* 打包结束后将其压缩成为.zip文件
*/
const JSZip = require('jszip')
const path = require('path')
const fs = require('fs')
const zip = new JSZip()
/**
* 读取文件夹中的文件
*/
const readDir = (filePath, jszip, name = null) => {
let files = fs.readdirSync(filePath)
files.forEach((fileName) => {
let fillPath = `${filePath}/${fileName}`
let file = fs.statSync(fillPath)
if (file.isDirectory()) {
const folderPath = name ? `${name}/${fileName}` : fileName
let disList = zip.folder(folderPath)
readDir(fillPath, disList, folderPath)
} else {
jszip.file(fileName, fs.readFileSync(fillPath))
}
})
}
const buildZip = async () => {
// 文件打包后渲染进程文件输出的文件夹
const filePath = path.join(__dirname, '../build/win-ia32-unpacked/resources/app/dist/electron/bundle/')
readDir(filePath, zip)
const content = await zip.generateAsync({
type: 'nodebuffer',
compression: "DEFLATE",
compressionOptions: {
level: 9
}
})
fs.writeFileSync(path.join(__dirname, '../build/bundle.zip'), content, "utf-8")
}
buildZip()
在package.json中添加
{
"scripts": {
"build:zip": "node .electron-vue/build-zip.js",
}
}
这个scripts必须在打包完成后才能执行,否者压缩的文件不是最新的,甚至没有压缩文件产出;
打完包后产出;
这样的话就实现了,渲染进程的压缩;在更新中就可以实现只下载bundle.zip就可以更新;以前需要下载大约174MB的文件,现在只需要下载18MB的文件,下载约节省了90%流量,大大提高用户的更新速度
③ 如何判断当前版本是热更新版本需要更新的版本;或者说热更新版本存放在哪个位置
- 热更新版本存放的位置
在看过我以前写的electron的自动更新文章里提到过一个添加更新日志的配置,同样在这里我使用了一个小小的技巧;更改原有字段的含义,以方便我们使用。在原有的releaseInfo中的配置中有以下几个配置
* releaseInfo - The release info. Intended for command line usage: -c.releaseInfo.releaseNotes="new features"
* releaseName String - The release name.
* releaseNotes String - The release notes.
* releaseNotesFile String - The path to release notes file. Defaults to release-notes-${platform}.md (where platform it is current platform — mac, linux or windows) or release-notes.md in the build resources.
* releaseDate String - The release date.
* target String | TargetConfiguration
这几个配置,观察了一圈貌似只有releaseName的作用不是那么大,那就用这个配置了;将releaseName约定成热更新版本号;这样的话在使用electron-builder的检查更新机制的时候,就可以使用这个字段了。
但是我们发现在打包压缩的时候,package,json是不在bundle文件夹中,为了避免这个问题,我们在打包的时候将热更新的版本号重新写入到渲染进程的文件夹中,具体实现:
const fs = require('fs')
const path = require('path')
const { build,version } = require('../package.json')
const bundle = path.join(__dirname,'../src/renderer/bundle.json')
const { releaseInfo } = build
const setHotUpdateVersion = async () => {
const hotVersion = { version: releaseInfo.releaseName, releaseVersion: version}
await fs.writeFileSync(bundle, JSON.stringify(hotVersion))
}
module.exports = {
setHotUpdateVersion
}
导出的bundle.json在后续的文件中可扩展,方便添加其他更多的配置内容。
-
如何判断当前版本是热更新版本需要更新的版本
1、使用electron-builder的默认检查更新机制
因为是热更新,所以版本号是不会发生变化的。所以一定是在没有可用更新中来检查是否更新
// 检测没有可用更新时发出 autoUpdater.on('update-not-available', info => { // info 的信息基本上就是 releaseInfo里面的内容 global.mainWindow.webContents.send('hotVersion', info) })我们可以在渲染进程中判断当前版本号是否小于检查版本号,这个里面我们就需要用到
compare-versions;// yarn add compare-versions 添加包 import compareVersions from 'compare-versions' compareVersions('0.1.2','0.1.1') // -1 compareVersions('0.1.2','0.1.2') // 0 compareVersions('0.1.2','0.1.3') // 1依据这个就可以判断是否触发热更新
④ 在主进程中如何解压渲染进程的zip文件,删除原有的渲染进程的文件夹
- 下载压缩文件
const filePath = path.join(__dirname, './bundles.zip') // 下载包存放地址
downloadBundleZip() {
return new Promise((resolve, reject) => {
const { channel, updaterUrl } = updaterChannel
const downloadUrl = `${你的热更新下载地址}bundle.zip`
const file = fs.createWriteStream(filePath)
Http.get(downloadUrl, response => {
if (response.statusCode !== 200) {
reject()
}
// After downloading, unzip the file
response.on('end', () => {
file
.on('finish', async () => {
await this.renameFileInDir() // 重命名文件夹
await this.UnzipBundleZip() // 解压文件夹
await this.deleteFileInDir() // 删除文件
app.relaunch()
app.exit()
file.close()
resolve()
})
.on('error', () => {
fs.unlink(filePath, e => {})
reject()
})
})
response.pipe(file)
})
})
}
当文件下载完成后,需要将原有的bundle文件夹给重命名,这样才能保证下载的zip文件,能够顺利解压。
- rename bundle文件夹
// 重命名文件夹
async renameFileInDir() {
const localPath = path.join(__dirname, './bundle')
const newPath = path.join(__dirname, '/bundle_bak')
await renameAsync(localPath, newPath)
}
// 封装的函数
function renameAsync(oldPath, newPath) {
return new Promise(resolve => {
fs.rename(oldPath, newPath, error => {
resolve(error)
})
})
}
- 解压bundle.zip文件
解压文件需要adm-zip,所以先安装
yarn add adm-zip
安装完成后,编写代码
import ADMZip from 'adm-zip'
async UnzipBundleZip() {
// 先判断文件目录是否存在
const exist = fs.existsSync(filePath)
if (exist) {
const admZip = new ADMZip(filePath)
// 解压文件
await admZip.extractAllTo(path.join(__dirname, './bundle'), true)
fs.unlinkSync(filePath)
}
}
- 删除原有的bundle文件夹
// 封装的删除文件夹函数
function deleteDir(dir) {
return new Promise((resolve, reject) => {
// 先读文件夹
try {
fs.stat(dir, (err, stat) => {
if (stat.isDirectory()) {
fs.readdir(dir, (err, files) => {
let Files = files.map(file => path.join(dir, file)) // a/b a/m
Files = Files.map(file => deleteDir(file)) // 这时候变成了promise
Promise.all(Files).then(() => {
fs.rmdir(dir, resolve)
})
})
} else {
fs.unlink(dir, resolve)
}
})
} catch (error) {
reject(error)
}
})
}
// 调用删除文件夹方法
async deleteFileInDir() {
const localPath = path.join(__dirname, './bundle_bak')
const exist = fs.existsSync(localPath)
if(exist){
try {
await deleteDir(localPath)
} catch (error) {
console.log(error)
}
}
}
⑤ 更新完成后如何重启
我们在这一系列的操作后,发现有些东西还是没有发生变化;这个时候我们就需要将应用程序给重启;
依据electron官方文档上的说明,我们可以看出有个app.relaunch() 方法,按照文档的方法,我们在经过上述四步方法后,将应用重启;这样我们就实现了热更新。
结束语
这个热更新方案还是有些小问题的,就是一旦主进程发生变化,就无法通过热更新来实现版本的迭代。就需要用户来下载最新的包来实现更新迭代;但是对于上一个版本的更新,已经有了很大的进步。如果你们有更好的方法可以随时@我
鸣谢: 我辉哥提供的思路!!