如何在Electron中实现一个简易的更新功能?

文章详细介绍了如何利用Electron的autoUpdater模块实现应用的自动更新,包括设置更新URL、处理Windows和macOS的更新差异,以及解决更新过程中遇到的错误,如乱码问题、更新锁获取失败和服务器403错误。文章还探讨了更新机制,指出更新时新版本文件被下载到特定目录,重启后应用自动运行新版本,实现静默更新。
摘要由CSDN通过智能技术生成

在这里插入图片描述

前言

上一篇文章《【玩转Electron】超详细的使用教程》中我提到了关于更新的问题,由于内容篇幅较长单独出一篇博客,于是便有了这篇文章。

关于Electron的更新官方其实已经提供了几种很便捷的方案
但是不是需要github,就是需要搭建一个服务端,因为我们的场景很小,electron只是一个壳,所以更新的需求不强烈,只是一个以防万一的功能,所以我们想寻求一个简单的方式来处理。

自带更新功能

electron就自带了更新功能,即autoUpdater。使用也很简单只需要几步,如下:

const { autoUpdater } = require('electron')
//先设置更新的url
autoUpdater.setFeedURL({url: "https://xxxxxx"});
//在合适的时机检查更新
autoUpdater.checkForUpdates();

其实这样就可以了,checkForUpdates会检查更新并自动下载安装,全程无感知。当重启应用的时候就会是新版本的了。

当然这是最简单的步骤,我们后面会丰富一下功能。

这里有几个问题。

首先,mac上如果想更新,那么必须是签名的应用,目前我们的mac应用未签名,所以不能使用,会提示。
Error: Could not get code signature for running application

其次,就是更新url,这地址对应的是什么?我们如何方便快捷的构建出一个更新服务?

在官方文档中没有详细的描述这个地址对应的是什么,因为如果使用官方提供的几种服务后台,可以通过后台界面直接添加一个更新即可,其他的无需关心。但是我们又不打算使用官方提供的方案,那么我们就必须自己研究出这个url对应的是什么?是文件?配置数据?

url背后的东西

经过我几天的摸索,查阅相关文档和源码,最终确定了url背后的东西。因为我们目前只考虑windows,所以下面都是以windows为准。

我们用forge通过squirrel-maker来创建windows安装包,创建后文件路径是项目根目录/out/make/squirrel.windows/x64/xxxx.exe。

但是同目录下还同时生成了另外两个文件RELEASESxxx.nupkg,这就是我们更新所需要的文件,其中RELEASES相当于配置文件,里面记录着nupkg文件的完整名称、SHA512(用于校验)和文件大小,如下:
674802FE0AE3B272F5182E4626893FDB2D8D2107 xxxxxx-0.1.0-full.nupkg 76560314

所以我们将这两个文件上传到文件服务器上,放在同一个目录下(或虚拟目录),然后将目录的地址设置为feedUrl即可。这样autoUpdater会自动下载该目录下的RELEASES文件并读取配置,然后通过拿到的文件名下载更新文件并校验,成功后即自动后台安装。

如果我们观察应用的根目录就会发现,实际上在应用根目录有以不同版本号命名的目录,后台安装实际上就是将新版本下载后解压到根目录中新版本号的目录中,然后重启的时候,执行文件exe就会使用新版本号的目录中的文件运行,这样就完成了更新。而旧版本的文件实际上还存在根目录中。所以才会无感知的进行安装,因为不需要删除修改文件(需要修改很少的配置文件)。

注意:还有一个electron-updater模块,本来electron自带的autoUpdater和这个模块里的是一样的,但是新版本的electron中貌似不一样了,导致feedurl直接用这个目录也不行了,不过改成electron-updater(6.1.4)就可以了。

所以这时候就需要线安装electron-updater, 然后将代码改成

const { autoUpdater } = require('electron-updater')
//先设置更新的url
autoUpdater.setFeedURL({url: "https://xxxxxx"});
//在合适的时机检查更新
autoUpdater.checkForUpdates();

再说一下mac,打包(用electron-builder)之后在生成dmg文件,如果在package.json中配置了为build选项配置了publish,那么在同目录下会生成latest-mac.yml文件,这个文件和dmg文件就是对应windows那两个文件的,将这两个文件放到一个目录下,然后将这个目录url设置给feedurl即可。

问题

其实并没有这么顺利,下面总结了中间遇到的几个问题。

出错弹窗乱码,查看详细日志

如果electron运行时出错,那么就会弹窗提示,但是在实际运行中发现,如果错误信息中有中文,那么就会导致错误信息乱码。这样就无法看到准确的信息。

如何处理呢?

