一、项目总结
electron是多窗口应用,因此如果分散的定义窗口,难以管理和操作窗口事件。为了让代码更模块化和易维护,我们可以封装一个工具对象来处理常用功能。
我们的工具对象包含以下核心功能:
- 创建普通窗口,父子窗口
- 对窗口进行显示、隐藏、查找、放大、缩小等操作
- 通过指定的窗口id向聚焦的窗口发送消息等操作。
二、工具管理函数
electron在开发环境中,可以直接使用开发服务器的地址;而在生产环境中,则需要加载本地文件的地址,因此要进行区分。
js
import { BrowserWindow } from 'electron';
import { join } from 'path';
declare type EWindow = Electron.BrowserWindow;
// 配置public 文件目录路径,VITE_DEV_SERVER_URL表示vite默认生成的服务url
process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL
? join(process.env.DIST_ELECTRON, '../public')
: process.env.DIST;
// 配置构建时输出文件目录路径
process.env.DIST_ELECTRON = join(__dirname, '..');
//窗口配置参数
const defaultConfig = {
width: 1200,
height: 800,
show: true,
id: '',
parentId: '', //父窗口id 创建父子窗口 -- 子窗口永远显示在父窗口顶部 【父窗口可以操作】
autoHideMenuBar: true,
isMainWin: false, //是否主窗口(当为true时会替代当前主窗口)
router: '', //路由
...(process.platform === 'linux' ? { icon } : {}),
webPreferences: {
preload:join(__dirname, '../preload/index.js'),
nodeIntegration: true, //在网页中集成Node
enableWebSQL: false,
webviewTag: true,
contextIsolation: false,
},
};
//工具类函数
class WinTools {
constructor() {
//窗口管理列表
const windowsList = new Map();
},
//创建窗口
createWin = (config?: object): EWindow => {
const params = {
...defaultConfig,
...config,
};
const window = new BrowserWindow(params);
if (url) {
window.webContents.openDevTools({
mode: 'bottom',
});
window.loadURL(`${url}/${params.router}.html`);
} else {
window.loadFile(join(process.env.DIST, `/${params.router}.html`));
}
//注册管理列表
this.windowsList.set(params.id, window);
//注册父子窗口
if (params.parentId) {
params.parent = this.getWindow(params.parentId);
this.setParentWindow(params.id, params.parentId);
}
return window;
};
//通过id判断窗口是否注册
isWindow = (id: string): boolean => {
if (!this.windowsList.has(id)) {
console.log('没有这个窗口id');
}
return this.windowsList.has(id);
};
//通过id显示窗口
windowShow = (id: string): void => {
if (!isWindow(id)) return;
this.windowsList.get(id).show();
windowsList.get(id).focus();
};
//通过id隐藏窗口
const windowHide = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).hide();
};
//通过id最大化窗口
windowMax = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).maximize();
windowsList.get(id).webContents.send('isMaxWin', 'max');
};
//通过id最小化窗口
windowMin = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).minimize();
};
//通过id还原窗口
windowRestore = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).restore();
windowsList.get(id).webContents.send('isMaxWin', 'min');
};
//关闭单个窗口
windowClose = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).close();
};
//重新加载
windowReload = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).reload();
};
//关闭所有窗口
closeAllWindow = (): void => {
windowsList.forEach((item) => {
item.close();
});
};
//获取某个窗口实例
getWindow = (id: string): EWindow => {
return windowsList.get(id);
};
//窗口是否可见
windowIsVisible = (id: string): EWindow => {
return windowsList.get(id).isVisible();
};
//设置子窗口
setParentWindow = (id: string, parentId: string): void => {
if (this.isWindow(id) && this.isWindow(parentId)) {
const childWindow = this.getWindow(id);
const parentWindow = this.getWindow(parentId);
childWindow.setParentWindow(parentWindow);
}
};
//闪烁窗口
flashWindow = (id: string): void => {
if (!this.isWindow(id)) return;
windowsList.get(id).flashFrame(true);
let timer;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
windowsList.get(id).flashFrame(false);
}, 10);
}
}
export default const winTool = new WinTools()
二、窗口监听函数
Electron 提供了多种进程间通信(IPC)方式,让主进程(Main Process)和渲染进程(Renderer Process)能够相互通信。
-
ipcMain.on(消息通道名称, 回调函数)用来监听渲染进程的消息。
-
ipcMain.once(消息通道名称, 回调函数)一次性监听渲染进程的消息
-
ipcMain.handle(消息通道名称, 接受参数并返回值的回调函数)异步响应渲染进程的消息
-
当前窗口名\].webContents.send(消息通道名称, 参数)当前窗口发送消息到渲染进程
-
ipcRenderer.on(消息通道名称,回调函数) 监听主进程回复
-
ipcRenderer.invoke(消息通道名称,参数).then()异步等待主进程的回复
js
import { shell, ipcMain, app } from 'electron';
import winTool from './WinTools'
//新开web窗口
ipcMain.on('newWin', (_, config: object, data:any): void => {
const newWin = winTool.createWin(config);
//窗口发送消息
if(data){
newWin.on('ready-to-show', () => {
newWin.webContents.send('msg',data)
}
}
});
//关闭窗口
ipcMain.on('close-window', (_, id: string): void => {
if (id) {
winTool.windowClose(id);
} else {
winTool.closeAllWindow();
}
});
//隐藏窗口
ipcMain.on('hide-window', (_, id: string): void => {
winTool.windowHide(id);
});
//显示窗口
ipcMain.on('show-window', (_, id: string): void => {
winTool.windowShow(id);
});
//最大化
ipcMain.on('max-window', (_, id: string): void => {
winTool.windowMax(id);
});
//最小化
ipcMain.on('min-window', (_, id: string): void => {
winTool.windowMin(id);
});
//还原
ipcMain.on('restore-window', (_, id: string): void => {
winTool.windowRestore(id);
});
//闪烁
ipcMain.on('flash-window', (_, id: string): void => {
winTool.flashWindow(id);
});
//打开网页
ipcMain.on('open-url', (_, url) => {
shell.openExternal(url); //打开系统默认浏览器到指定的url
});
//退出应用
ipcMain.on('Exit', (_, url) => {
app.quit();
})
// 异步方法 支持同时获取多个key的数据
ipcMain.handle('get-data', async (event, name) => {
//获取数据的方法
let data = await getData(name)
return data;
})
三、主进程
在主进程中引出工具函数,并创建主窗口页面,设置开机自启动、托盘等操作。
js
import { app, BrowserWindow, Tray, Menu } from 'electron';
import winTool from './WinTool'
import './util/hook';
let mainWind: BrowserWindow | null = null;
function createWindow() {
mainWind = winTool.createWin({
isMainWin: true,
id: 'mainWindow',
router: 'index',
frame: false,
});
//窗口显示前事件
mainWind.on('ready-to-show', () => {
//线上环境清空缓存
if(!process.env.VITE_DEV_SERVER_URL){
mainWind?.webContents.session.clearStorageData({
storages:['localstorage','indexdb','cookies']
});
session.defaultSession.clearCache()
}
mainWind?.show(); // 初始化后再显示
});
// 程序崩溃的处理
mainWind?.webContents.on('render-process-gone', (res) => {
setTimeout(() => {
mainWind?.reload();
}, 1000);
});
createTray(mainWind);
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
// 此处解决mac系统关闭app后,但程序坞中还存在图标,再次点击可以重新创建进程
if (BrowserWindow.getAllWindows.length === 0) createWindow();
});
});
app.on('window-all-closed', () => {
// electron 运行在三个环境(win32 Windows系统、linux Linux系统、 darwin Mac系统)
// 此处解决的是非mac系统,程序退出进程 (Mac系统关闭app会保留在程序坞中)
if (process.platform !== 'darwin') app.quit();
});
//托盘
const createTray = (mainWind: any) => {
const icon = join(__dirname, '../../public/assets/images/main/logo-32.png');
let tray = new Tray(icon);
//设置菜单
const contextMenu = Menu.buildFromTemplate([
{
label: '退出系统',
type: 'normal',
icon: '',
click: () => {
app.quit();
},
},
{ label: '设置', icon: '', type: 'normal' },
]);
tray.setContextMenu(contextMenu);
tray.setToolTip('智能自动化平台');
tray.setTitle('智能自动化平台');
// 点击托盘图标,显示主窗口
tray.on('click', () => {
mainWind.show();
});
};
//开机自启动
function autoOpen() {
const isDevelopment = process.env.NODE_ENV === 'development';
//读取本地设置是否开机自启动
const isAutoOpen = getLocalData('setting')?.autoOpen;
//当前执行程序的地址
const ex = path.basename(process.execPath);
if (!isDevelopment) {
if (!isAutoOpen) {
app.setLoginItemSettings({
openAtLogin: false, //是否开机启动
});
logger.info('关闭开机自启动:' + isAutoOpen);
} else {
app.setLoginItemSettings({
openAtLogin: true,
openAsHidden: false,
path: process.execPath,
args: ['--processStart', `"${ex}"`, '--process-start-args'],
});
logger.info('开启开机自启动:' + isAutoOpen);
}
}
}
autoOpen()
四、preload文件
在 Electron 中,HTML 页面(渲染进程)默认不能直接使用 ipcRenderer
的主要原因是为了 安全性。
上下文隔离(Context Isolation)
- Electron 默认启用
contextIsolation: true
。渲染进程的 JavaScript 运行环境(如window
)和 Node.js 环境(如require
)是隔离的。 - 直接访问
require('electron').ipcRenderer
会报错 - 目的 :防止恶意代码通过
require
直接访问 Node.js API,避免安全漏洞(如 RCE 攻击)。
启用 Node.js 集成时仍有限制
- 即使你手动关闭
contextIsolation
并启用nodeIntegration: true
- 虽然此时可以直接用
require('electron').ipcRenderer
,但 极度不安全,因为网页中的第三方脚本(如广告、CDN 库)也能调用 Node.js API,可能导致系统被入侵。
因此需要通过 preload
脚本 暴露有限的 IPC 功能给渲染进程
js
import { ipcRenderer , contextBridge } from 'electron';
//方法一:
(window as any).ipcRenderer = ipcRenderer;
//方法二: 通过 contextBridge 安全地暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => ipcRenderer.send(channel, data),
on: (channel, callback) => ipcRenderer.on(channel, callback),
invoke: (channel, data) => ipcRenderer.invoke(channel, data),
});
五、渲染进程中使用
在渲染页面中通过window挂载的方法去调用窗口的通讯方法。
js
//发送数据给窗口
window.ipcRenderer.send('Exit');
//接口窗口数据
window.ipcRenderer.on('msg', (_, val: string) => {
msg.value = val;
});
//异步等待窗口回应
let msg = await window.ipcRenderer.invoke('get-data', 'setting')
// 方法二的使用: 发送消息到主进程
window.electronAPI.send('message', 'Hello from renderer!');