不好意思,鸽了挺久了,上一期我们使用app.asar.unpacked完成了electron的增量更新,本期我们介绍的是如何使用exe替换asar来实现增量更新。
本期内容是基于上一期的内容来讲解的,且主要针对于Windows系统(mac系统可以对app.asar修改)

替换难点

  1. 在使用了asar后,Windows系统的Electron应用启动后其app.asar会被占用,不能对其进行修改和删除,必须结束Electron应用的进程后才可以对其进行修改。
  2. 由于Windows的uac限制,如果安装在C盘的话,修改app.asar会有权限问题。

解决思路

其实呢子线程也可以跑node,但是呢由于主进程结束了我们并没有node环境运行node命令,所以此方法不通,当然你也可添加一个编译好的node运行子线程js,但是体积问题得不偿失。 在Windows下我们可以用批处理文件bat来处理文件,但是bat还是有uac以及执行时会有cmd窗口,我们可以把写好的bat文件转换为exe文件来解决这些问题。

  1. 我们可以使用node衍生独立存在的子进程,把子进程和主进程分离开,结束掉Electron应用的进程,用这个子进程进行替换。
  2. uac限制可以使用exe文件来获取管理员权限进行处理。

解决方案

1. 打包修改

我们这里去除之前的app.asar.unpacked打包,把之前vue.config的注释掉,这样我们打包就只有asar文件了

 1// extraResources: [{
 2//   from: "dist_electron/bundled",
 3//   to: "app.asar.unpacked",
 4//   filter: [
 5//     "!**/icons",
 6//     "!**/preload.js",
 7//     "!**/node_modules",
 8//     "!**/background.js"
 9//   ]
10// }],
11// files: [
12//   "**/icons/*",
13//   "**/preload.js",
14//   "**/node_modules/**/*",
15//   "**/background.js"
16// ],

2. 构建增量zip

上一期构建增量zip时使用了afterPack钩子,这里我们对其修改,把新版本的app.asar重命名为update.asar,放入增量的app.zip包里,不了解的可以看看上一期的内容

 1const path = require('path')
 2const AdmZip = require('adm-zip')
 3const fse = require('fs-extra')
 4
 5exports.default = async function(context) {
 6  let targetPath
 7  if(context.packager.platform.nodeName === 'darwin') {
 8    targetPath = path.join(context.appOutDir, `${context.packager.appInfo.productName}.app/Contents/Resources`)
 9  } else {
10    targetPath = path.join(context.appOutDir, './resources')
11  }
12  const asar = path.join(targetPath, './app.asar')
13  fse.copySync(asar, path.join(context.outDir, './update.asar'))
14  var zip = new AdmZip()
15  zip.addLocalFile(path.join(context.outDir, './update.asar'))
16  zip.writeZip(path.join(context.outDir, 'app.zip'))
17  fse.removeSync(path.join(context.outDir, './update.asar'))
18}

3. 模拟接口

同上一期,我们这里修改了upDateUrl和upDateExe,upDateUrl是增量zip,upDateExe是我们用来替换asar的exe文件。

 1{
 2  "code": 200,
 3  "success": true,
 4  "data": {
 5    "forceUpdate": false,
 6    "fullUpdate": false,
 7    "upDateUrl": "http://127.0.0.1:4000/app.zip",
 8    "upDateExe": "http://127.0.0.1:4000/update.exe",
 9    "restart": false,
10    "message": "我要升级成0.0.2",
11    "version": "0.0.2"
12  }
13}

4. 加载策略修改

这里我们再把加载策略修改为加载app.asar里的文件

 1// createProtocol('app', path.join(resources, './app.asar.unpacked'))
 2createProtocol('app')

5. 渲染进程增量更新

这个没变动,同上一期

6. 主进程处理

我们把之前的主进程下载修改一下,先判断update.exe是否存在,不存在的话先下载update.exe放入app.getPath('userData')里:

 1winC:\Users\Administrator你的用户)\AppData\Roaming\<app name>\
 2mac/Users/你的用户/Library/Application Support/<app name>