在应用的根目录(安装目录,一般在c:/用户/[用户名]/AppData/Local/[应用名])会生成一个SquirrelSetup.log的日志文件,这里面就记录着错误的详细信息。

Couldn’t acquire lock, is another instance running

查看SquirrelSetup.log看到这个错误的详细信息如下:

2021-04-25 15:09:13> SingleGlobalInstance: Failed to grab lockfile, will retry: C:\Users\guozh\AppData\Local\Temp\.squirrel-lock-68CEC12091756AFBF3BF0445D48359FFDABDAB12: System.IO.IOException: 文件“C:\Users\guozh\AppData\Local\Temp\.squirrel-lock-68CEC12091756AFBF3BF0445D48359FFDABDAB12”正由另一进程使用,因此该进程无法访问此文件。
   在 System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
   在 System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath, Boolean checkHost)
   在 System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
   在 Squirrel.SingleGlobalInstance..ctor(String key, TimeSpan timeOut)
2021-04-25 15:09:14> Unhandled exception: System.AggregateException: 发生一个或多个错误。 ---> System.Exception: Couldn't acquire lock, is another instance running
   在 Squirrel.SingleGlobalInstance..ctor(String key, TimeSpan timeOut)
   在 Squirrel.UpdateManager.<acquireUpdateLock>b__32_0()
   在 System.Threading.Tasks.Task`1.InnerInvoke()
   在 System.Threading.Tasks.Task.Execute()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   ...
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Update.Program.<CheckForUpdate>d__8.MoveNext()<---

出现这个错误怀疑是与electron编译的安装包有关,运行安装包的时候会展示安装动画,但是安装完成已经打开应用了,动画还没有消失,有时候甚至持续几分钟。

应用一打开的时候就会进行更新,所以这时候有可能应用进程和安装器进程有冲突,导致上面的问题。
目前这个问题还没有很好的规避,但是可以通过注册处理autoUpdater的error事件进行规避,如下:

autoUpdater.on('error', (error) => {
    //dialog.showMessageBox({message:"error:" + error.name + "," + error.message + "," + error.stack})
    console.log("error:" + error.name + "," + error.message + "," + error.stack)
  });

在添加了这样的代码后,就不会再弹窗提示了。但是实际问题还存在,在SquirrelSetup.log中还会记录相关错误,而且更新中断。

所以这并不是解决办法,这样处理后会导致第一次启动更新大概率失败,不过再次启动的时候就会正常更新了,所以暂时可以接受。

服务器403

查看SquirrelSetup.log看到这个错误的详细信息如下:

2021-04-25 14:51:43> CheckForUpdateImpl: Download resulted in WebException (returning blank release list): System.Net.WebException: 远程服务器返回错误: (403) 已禁止。
   在 System.Net.HttpWebRequest.EndGetResponse(IAsyncResult asyncResult)
   在 System.Net.WebClient.GetWebResponse(WebRequest request, IAsyncResult result)
   在 System.Net.WebClient.DownloadBitsResponseCallback(IAsyncResult result)
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.Utility.<LogIfThrows>d__43`1.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 Squirrel.FileDownloader.<DownloadUrl>d__3.MoveNext()
--- 引发异常的上一位置中堆栈跟踪的末尾 ---
   在 System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   在 System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
   在 Squirrel.UpdateManager.CheckForUpdateImpl.<CheckForUpdate>d__2.MoveNext()

其实上面只是告诉我们服务端返回了403,至于为什么并没有说明。url是没问题的,文件也存在,在浏览器中也可以访问,为什么会出现403。最后通过charles抓包发现,服务器返回的是:

{
    "code": "40310011",
    "msg": "invalid User-Agent header"
}

在charles中查看这个请求的header发现没有User-Agent,所以应该就是这里出现的问题。

通过postman我们模拟请求,发现当删除User-Agent就会出现上面的错误,随便添加一个就可以正常访问。

因为应用用的是electron自带的更新,所以无法干预这个请求,那么就从服务器这边入手。经过测试发现七牛没有这样的问题,即使没有User-Agent也可以正常访问,所以应该是upyun有什么配置。

替换成功七牛后就可以正常访问了。

完整功能

上面只是最简单的步骤,打开应用后就会自动检测更新,又更新就自动下载安装。用户无感知,所以不知道何时更新,只有用户关闭重启应用后才会使用新版本。所以我们需要通知用户。

autoUpdater有很多事件回调,我们上面提到了error,我们就通过监听这些事件来通知用户,这样就实现了更新功能,相对于官方的方案更简单轻量,后续只要更新服务器上的两个文件即可。

完整源码如下:

