前言
最近使用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 install npm 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。