Electron入门
一、Electron 脚手架
1、添加 package.json
Electron + JS + 原生
nodemon 可以自动兼容文件变化,重启Electron客户端
{"name": "electron-app","version": "1.0.0","description": "","main": "main.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1","dev": "nodemon --exec electron ."},"keywords": [],"author": "","license": "ISC","dependencies": {"electron": "^28.0.0"},"devDependencies": {"nodemon": "^3.0.2"}}
nodemon.json
{"ignore": ["node_modules","dist"],"colours": true,"verbose": true,"watch": ["*.*"],"ext": "html,js"}
Electron + Vite + 任意框架
concurrently 执行同时执行多命令
{"name": "electron-vue","private": true,"version": "0.0.0","type": "module","main": "main.cjs","scripts": {"dev": "concurrently \"vite\" \"electron .\"","build": "vue-tsc && vite build","preview": "vite preview"},"dependencies": {"concurrently": "^8.2.2","electron": "^28.0.0","vue": "^3.3.8"},"devDependencies": {"@vitejs/plugin-vue": "^4.5.0","typescript": "^5.2.2","vite": "^5.0.0","vue-tsc": "^1.8.22"}}
2、添加index.html
配置跨域
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" /><meta http-equiv="X-Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
3、添加main.js
const createWindow = () => {const mainWindow = new BrowserWindow({width: 1000,height: 800,x: 20,y: 20,alwaysOnTop: false,frame: true,transparent: false,});mainWindow.loadURL(path.resolve(__dirname, 'index.html'));mainWindow.webContents.openDevTools();return mainWindow;}app.whenReady().then(() => {createWindow();})
另外推荐一个 React + TypeScript + Vite + Electron 的脚手架
https://github.com/maxstue/vite-reactts-electron-starter
二、Electron 进程通信
1、Electron 进程
Electron 主进程与渲染进程同时支持Node环境,但一般的,为了渲染进程安全,建议通过进程通信的方式在渲染进程中使用Node.
Electron 进程分为 主进程 main.js,渲染进程 renderer.js,预加载进程 preload.js
预加载进行用于预加载事件通信。
注册preload.js 预加载进程
const mainWindow = new BrowserWindow({webPreferences: {preload: path.resolve(__dirname, 'preload.js'),nodeIntegration: true,}})
2、渲染进程向主进程发送消息
- 预渲染进程注册 IPC通道
contextBridge.exposeInMainWorld('api', {IPC_SET_TITLE: (preload) => ipcRenderer.send('CHANNEL_SET_TITLE', preload),})
- 渲染进程向 IPC 通道发送消息
function createFormElement () {const input = document.createElement('input');input.name = 'title'const button = document.createElement('button');button.innerText = 'Submit'button.type = 'submit';const form = document.createElement('form');form.onsubmit = (event) => {event.preventDefault();const inputTitle = event.target.querySelector('input[name="title"]').valuewindow.api.IPC_SET_TITLE(inputTitle)}form.appendChild(input);form.appendChild(button);document.body.appendChild(form);}
- 在主进程监听 IPC 通道消息
ipcMain.on('CHANNEL_SET_TITLE', (event, preload) => {//获取用于控制网页的webContents对象const webContents = event.sender//获取窗口const win = BrowserWindow.fromWebContents(webContents)//设置窗口标题win.setTitle(preload)})
3、主进程向渲染进程发送消息
一个Electron菜单向页面发送消息的例子
- 预渲染进程注册 IPC通道
IPC_INCREMENT: (callback) => ipcRenderer.on('CHANNEL_INCREMENT', callback)
这里注册的是一个监听通道事件,它返回了一个回调函数,每当有通道消息,都会触发callback回调函数。
callback 它的类型大概是这样的
(event, preload) => any
- 菜单向IPC 通道发送消息
注册菜单:
// const mainWindow = new BrowserWindow(...)createMenu(mainWindow);
menu.js
const { Menu } = require('electron')function createMenu(win) {const menu = Menu.buildFromTemplate([{label: '菜单',submenu: [{//主进程向渲染进程发送消息click: () => win.webContents.send('CHANNEL_INCREMENT', 1),label: '增加',},],},]);Menu.setApplicationMenu(menu);}module.exports = { createMenu }
- 重写 IPC_INCREMENT 函数,使其能够被 IPC通道 调用。
renderer.js
window.api.IPC_INCREMENT((event, value) => {const h1 = document.querySelector('h1');h1.innerHTML = Number(h1.innerText) + value;event.sender.send('CHANNEL_FINISH', h1.innerHTML); // 这个可以直接用// window.api.IPC_FINISH(h1.innerHTML); 这个需要在preload里先注册})
- main.js
ipcMain.on('CHANNEL_FINISH', (event, preload) => {console.log(preload) // IDE 打印 1 2 3 4})
4、Invoke 双向通信
- preload.js 注册通道
IPC_MAIN_SHOW: async (preload) =>{console.log('IPC_MAIN_SHOW', preload);return ipcRenderer.invoke('CHANNEL_MAIN_SHOW', preload) // Promise}
- main.js 向通道 返回数据
app.whenReady().then(() => {createWindow();ipcMain.handle('CHANNEL_MAIN_SHOW', () => {return 'is main handle'})})
- renderer 拿到通道 返回的数据,并向通道传入新的请求参数
function createInvokeButton () {const btn = document.createElement('button');btn.innerText = 'Invoke';btn.onclick = async () => {const res = await api.IPC_MAIN_SHOW('renderer');document.body.insertAdjacentText('afterend', res);}document.body.appendChild(btn)}
三、进程隔离
参考文档:https://doc.houdunren.com/%E7%B3%BB%E7%BB%9F%E8%AF%BE%E7%A8%8B/electron/4%20%E9%9A%94%E7%A6%BB%E8%BF%9B%E7%A8%8B.html#%E4%B8%8A%E4%B8%8B%E6%96%87%E9%9A%94%E7%A6%BB
进程隔离有三个选项
contextIsolation 上下文隔离
nodeIntegration 集成Node
sandbox 沙盒环境
const mainWindow = new BrowserWindow({webPreferences: {preload: path.resolve(__dirname, 'preload.js'),contextIsolation: false,nodeIntegration: true,sandbox: false,},})
隔离的配置有几种情况:
默认 contextIsolation 为 true,nodeIntegration为 false,sandbox为true(安全,啥API用不了)
- 此时main.js 为完全node环境,renderer 和 preload 为浏览器环境
nodeIntegration 为 true(推荐)
- nodeIntegration 为 true,sandbox 自动设置 true,可以在preload中使用各种Node 高级模块,比如fs 等
nodeIntegration 为 true,sandbox为 false(不安全,啥API用不了)
- 只能在preload 中用一些很低级的模块
contextIsolation为 false(不安全)
- 不支持 contextBridge.exposeInMainWorld 等 API,在开启Node集成,关闭沙盒后,可以直接在render.js 中 使用完整的 NodeJS API,不安全
四、窗口定义
窗口实例常用的方法
方法 | 说明 |
---|---|
win.loadFile() | 加载文件 |
win.loadURL() | 加载链接 |
win.webContents.openDevTools() | 打开开发者工具 |
win.setContentBounds() | 控制窗口尺寸与位置 |
win.center() | 将窗口移动到屏幕中心 |
常用的窗口属性
属性 | 说明 |
---|---|
title | 标题,也可以修改html模板的title标签,模板的title标签优先级高 |
icon | window系统窗口图标 |
frame | 是否显示边框 |
transparent | 窗口是否透明 |
x | x坐标 |
y | y坐标 |
width | 宽度 |
height | 高度 |
movable | 是否可以移动窗口 |
minHeight | 最小高度,不能缩放小于此高度 |
minWidth | 最大高度,不能缩放小于此高度 |
resizable | 是否允许缩放窗口 |
alwaysOnTop | 窗口是否置顶 |
autoHideMenuBar | 是否自动隐藏窗口菜单栏。 一旦设置,菜单栏将只在用户单击 Alt 键时显示 |
fullscreen | 是否全屏幕 |
五、菜单管理
在electron中可以方便的对应用菜单进行定义。
清除菜单
下面先来学习不显示默认菜单,在主进程main.js中定义以下代码。
const { BrowserWindow, app, Menu } = require('electron')Menu.setApplicationMenu(null)
我们需要用到 Menu (opens new window)模块、MenuItem (opens new window)菜单项与 accelerator
(opens new window)快捷键知识。
const { Menu, BrowserWindow } = require('electron')const isMac = process.platform === 'darwin'function createMenu(window) {const menu = Menu.buildFromTemplate([{label: '菜单',submenu: [{label: '打开新窗口',click: () => new BrowserWindow({ width: 800, height: 600 }).loadURL('https://baidu.com'),accelerator: 'CommandOrControl+n',},{ //主进程向渲染进程发送消息label: '增加',click: () => window.webContents.send('CHANNEL_INCREMENT', 1),},],},{type: 'separator',},isMac? { label: '关闭', role: 'close' }: { role: 'quit' },]);Menu.setApplicationMenu(menu);}module.exports = {createMenu,}
右键菜单
electron 可以定义快捷右键菜单,需要预加载脚本与主进程结合使用
main.js 主进程定义ipc事件,当preload.js 触发事件时显示右键菜单
ipcMain.on('show-context-menu', (event) => {const popupMenuTemplate = [{ label: '退出', click: () => app.quit() },]const menu = Menu.buildFromTemplate(popupMenuTemplate,)menu.popup(BrowserWindow.fromWebContents(event.sender),)})
preload.js 预加载脚本定义,用于触发右键事件,然后通过IPC调用主进程显示右键菜单
window.addEventListener('contextmenu', (e) => {e.preventDefault()ipcRenderer.send('show-context-menu')})