const { app, BrowserWindow, globalShortcut, Menu, dialog } = require('electron')
const { autoUpdater } = require('electron-updater')
var win;


if(require('electron-squirrel-startup')){
    return app.quit();
}


function createWindow () {
  win = new BrowserWindow({
    width: 1280,
    height: 744,
    minWidth: 1280,
    minHeight: 744,
    webPreferences: {
      nodeIntegration: true,
      webSecurity: false
    }
  })


  win.loadURL('https://appd.knowbox.cn/class/#main')
   
  globalShortcut.register('Alt+CommandOrControl+Shift+D', () => {
    win.webContents.openDevTools({mode:'detach'}) //开启开发者工具
  })


  //设置url
  autoUpdater.setFeedURL({url: "https://xxxxxx"});
  //开始检查更新回调
  autoUpdater.on('checking-for-update', () => {
    //dialog.showMessageBox({message:"check for update"})
    console.log("check for update")
  });
  //有新版本回调
  autoUpdater.on('update-available', () => {
    //dialog.showMessageBox({message:"update-available"})
    console.log("update-available")
  });
  //已下载完成回调,这里弹窗提示用户
  autoUpdater.on('update-downloaded', (event, releaseNotes, releaseName, releaseDate, updateURL) => {
    dialog.showMessageBox({message:"新版本已准备好,是否现在重启程序?", title: "更新应用", buttons: ['重启程序', '暂不重启']}).then((value) => {
      if(value.response == 0){
          //重启并安装新版本
        autoUpdater.quitAndInstall()
      }
    })
    console.log("update-downloaded:" + updateURL)
  });
  //没有新版本回调
  autoUpdater.on('update-not-available', () => {
    //dialog.showMessageBox({message:"update-not-available"})
    console.log("update-not-available")
  });
  //更新出错回调
  autoUpdater.on('error', (error) => {
    //dialog.showMessageBox({message:"error:" + error.name + "," + error.message + "," + error.stack})
    console.log("error:" + error.name + "," + error.message + "," + error.stack)
  });
  //检查更新
  autoUpdater.checkForUpdates();
  //或者可以设置成定时循环检查,比如10分钟
  // setInterval(() => {
  //   autoUpdater.checkForUpdates();
  // }, 60000);


}


Menu.setApplicationMenu(null)


app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
app.whenReady().then(createWindow)




app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') {
    app.quit()
  }
})


app.on('activate', () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow()
  }
})

本地更新

官方还提供了一个方案,手动下载更新包到本地,然后通过本地更新,但是没有上面的简单,但是因为一起调研了一下,所以也简单记录一下。

下载这部分就不说了,参考网上的文档即可。主要说一下本地文件位置和更新。
electron如何保存一些临时文件,在哪里保存比较好?官网的给了一个很好的例子,代码如下:

var path = require('path');
var fs = require('fs');

global.tmpPath = path.join( app.getPath("temp"), "AICLASS");
if( !fs.existsSync(global.tmpPath)){
    fs.mkdirSync(global.tmpPath);
}

这样我们得到了一个临时目录tmpPath,那么这个目录在哪里呢?

它的位置在c:/用户/[用户名]/AppData/Local/Temp/AICLASS,其实就是浏览器的缓存目录,其中AICLASS是我们自己定义的目录。

我们将文件下载到这个目录中,就可以通过autoUpdater进行本地更新了,与网络更新一样,只不过feedUrl变成了本地目录而已,如下:

autoUpdater.setFeedURL({url: global.tmpPath});

总结

综上可以看到,Electron更新实际上很简单,而且更新的时候不需要关闭应用,应用仍然可以正常使用,更新完重新打开就可以直接使用新版本。

