一、创建项目
mkdir test-electron
cd test-electron
npm init -y
npm i --save-dev electron
切记:一定要安装Node环境,版本最好在V8.0.0以上
二、进程/线程基本概念
三、进程间通信IPC(Inter-Process Communication)
3.1, 基本概念
进程间通信是指在不同进程之间传播或交换信息,进程地址空间相互独立,每个进程各自有不同的用户地址空间,进程之间不能相互访问。必须通过内核才能进行数据交换:
3.2,为什么进程间要通信
- 数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几M字节之间
- 共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。
3.3, 操作系统中常见的IPC
(1)、管道(pipe):管道可用于具有亲缘关系的进程间的通信,是一种半双工的方式,数据只能单向流动,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
(2)、命名管道(named pipe):命名管道克服了管道没有名字的限制,同时除了具有管道的功能外(也是半双工),它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
(3)、信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事件发生了,除了进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
(4)、消息队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
(5)、共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
(6)、内存映射:内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
(7)、信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
(8)、套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
3.4,Electron中进程间通信
Electron和Chrome一样,并未使用一个进程,多个线程的模式,而是采用了多进程方式。
Electron和Chrome一样,底层基于chromium。
Electron只有一个主进程:Main Process,其他的都是子进程,叫渲染进程:Renderer Process
现在我们先以Chrome浏览器为例,有多少个tab,就有多少个渲染进程(子进程),还有一个,有且仅有一个的叫Main Process,统领整个程序。他就像一个将军一样,负责其他Renderer Process的创建,分配,控制。这样的好处也很明显,当其中一个tab崩溃,不会影响到其他进程。也正是因为每个tab都是一个进程,而不是更小单位的线程,所以,大家都抱怨Chrome是个吃内存的大户。
我们来看看Mac电脑的Activity Monitor:
可以看到,有一个主进程,下面统领了很多渲染进程(子进程)。
如果电脑运行有Electron程序,我们可以看到大概如下的Activity Monitor,和Chrome如出一辙。
3.5,主进程 - MainProcess特点
3.5.1 可以使用和系统对接的Electron API,如创建菜单,上传文件 等等;
3.5.2 创建渲染进程;
3.5.3 全面支持Node.js;
3.5.4 只有一个,作为整个程序的入口;
3.6,渲染进程 - Renderer Process
3.6.1 可以有多个,每个对应一个窗口;
3.6.2 每个都是一个单独的进程;
3.6.3 全面支持Node.js 和 DOM API;
3.6.4 可以使用一部分Electron 提供的 API;
3.7,主进程和渲染进程访问API范围
上面的图,我们可以清晰的看到主进程和渲染进程,方为Electron API的范围,详细内容可以参考Electron官方文档,,这里不细讲。
Main主进程访问的Electron API要多一些,毕竟它要控制所有的渲染进程,权力更大,所以拥有更多的API访问能力。
由于渲染进程主要是用于渲染网页,在网页中开发业务,所以渲染进程具备访问DOM的能力,而主进程则不需要做这些事情,因为这些属于渲染进程需要做的工作。
无论主进程还是渲染进程,它们被Node.js的环境包裹了,它们都是可以使用Nodejs的。
四、举例应用
4.1,需求
初始化的时候,主进程创建一个视窗A,视窗A有个按钮,点击按钮创建视窗B
4.2,分析
视窗A是一个渲染进程,点击视窗A,A要通知主进程,由主进程再次创建一个渲染进程(视窗B)
4.3 ,创建目录结构
4.4,安装主进程热更新及相关配置
npm install nodemon -D
在package.json中配置
{
"name": "test-electron",
"version": "1.0.0",
"description": "",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "electron .",
"dev": "nodemon --watch main.js --exec 'electron .'"
},
"author": "",
"license": "ISC",
"devDependencies": {
"electron": "^11.2.0"
},
"dependencies": {
"bootstrap": "^4.5.3"
}
}
4.5,安装所需的包
npm install bootstrap -S
4.6,写代码
helper/index.js (工具函数)
exports.$ = id => {
return document.getElementById(id)
}
main.js (主进程-修改前)
const {
app, BrowserWindow, ipcMain, dialog } = require('electron')
app.on('ready', () => {
const windowA = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
})
windowA.loadFile('./renderer/processA/a.html')
windowA.webContents.openDevTools()
ipcMain.on('add-B-window', () => {
const windowB = new BrowserWindow({
width: 600,
height: 400,
webPreferences: {
nodeIntegration: true
},
parent: windowA
})
windowB.loadFile('./renderer/processB/b.html')
windowB.webContents.openDevTools()
})
ipcMain.on('open-pic-file', () => {
dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{
name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif'] }]
}).then(files => {
console.log('files: ', files)
})
})
})
main.js 主进程,修改后
const {
app, BrowserWindow, ipcMain, dialog } = require('electron')
class AppWindow extends BrowserWindow {
constructor(config, htmlFile) {
const baseConfig = {
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true
}
}
const finalConfig = {
...baseConfig, ...config}
super(finalConfig)
this.loadFile(htmlFile)
this.webContents.openDevTools()
}
}
app.on('ready', () => {
const windowA = new AppWindow({
}, './renderer/processA/a.html')
ipcMain.on('add-B-window', () => {
console.log('接到A进程的通知,我来创建进程B')
const windowB = new AppWindow({
width: 600,
height: 400,
parent: windowA
}, './renderer/processB/b.html')
})
ipcMain.on('open-pic-file', () => {
console.log('接到B进程的通知,我来打开系统视窗')
dialog.showOpenDialog({
properties: ['openFile', 'multiSelections'],
filters: [{
name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'gif'] }]
}).then(files => {
console.log('files: ', files)
})
})
})
renderer/processA/a.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Render-A</title>
<link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-4">
<h1>我是渲染进程A</h1>
<button type="button" class="btn btn-primary btn-lg btn-block mt-4" id="add-B-button">点我通知主进程去创建B</button>
</div>
<script>
// Electron里面的渲染进程的js既可以使用node也可以使用DOM,真是绝了
require('./a.js')
</script>
</body>
</html>
renderer/processA/a.js
const {
ipcRenderer } = require('electron')
const {
$ } = require('../../helper')
$('add-B-button').addEventListener('click', () => {
console.log('通知主进程去创建B视窗')
ipcRenderer.send('add-B-window')
})
renderer/processB/b.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Render-B</title>
<link rel="stylesheet" href="../../node_modules/bootstrap/dist/css/bootstrap.min.css">
</head>
<body>
<div class="container mt-4">
<h1>我是渲染进程B</h1>
<button type="button" class="btn btn-outline-primary btn-lg btn-block mt-4" id="select-pic" >请选择图片</button>
</div>
<script>
// Electron里面的渲染进程的js既可以使用node也可以使用DOM,真是绝了
require('./b.js')
</script>
</body>
</html>
renderer/processB/b.js
const {
ipcRenderer } = require('electron')
const {
$ } = require('../../helper')
$('select-pic').addEventListener('click', () => {
console.log('通知主进程去打开操作系统视窗')
ipcRenderer.send('open-pic-file')
})
说明:下面的配置是让渲染进程能够使用nodejs的api,如果不配置,就不能用
webPreferences: {
nodeIntegration: true
}