从0到1实践electron开发多窗口应用到打包升级
/ 8 min read
Table of Contents
我正在参加「掘金·启航计划」
前言
最近使用 Eelctron 开发了一个桌面端软件,并对之前所学习的大文件切片上传,以及 nest 框架做一个实践
开源地址https://github.com/whwanyt/fen_im_pc
阅读须知:
- 代码使用 TypeScript,vue-setup
- 脚手架 electron-vite
- 开发环境:window,node:v16 .15.1,pnpm:v7 .9.0
本文知识点
- 使用 electron ipc 进行渲染进程和主进程的通行
- 主进程使用单例模式和 Map 对多窗口进行管理
- 使用 electron-updater 进行软件更新
项目效果图
项目搭建
- 拉取模板项目https://github.com/alex8088/electron-vite-boilerplate
- 执行项目初始化并运行
npm installnpm run dev效果如下
多窗口管理
在 main 目录下新建 windows.ts 文件,并实现窗口创建及管理的单例类
import { shell, BrowserWindow, ipcMain } from "electron";import { is } from "@electron-toolkit/utils";import * as path from "path";
export interface CreateWindowOptions { module: string; //窗口模块名称 center?: boolean; //打开新页面时是否显示在屏幕中心 url?: string; //窗口链接 width?: number; height?: number; maximizable?: boolean; //是否可以最大化}
export type winModule = { id: number; url: string;};
export class WindowsMain { //key为winid,value为创建窗口返回的对象 BrowserWindowsMap = new Map<number, BrowserWindow>(); //key为窗口模块名称,方便通过模块名称查询 winModulesMap = new Map<string, winModule>(); constructor() {}
static instance: WindowsMain;
static getInstance() { if (!this.instance) { this.instance = new WindowsMain(); } return this.instance; }}实现创建窗口方法
newWindow(options: CreateWindowOptions): BrowserWindow { //通过创建窗口模块名称判断是否已经存在,存在就获取焦点,并将数据通过ipc通知到该窗口 if (this.winModulesMap.has(options.module)) { const id = this.winModulesMap.get(options.module)!.id const win = this.BrowserWindowsMap.get(id) win!.focus() const params = getRequest(options.url || '') win!.webContents.send('uploadData', params) return win! } options.url = options.url || '' options.width = options.width || 990 options.height = options.height || 570 options.maximizable = options.maximizable != undefined ? options.maximizable : true const currentWindow = BrowserWindow.getFocusedWindow() let coord: { x: number | undefined; y: number | undefined } = { x: undefined, y: undefined } //如果已经有打开的窗口,并且新窗口不是居于屏幕中央,则相对于上一个窗口进行偏移 if (currentWindow && !options.center) { const [currentWindowX, currentWindowY] = currentWindow.getPosition() coord.x = currentWindowX + 30 coord.y = currentWindowY + 30 } const mainWindow = new BrowserWindow({ width: options.width, height: options.height, show: false, frame: false, ...coord, center: options.center, maximizable: options.maximizable, autoHideMenuBar: true, ...(process.platform === 'linux' ? { icon: path.join(__dirname, '../../build/icon.png') } : {}), webPreferences: { preload: path.join(__dirname, '../preload/index.js') } })
mainWindow.on('close', () => { this.detWin(mainWindow.id) })
mainWindow.on('ready-to-show', () => { console.log('ready-to-show') //在窗口刷新时将窗口信息发送到渲染进程,方便指定窗口交互 mainWindow.webContents.send('setWinInfo', { winViewId: mainWindow.id, winViewModule: options.module }) mainWindow.show() })
mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) //开发模式下拼接打开路由 if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'] + options.url) } else { //打包后读取文件,并使用哈希打开指定路由 mainWindow.loadFile(path.join(__dirname, '../renderer/index.html'), { hash: options.url }) } //将窗口信息存储到map this.BrowserWindowsMap.set(mainWindow.id, mainWindow) this.winModulesMap.set(options.module, { id: mainWindow.id, url: options.url || '' }) return mainWindow }实现获取窗口对象的方法
getWin(winId: number) { return this.BrowserWindowsMap.get(winId)}实现删除窗口方法
detWin(winId: number) { const win = this.BrowserWindowsMap.get(winId) try { if (this.BrowserWindowsMap.size > 1) { let key = '' this.winModulesMap.forEach((item, k) => { if (item.id === winId) { key = k } }) if (key !== '') { this.winModulesMap.delete(key) } this.BrowserWindowsMap.delete(winId) } win?.close() } catch (error) {} }修改 index.ts 文件中的 createWindow 函数如下,即可打开默认主窗口
function createWindow(): void { // Create the browser window. const windowMain = WindowsMain.getInstance() const win = windowMain.newWindow({ module: 'app' })}IpcMain 交互
在 preload 目录下新建 ipc.ts 文件
实现窗口最小化和关闭
备注:window .winViewId 来源于创建窗口时主进程向渲染进程的 ipc
//渲染进程window.api.WindowAppQuit({ winViewId: window.winViewId });//主进程function WindowAppMinimize() { ipcMain.on("appMinimize", (_event, data: PreloadOptions) => { const win = WindowsMain.getInstance().getWin(data.winViewId); win && win.minimize(); });}function WindowAppQuit() { ipcMain.on("appQuit", (_event, data: PreloadOptions) => { WindowsMain.getInstance().detWin(data.winViewId); });}实现窗口尺寸变更
function changWindowSize() { ipcMain.on("changWindowSize", (_event, data: PreloadSizeOptions) => { const win = WindowsMain.getInstance().getWin(data.winViewId); win && win.setSize(data.width, data.height); });}实现打开新窗口
//主进程function openWin() { ipcMain.on("openWin", (_event, data: PreloadUrlOptions) => { WindowsMain.getInstance().newWindow(data); });}//渲染进程window.api.openWin({ module: "friend", url: "#/friend", width: 500, height: 420, maximizable: false, center: true,});项目打包
cannot unpack electron zip file, will be re-downloaded error=zip: not a vali
将 electron-v17.4.11-win32-x64.zip 下载,放到 C:xxx \AppData\Local\electron\Cache\目录下,
打包时缺少 nsis 等都可以先下载,然后通过上述方法解决
软件升级
创建 update.ts,并实现autoUpdater的方法
import { app, BrowserWindow, ipcMain } from 'electron'import { autoUpdater } from 'electron-updater'
const message = { error: '检查更新出错', checking: '正在检查更新…', updateAva: '正在更新', updateNotAva: '已经是最新版本', downloadProgress: '正在下载...'}
export const handleUpdate = (win: BrowserWindow) => { autoUpdater.autoDownload = false autoUpdater.setFeedURL('http://192.168.0.105:8080/') // 通过main进程发送事件给renderer进程,提示更新信息 const sendUpdateMessage = (data) => { win.webContents.send('update-message', data) } autoUpdater.on('error', function (_e) { // 异常处理 sendUpdateMessage({ cmd: 'error', message: message.error }) }) autoUpdater.on('checking-for-update', function () { // 校验 sendUpdateMessage({ cmd: 'checking-for-update', message: message.checking }) }) autoUpdater.on('update-available', function (info) { //可用更新 sendUpdateMessage({ cmd: 'update-available', message: message.updateAva, info }) }) autoUpdater.on('update-not-available', function (info) { // 更新失败 sendUpdateMessage({ cmd: 'update-not-available', message: message.updateNotAva, info: info }) }) autoUpdater.on('download-progress', function (progressObj) { // 更新下载进度事件 sendUpdateMessage({ cmd: 'downloadProgress', message: message.downloadProgress, progressObj }) }) autoUpdater.on( 'update-downloaded', function (_event, _releaseNotes, _releaseName, _releaseDate, _updateUrl, _quitAndUpdate) { ipcMain.on('isUpdateNow', (_e, _arg) => { // 开始更新 autoUpdater.quitAndInstall() app.quit() // callback() }) sendUpdateMessage({ cmd: 'isUpdateNow', message: null }) } )
ipcMain.on('checkForUpdate', () => { // 执行自动更新检查 autoUpdater.checkForUpdates() })
ipcMain.on('downloadUpdate', () => { // 执行下载 autoUpdater.downloadUpdate() })}在主进程 main.ts 中调用handleUpdate函数
function createWindow(): void { // Create the browser window. const windowMain = WindowsMain.getInstance(); const win = windowMain.newWindow({ module: "app" }); ipc(); //调用 handleUpdate(win);}在渲染进程主界面实现升级组件,并触发主进程autoUpdater检查是否需要升级
const onUpdate = () => { //判断是否主窗口 if (window.winViewModule === "app") { //触发升级检测 window.electron.ipcRenderer.send("checkForUpdate"); //监听主进程发过来的更新消息 window.electron.ipcRenderer.on("update-message", (_event, val) => { console.log(val); switch (val.cmd) { case "update-available": showUpdateModal.value = true; info.value.version = val.info.version; info.value.description = val.info.description || ""; break; case "downloadProgress": console.log("下载进度", val.progressObj); case "isUpdateNow": isCompletes.value = true; break; default: break; } }); }};//触发下载const onDownloadUpdate = () => { window.electron.ipcRenderer.send("downloadUpdate");};//重启安装方法const onResetUpdate = () => { window.electron.ipcRenderer.send("isUpdateNow");};参考文档
- [Electron-updater 升级参考] https://www.jianshu.com/p/a6ed9daa150e
- [Electron 文档]https://www.electronjs.org/zh/docs/latest