通过观察安装目录就知道了,在Electron安装目录下有以版本命名的目录,主要文件都放在这个下面,当更新的时候,会以新版本新建一个目录并下载文件到这里,这样不会修改文件就不会影响应用运行。当更新完重新打开应用的时候,应用会自动找到新版本,会运行新版本的文件,这样就更新成功了。所以Electron是可以实现静默更新的。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: Electron 是一种基于 Web 技术(JavaScript、HTML 和 CSS)的跨平台桌面应用程序开发框架,可以使用它来开发功能丰富的跨平台桌面应用程序。下面是使用 Electron 实现屏幕截图功能的简单示例: 1. 创建一个新的 Electron 项目: ``` mkdir screenshot-app cd screenshot-app npm init -y npm install --save electron ``` 2. 在项目根目录下创建一个 `main.js` 文件,用于启动 Electron 应用程序。在这个文件,我们将创建一个 Electron 窗口,包含一个按钮,点击该按钮将触发屏幕截图操作: ```javascript const { app, BrowserWindow, globalShortcut, ipcMain } = require('electron') const path = require('path') const { desktopCapturer, screen } = require('electron') let mainWindow = null function createWindow () { mainWindow = new BrowserWindow({ width: 800, height: 600, webPreferences: { nodeIntegration: true, contextIsolation: false } }) mainWindow.loadFile('index.html') mainWindow.on('closed', function () { mainWindow = null }) globalShortcut.register('CommandOrControl+Alt+D', () => { captureScreen() }) } function captureScreen() { desktopCapturer.getSources({types: ['screen']}, (error, sources) => { if (error) throw error for (let i = 0; i < sources.length; ++i) { if (sources[i].name === 'Entire screen') { navigator.mediaDevices.getUserMedia({ audio: false, video: { mandatory: { chromeMediaSource: 'desktop', chromeMediaSourceId: sources[i].id, minWidth: 1280, maxWidth: 1280, minHeight: 720, maxHeight: 720 } } }) .then((stream) => { const video = document.createElement('video') video.srcObject = stream video.onloadedmetadata = () => { const canvas = document.createElement('canvas') canvas.width = video.videoWidth canvas.height = video.videoHeight const ctx = canvas.getContext('2d') ctx.drawImage(video, 0, 0, canvas.width, canvas.height) const dataURL = canvas.toDataURL('image/png') mainWindow.webContents.send('screenshot', dataURL) video.remove() stream.getTracks()[0].stop() } }) .catch((error) => { console.error(error) }) } } }) } app.on('ready', createWindow) app.on('window-all-closed', function () { if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', function () { if (mainWindow === null) { createWindow() } }) ipcMain.handle('synchronous-message', (event, arg) => { console.log(arg) }) ``` 在 `createWindow()` 函数,我们创建了一个 Electron 窗口,并加载了 `index.html` 文件。我们还使用 `globalShortcut` 模块注册了一个全局快捷键(CommandOrControl+Alt+D),在按下这个快捷键时,将调用 `captureScreen()` 函数进行屏 ### 回答2: 使用Electron编写一个屏幕截图功能可以通过以下步骤实现: 1. 首先,你需要在Electron应用程序创建一个窗口,作为截图的界面。 2. 然后,你需要使用Electron的桌面捕获API来捕获屏幕上的图像。可以使用`desktopCapturer`模块来实现。 3. 接下来,你可以将捕获到的图像显示在截图界面上。你可以使用Electron的`BrowserWindow`模块来创建一个显示图像的窗口。 4. 然后,你可以添加一些用户交互功能,例如拖动和缩放截图框来选择要截取的区域。 5. 当用户确定选择区域后,你可以使用Electron的`ipcRenderer`模块将所选区域的图像发送到主进程。 6. 在主进程,你可以使用Electron的`nativeImage`模块将图像保存到本地文件。 7. 最后,你可以在截图界面上显示已保存的图像,或者提供一个保存成功的提示。 总结来说,使用Electron编写一个屏幕截图功能主要涉及到创建窗口、捕获屏幕图像、显示图像、用户交互、保存图像等步骤。通过合理运用Electron的模块和API,可以很方便地实现这个功能。 ### 回答3: 要使用Electron编写一个屏幕截图功能,首先需要安装Electron开发环境并创建一个Electron项目。然后,在项目创建一个主进程和一个渲染进程。 主进程负责创建一个窗口,并接收渲染进程的请求进行屏幕截图。在主进程,我们可以使用Electron的屏幕模块来获取屏幕的尺寸信息。然后,可以使用Electron的桌面捕获模块来进行屏幕截图操作。 渲染进程负责创建一个界面,提供用户进行屏幕截图的操作界面。可以使用HTML、CSS和JavaScript来创建一个按钮用于触发屏幕截图功能,并通过与主进程的通信来发送截图请求。 当用户点击截图按钮时,渲染进程会向主进程发送一个截图请求。主进程接收到请求后,会调用屏幕模块获取屏幕尺寸,并通过桌面捕获模块进行屏幕截图。截图完成后,主进程将截图保存到指定的位置,并将截图的文件路径发送给渲染进程。 渲染进程接收到截图路径后,可以通过JavaScript将截图显示在界面上,或者进一步处理截图数据。 总结来说,使用Electron编写一个屏幕截图功能的过程主要包括创建主进程和渲染进程、使用屏幕模块获取屏幕尺寸信息、使用桌面捕获模块进行屏幕截图操作、与主进程进行通信发送截图请求和接收截图路径。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BennuCTech

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值