app.getPath('userData')这个路径呢,比较特殊,安装之后就存在了,是数据文件,你的indexDB,localStorage等都存在这里面,软件的全量更新,卸载都不会改动这个文件里的东西, 我们把update.exe放入这里,避免全量更新后重新安装。之后下载增量更新包解压到resourcesh中(update.asar),也就是和app.asar同级目录,删除zip包,运行app.exit(0)关闭主进程

 1import downloadFile from './downloadFile'
 2import { app } from 'electron'
 3const fse = require('fs-extra')
 4const path = require('path')
 5const AdmZip = require('adm-zip')
 6
 7export default async (data) => {
 8  const resourcesPath = process.resourcesPath
 9    if (!fse.pathExistsSync(path.join(app.getPath('userData'), './update.exe'))) {
10      await downloadFile({ url: data.upDateExe, targetPath: app.getPath('userData') })
11    }
12    downloadFile({ url: data.upDateUrl, targetPath: resourcesPath }).then(async (filePath) => {
13      const zip = new AdmZip(filePath)
14      zip.extractAllToAsync(resourcesPath, true, (err) => {
15        if (err) {
16          console.error(err)
17          return
18        }
19        fse.removeSync(filePath)
20        app.exit(0)
21      })
22    }).catch(err => {
23      console.log(err)
24    })
25}

7. 子进程处理

主进程关闭后会触发quit事件,我们在这个事件里检测update.exe以及update.asar是否同时存在, 同时存在的话我们用spawn开启一个子进程运行我们的update.exe,并且传入resourcesPath(app.asar所在目录路径),app.getPath('exe')(我们软件的启动路径), 使用child.unref()让子进程和父进程分离,可以不退出子进程的情况下退出父进程。

 1const { spawn } = require('child_process')
 2const fse = require('fs-extra')
 3const fs = require('fs')
 4const resourcesPath = process.resourcesPath
 5
 6app.on('quit', () => {
 7  console.log('quit')
 8  if (fse.pathExistsSync(path.join(app.getPath('userData'), './')) && fse.pathExistsSync(path.join(resourcesPath, './update.asar'))) {
 9    const logPath = app.getPath('logs')
10    const out = fs.openSync(path.join(logPath, './out.log'), 'a')
11    const err = fs.openSync(path.join(logPath, './err.log'), 'a')
12    const child = spawn(`"${path.join(app.getPath('userData'), './update.exe')}"`, [`"${resourcesPath}"`, `"${app.getPath('exe')}"`], {
13      detached: true,
14      shell: true,
15      stdio: ['ignore', out, err]
16    })
17    child.unref()
18  }
19})

也就是说这里是父进程退出后,子进程执行我们的exe,替换app.asar,out和err是将子进程执行的日志重定向到app.getPath('logs')中,这个路径和electron-log不一样(你也可以自己设置为electron-log路径一样)

 1winC:\Users\Administrator你的用户)\AppData\Roaming\<app name>\<app productName>\logs
 2mac: ~/Library/Logs/<app name>应该是这个下面的这个我没验证
 3

8. 构建exe

准备工作完成了,这里我们编写exe,其实这个没啥难度的,我们使用bat脚本打包成exe就行。
update.bat

 1@echo off
 2timeout /T 1 /NOBREAK
 3del /f /q /a %1\app.asar
 4ren %1\update.asar app.asar
 5start "" %2

简单解释一下吧,%1和%2为运行脚本传入的参数,比如update.bat aaa bbb,那么%1为aaa,%2为bbb,上面我们用spawn运行exe时传入的, 也就是%1为resourcesPath,%2为软件的启动exe,我们运行bat脚本,先暂停1秒钟保证主进程退出了,然后删除app.asar,将update.asar重命名为app.asar,启动软件exe。
一个简单的bat替换就完成了,我们下载Bat To Exe Converter这个软件,将update.bat转换为update.exe,然后将update.exe放入我们的http-server目录中。运行软件检测更新,看看更新是否完成。

补充说明

 1spawn(`"${path.join(app.getPath('userData'), './update.exe')}"`, [`"${resourcesPath}"`, `"${app.getPath('exe')}"`], {
 2  detached: true,
 3  shell: true,
 4  stdio: ['ignore', out, err]
 5})

这里有同学可能会有疑问,为什么要在几个路径外加一个"",这是由于node运行脚本的路径名中包含空格的话,需要加上引号,bat处理也一样,比如我们的软件安装在c盘, C:\Program Files\electronVueDEV,最常见的问题就是Program Files这里有个空格,这会导致bat命令里有这样的路径的话会处理失败,所以我们的路径都加了引号的。

本系列更新只有利用周末和下班时间整理,比较多的内容的话更新会比较慢,希望能对你有所帮助,请多多star或点赞收藏支持一下

本文地址:xuxin123.com/electron/in…
本文github地址:链接

个人笔记记录 2021 ~ 2025