# 核心概念
# 主进程与渲染进程
主进程与渲染进程
- 主进程:启动项目时运行的 main.js 脚本就是我们说的主进程。在主进程运行的脚本可以以创建 web 页面的形式展示 GUI,主进程只有一个
- 渲染进程: 每个 Electron 的页面都在运行着自己的进程,这样的进程称之为渲染进程(基于Chromium的多进程结构)。
Electron的架构
说明:
- 主进程使用BrowserWindow创建实例;
- 主进程销毁后,对应的渲染进程会被终止;
- 主进程与渲染进程通过IPC方式(事件驱动)进行通信;
# 如何调试Electon
Electron中集成了Chromium内核,可以通过菜单打开devTools窗口与chrome中一样。
还可以在开发的时候,通过设置主窗口自动打开devTools:
win.webContents.openDevTools()
还可以配合着nodemon
对Electron的代码进行调试(自动重启)。
还可以使用VSCode的调试功能进行单步调试,新增文件.vscode/launch.json
配置如下:
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Electron Main",
"program": "${workspaceFolder}/index.js",
"request": "launch",
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
"skipFiles": [
"<node_internals>/**"
],
"type": "pwa-node"
}
]
}
# 常见API
# 主进程生命周期
官方文档:https://www.electronjs.org/zh/docs/latest/api/app (opens new window)
const { app } = require('electron')
app.on('window-all-closed', () => {
app.quit()
})
常见的事件:
- 'will-finish-launching':当应用程序完成基础的启动的时候被触发。;
- 'ready':当 Electron 完成初始化时,发出一次;
- 'window-all-closed':当所有的窗口都被关闭时触发;
- 'before-quit':在程序关闭窗口前发信号;
- 'will-quit':当所有窗口被关闭后触发,同时应用程序将退出;
- 'quit':在应用程序退出时发出。
# 渲染进程使用Node
后续会介绍安全原则 ,一般不推荐直接在渲染进程中使用node,因为这样不安全。
一般在渲染进程中,通过映射的方式,把主进程中的node相关的接口进行映射使用,不是直接调用node。
示例代码:
const createWindow = () => {
const win = new BrowerWindow({
// ...
webPreferences: {
nodeIntergration: true,
// 最新版本的electron v15需要添加
contextIsolation: false,
}
})
}
上面的改造方式,适合老的项目改造成Electron桌面端的应用的。
# 通信
# 主进程与渲染进程
修改index.js
项目的入口文件:
// 在主进程中.
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.reply('asynchronous-reply', 'pong')
})
ipcMain.on('synchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.returnValue = 'pong'
})
修改index.html
中的scripts
部分
//在渲染器进程 (网页) 中。
// 注意: Electron 的 API 在预加载后即可访问,直到上下文隔离被禁用
// 详见: https://www.electronjs.org/docs/tutorial/process-model#preload-scripts
const { ipcRenderer } = require('electron')
console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
ipcRenderer.on('asynchronous-reply', (event, arg) => {
console.log(arg) // prints "pong"
})
ipcRenderer.send('asynchronous-message', 'ping')
说明:
使用ipc通信的一些应用场景:
- 使用主进程进行请求可以解决跨域问题;
- 背景音频&视频;
- 获取数据任务(定时任务);
- canvas渲染;
- 后台任务等;
在Electron v14之后,已经移除了
remote
模块,没有了remote
通信方式了。
# 渲染进程之间
这个部分可能不太好理解。
主进程index.js
文件
const { app, BrowserWindow, ipcMain, MessageChannelMain } = require('electron');
// 主进程
const createWindow = async () => {
// 创建浏览器窗口
const worker = new BrowserWindow({
show: false,
webPreferences: {
nodeIntegration: true,
// 最新版本的Electron v15
contextIsolation: false,
},
});
// 加载index.html文件
await worker.loadFile('child.html');
// 创建浏览器窗口
const win = new BrowserWindow({
width: 1200,
height: 600,
webPreferences: {
nodeIntegration: true,
// 最新版本的Electron v15
contextIsolation: false,
},
});
// 加载index.html文件
win.loadFile('index.html');
win.webContents.openDevTools();
// 主进程与渲染进程通信
ipcMain.on('my-channel', (event, args) => {
// console.log('🚀 ~ file: index.js ~ line 55 ~ ipcMain.on ~ args', args);
// console.log('🚀 ~ file: index.js ~ line 55 ~ ipcMain.on ~ event', event);
// event.reply('child-channel', { event: 'msg', data: 'hello child!' });
// For security reasons, let's make sure only the frames we expect can
// access the worker.
if (event.senderFrame === win.webContents.mainFrame) {
// Create a new channel ...
const { port1, port2 } = new MessageChannelMain();
// ... send one end to the worker ...
worker.webContents.postMessage('new-work', null, [port1]);
// ... and the other end to the main window.
event.senderFrame.postMessage('child-channel', null, [port2]);
// Now the main window and the worker can communicate with each other
// without going through the main process!
}
});
};
app.on('window-all-closed', () => {
console.log('window-all-closed');
// 对于MacOS系统 -> 关闭窗口时,不会直接退出应用
if (process.platform !== 'darwin') app.quit();
});
app.on('will-finish-launching', () => {
console.log('will-finish-launching');
});
app.on('ready', () => {
console.log('ready');
});
app.on('will-quit', () => {
console.log('will-quit');
});
app.on('before-quit', () => {
console.log('before-quit');
});
app.on('quit', () => {
console.log('quit');
});
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
渲染进程1index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Electorn Demo</title>
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.min.js"></script>
</head>
<body>
Hello Electron
<div id="app">
hello1
<p>Electron Version:{{electronVersion}}</p>
<p>Node Version: {{nodeVersion}}</p>
<p>Chromium version: {{chromeVersion}}</p>
<p>count: {{count}}</p>
<p>result: {{result}}</p>
<button @click="sendMsg">发送消息给main</button>
</div>
<script>
// const path = require('path');
// console.log(path.resolve(__dirname, 'test.js'));
// console.log(process.versions);
const { ipcRenderer } = require('electron');
ipcRenderer.on('child-channel', (event, arg) => {
const [port] = event.ports;
port.onmessage = (event) => {
// 取worker返回的处理过后的数据
app.result = event.data;
};
port.postMessage(app.count);
});
const app = new Vue({
el: '#app',
data: {
electronVersion: process.versions.electron,
nodeVersion: process.versions.node,
chromeVersion: process.versions.chrome,
count: 10,
result: 0,
},
methods: {
sendMsg: function () {
// 准备的测试数据 -> 发送给worker渲染进程 -> 接收处理完成的数据
this.count += 10;
ipcRenderer.send('my-channel');
},
},
});
</script>
</body>
</html>
渲染进程2index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
// ipcRenderer -> 建立与主ipc的通信
// onmessage -> 接收通道的消息
// postMessage -> 发送处理完成的数据消息 -> result
const { ipcRenderer } = require('electron');
function doWork(input) {
return input * 2;
}
ipcRenderer.on('new-work', (event, args) => {
const [port] = event.ports;
port.onmessage = (event) => {
// 从index渲染进行(通道1)传递过来的数据
const result = doWork(event.data);
// 响应通道数据 -> 通道2 发送数据
port.postMessage(result);
};
});
</script>
</body>
</html>
# 安全原则
这一块,参考官方文档:安全性,原生能力和你的责任 (opens new window)
为加强程序安全性,你至少应当遵循下列规则:
- 只加载安全的内容 (opens new window)
- 禁止在所有渲染器中使用Node.js集成显示远程内容 (opens new window)
- 在所有显示远程内容的渲染器中启用上下文隔离。 (opens new window)
- Enable sandboxing (opens new window)
- 在所有加载远程内容的会话中使用
ses.setPermissionRequestHandler()
. (opens new window) - 不要禁用
webSecurity
(opens new window) - 定义一个
Content-Security-Policy
(opens new window)并设置限制规则(如:script-src 'self'
) - 不要设置
allowRunningInsecureContent
为 true. (opens new window) - 不要开启实验性功能 (opens new window)
- 不要使用
enableBlinkFeatures
(opens new window) webview
:不要使用allowpopups
(opens new window)<webview>
:验证选项与参数 (opens new window)- 禁用或限制网页跳转 (opens new window)
- 禁用或限制新窗口创建 (opens new window)
- 不要对不可信的内容使用
openExternal
(opens new window) - 使用当前版本的 Electron (opens new window)