Appearance
多进程模型
多进程模型
Electron 将使用两种类型的进程:主进程 和 渲染器进程。
不同进程承载着不同的任务,本章讨论的进程通信(IPC)就是解决不同进程间任务传递的方式。
- 比如渲染进程通过主进程调用原生Node.js API,比如文件操作
- 主进程通过原生菜单改变渲染进程页面内容
- IPC通信使用 ipcMain 和 ipcRenderer 两个模块传递消息
主进程
每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有使用 Node.js API 的能力。
主进程的主要目的是使用 BrowserWindow 模块创建和管理应用程序窗口。
渲染器进程
每个 Electron 应用都会为使用 BrowserWindow
打开的窗口生成一个单独的渲染器进程。
默认情况下渲染进程与主进程使用 preload.js预加载做为通信桥梁。
预加载脚本
预加载(preload)脚本包含了那些执行于渲染进程中,且先于网页内容开始加载的代码 。这些脚本虽运行于渲染器的环境中,却因能访问有限的 Node.js、Electron高级权限。
因为Electron项目与其他桌面应用是有区别的,他具有浏览器的特性,所以开放主进程的node.js给渲染进程,是有安全隐患的。默认electron是不会开放高级权限给渲染进程,而是要求开发者自行决定渲染进程可以使用哪些主进程任务,这块功能就要在预加载脚本中完成。
预加载脚本像一个桥接器,用于渲染脚本renderer.js与main.js脚本的连接。
基础知识
预加载脚本运行在具有 HTML DOM APIs 和 Node.js、Electron 的有限功能访问权限的环境中。
Preload.js 是渲染进程与主进程通信的桥梁。
使用场景
- 使用有限的node.js、Electron高级api
- 主进程与渲染进程进行IPC通信,比如渲染进程让主进程帮助在本地保存文件
使用示例
下面使用预加载脚本preload.js,通过node 的process查看软件版本的信息
main.js 主进程脚本
const { BrowserWindow, app } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
webPreferences: {
//预加载脚本
preload: path.resolve(__dirname, 'preload.js'),
},
})
win.loadFile(path.resolve(__dirname, 'index.html'))
win.webContents.openDevTools()
}
app.whenReady().then(() => {
createWindow()
})
preload.js 预加载脚本
document.addEventListener('DOMContentLoaded', () => {
for (const soft of ['chrome', 'electron', 'node']) {
console.log(soft)
document.querySelector(`#${soft}`).innerHTML = `${soft}:` + process.versions[soft]
}
})
index.html 模板文件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<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'" />
<title>banmashou</title>
</head>
<body>
<div id="chrome"></div>
<div id="node"></div>
<div id="electron"></div>
<script src="renderer.js"></script>
</body>
</html>
最终会打印出node、electron、chrome版本信息

进程通信
下面介绍主进程与渲染进程是如何进行通信的。
渲染进程到主进程
下面介绍渲染进程向主进程通信,这是单向通信行为。本例实现的功能是渲染进程向主进程发送请求,更改窗口标题。
main.js
const { BrowserWindow, app, ipcMain } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
alwaysOnTop: true,
x: 1500,
y: 100,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
win.webContents.openDevTools()
win.loadFile(path.resolve(__dirname, 'index.html'))
}
app.whenReady().then(() => {
createWindow()
//主进程事件监听
ipcMain.on('setTitle', (event, title) => {
//获取用于控制网页的webContents对象
const webContents = event.sender
//获取窗口
const win = BrowserWindow.fromWebContents(webContents)
//设置窗口标题
win.setTitle(title)
})
})
preload.js
const { contextBridge, ipcRenderer } = require('electron')
//为渲染进程暴露API
contextBridge.exposeInMainWorld('api', {
//该API用于向主进程事件
setTitle: (title) => ipcRenderer.send('setTitle', title),
})
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>banmashou</title>
</head>
<body>
<input type="text" name="title" />
<button>更改标题</button>
<script src="renderer.js"></script>
</body>
</html>
renderer.js
window.addEventListener('DOMContentLoaded', () => {
document.querySelector('button').addEventListener('click', () => {
const value = document.querySelector('[name=title]').value
//使用preload.js暴露出的API,触发主进程事件
window.api.setTitle(value)
})
})
主进程到渲染进程
下面介绍主进程主动向渲染进程通信,这也是单向通信IPC。
将消息从主进程发送到渲染器进程时,需要指定是哪一个渲染器接收消息。 消息需要通过 WebContents
实例的send方法发送到渲染器进程。
main.js
const { BrowserWindow, app, ipcMain, Menu } = require('electron')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
alwaysOnTop: true,
x: 1500,
y: 100,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
})
//定义菜单
const menu = Menu.buildFromTemplate([
{
label: '菜单',
submenu: [
{
//主进程向渲染进程发送消息
click: () => win.webContents.send('increment', 1),
label: '增加',
},
],
},
])
Menu.setApplicationMenu(menu)
//打开开发者工具
win.webContents.openDevTools()
win.loadFile(path.resolve(__dirname, 'index.html'))
}
app.whenReady().then(() => {
createWindow()
})
//接收渲染进程的结果
ipcMain.on('finish', (event, value) => {
console.log('最后结果是:' + value)
})
perload.js
const { contextBridge, ipcRenderer } = require('electron')
//为渲染进程暴露API
contextBridge.exposeInMainWorld('api', {
//为渲染进程设置接口,用于接收主进程的消息
incrementNumber: (callback) => ipcRenderer.on('increment', callback),
})
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'" />
<title>banmashou</title>
</head>
<body>
<h1></h1>
<script src="renderer.js"></script>
</body>
</html>
renderer.js
//向预加载脚本传递回调方法,用于处理主进程的消息
window.api.incrementNumber((event, value) => {
const h1 = document.querySelector('h1')
h1.innerHTML = Number(h1.innerText) + value
//向主进程发送消息
event.sender.send('finish', h1.innerHTML)
})
双向通信
使用 ipc 的invoke 进行渲染进程与主进程的通信,主进程会返回 promise。
index.html
...
<button id="btn">IPC</button>
<script src="renderer.js"></script>
...
main.js
const { BrowserWindow, app } = require('electron')
const { ipcMain } = require('electron/main')
const path = require('path')
const createWindow = () => {
const win = new BrowserWindow({
width: 300,
height: 300,
alwaysOnTop: true,
webPreferences: {
preload: path.resolve(__dirname, 'preload.js'),
},
})
win.webContents.openDevTools()
win.loadFile(path.resolve(__dirname, 'index.html'))
return win
}
app.whenReady().then(() => {
const win = createWindow()
ipcMain.handle('mainShow', (event) => {
return 'is main handle'
})
})
preload.js
const { ipcRenderer } = require('electron')
const { contextBridge } = require('electron/renderer')
contextBridge.exposeInMainWorld('api', {
show: () => {
return ipcRenderer.invoke('mainShow')
},
})
renderer.js
const bt = document.querySelector('#btn')
bt.addEventListener('click', async () => {
const res = await api.show()
console.log(res)
})
其他进程的通信的使用是类似的,可以参考 electron 文档学习