skip to content
Logo 裁晨

从0到1实践electron开发多窗口应用到打包升级

/ 8 min read

Table of Contents

我正在参加「掘金·启航计划」

前言

最近使用 Eelctron 开发了一个桌面端软件,并对之前所学习的大文件切片上传,以及 nest 框架做一个实践

开源地址https://github.com/whwanyt/fen_im_pc

阅读须知:

  1. 代码使用 TypeScript,vue-setup
  2. 脚手架 electron-vite
  3. 开发环境:window,node:v16 .15.1,pnpm:v7 .9.0

本文知识点

  • 使用 electron ipc 进行渲染进程和主进程的通行
  • 主进程使用单例模式和 Map 对多窗口进行管理
  • 使用 electron-updater 进行软件更新

项目效果图

0342bdefd6e94734a8f3cd1af874a0a.png

项目搭建

  1. 拉取模板项目https://github.com/alex8088/electron-vite-boilerplate
  2. 执行项目初始化并运行
npm install
npm run dev

效果如下

1665305936795.png

多窗口管理

在 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");
};

参考文档