大家好。今天要和你们分享的是 Electron 中主进程和渲染进程的工作方式。
上一讲我们介绍了 Electron 入门和其主进程和渲染进程的配合模式。我们已经知道这两进程将承担我们所有的业务逻辑。主进程就像是大脑,渲染进程就是被大脑支配的各个肢体,它们配合完成整个软件的高效运行。磨刀不误砍柴工,这一讲我们将继续详细介绍下这两个进程。
什么是进程
进程是正在进行的一个过程或者说一个任务。而负责执行任务则是 cpu。 进程与程序的区别为 程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。一个操作系统级别的进程,或者如 Wikipedia 所述,它是“正在执行的计算机程序的实例”(进程的内存和资源是相互隔离的)。
我们启动一个 Electron 应用程序,然后在 macOS 中通过“活动监视器”可以看到与该程序关联的进程数。其中 “ Electron” 是主进程,一个 “Electron Helper” 是 GPU 进程,还有一个“ Electron Helpers” 是渲染进程。对于 Electron 开发来说,重点关注主进程和渲染进程两个进程即可,其实说是进程,简单点就是我们不同的代码模块划分而已,开发时就是不同的目录结构。不过需要有这个意识去区分不同进程的代码,因为他们的运行时状态是相互独立的,变量也是隔离的。
主进程介绍
主进程 和 渲染进程 是 Electron 框架中非常重要的概念,对于一直做 web 端开发的同学来讲,多进程可能稍微陌生一点,不过其实也不复杂,了解之后,对于程序设计的思路将提供新的方向。在前文已经简单介绍了它们的配合使用,在 package.json 中的入口文件就是指向 Electron 的主进程文件。虽然只是用一个 main.js 文件来表示主进程了,它除了是整个应用的入口点,其实它和渲染进程的差异还有很多,主进程只是运行的一段程序,只有打开了新的窗口,我们才能看见应用的界面,也就是对于可视化的应用,单独运行主进程是没有太大意义的,至少应该打开一个窗口。了解它们有利于我们对项目进行模块设计。
主进程 负责创建和管理窗口实例以及应用的很多事件,比如打开了很多窗口后,这些窗口之间的唯一联系就是通过主进程来完成的,它是应用的大脑,是渲染进程之间的纽带。应用的生面周期事件监听大多也在主进程中控制和管理的,比如应用初始化完成、窗口关闭、应用退出等。它还可以执行诸如注册全局快捷方式,创建系统菜单和对话框,响应自动更新事件等操作。总之很多偏操作系统和应用管理的功能,都需要 主进程 来调用。
渲染进程介绍
我们使用浏览器打开多个网页时,是不是会有多个 tab 标签,并且各个网页之间相互独立,渲染进程 就类似这些不同的标签页,负责运行应用程序的用户界面,并且提供了所有 DOM API。在普通的浏览器里,网页页面跑在一个沙盒环境下,不能接触到 native 源码,所以无法操作本地一些功能。而 Electron 则允许你在页面中使用 Node.js 的 API,即渲染进程打开的网页不仅包含浏览器网页的所有能力,另外还能使用 nodejs 模块,从而和操作系统进行交互。每一个由主进程打开的窗口(BrowserWindow)都是一个独立的 渲染进程。这些独立的窗口的实例引用都在 主进程 管理着,当 BrowserWindow 实例销毁时(比如关闭),对应的渲染进程也就终止了,开发者不用去过多关心进程的管理。如果窗口间需要进行通信,可以通过 主进程 进行消息传递(窗口 1-主进程-窗口 2)。窗口之间内存独立,所以开发时需要注意。
多个渲染进程的好处就是如果某一个页面发送了致命错误,卡死等,不会影响到其他的窗口,也不会导致整个应用崩溃。所以我们在做项目架构设计时尽量将复杂的业务逻辑设计在渲染进程,主进程只存放必须的应用逻辑。
常用模块介绍
Electron 提供了很多模块 api 供我们调用,有些模块是只能在主进程调用的,有一些是只能在渲染进程调用的。还有一些是通用的,比如 前面 demo 里我们用到的 app 模块,就只能是主进程使用,nodejs 的 api 是可以通用的。这个官网上有分 main process 模块和 Renderer 模块,用几次基本就理解了。
下图是网上提供的一份大致清单
下面我们介绍一下最常用的一些模块,更详细的 api 和内容请参考 Electron 官网 。
app 模块(主进程)
app 模块管理着整个应用的生命周期、内置事件和应用的全局信息等 api,例如下面一段代码表示 app 监听到所有窗口都被关闭的事件时,调用退出方法,退出整个应用。
const { app } = require('electron')
app.on('window-all-closed', () => {
app.quit()
})
在比如下面的代码表示监听到应用即将退出,然后在退出前做一些操作(如询问用户等):
const { app } = require('electron')
app.on('before-quit', (event) => {
...// 询问用户是否退出
event.preventDefault(); // 阻止本次退出
})
App 模块还提供很多常用的 api 如下:
app.hide(); // 隐藏整个应用(所有窗口)
app.show(); // 显示被隐藏的窗口
app.getName(); // 获取当前应用的名称
app.getVersion(); // 获取当前应用的版本号
app.showEmojiPanel(); // 打开系统的 emjio 选取器
// 等等
相信到这里你对 App 模块已经比较熟悉了,它是在主进程中调用的一个模块,在相对全局的角度提供了诸多 API 和事件。
这里请思考一个场景:几乎大部分软件,都有单例控制,什么意思? 比如你已经运行了一个软件,当你再次双击图标打开时,不会重新再打开一次,而是直接显示前面已经打开的那个。在 Electron 中,要实现这个就需要依赖 App 模块的 API 来完成。
app.requestSingleInstanceLock()
官网解释是:此方法的返回值表示你的应用程序实例是否成功取得了锁。 如果它取得锁失败,你可以假设另一个应用实例已经取得了锁并且仍旧在运行,并立即退出。
意思就是如果有返回值,表示这是第一次打开应用。否则表示这次是在重复打开,应立即退出。
app.on('second-instance')
监听到第二个实例被打开时触发。
利用上面两个关键 API,我们可以实现 Electron 应用的单例运行,代码如下:
const { app } = require('electron')
let myWindow = null
const gotTheLock = app.requestSingleInstanceLock()
if (!gotTheLock) {
// 检测到本次未取得锁,即有已存在的实例在运行,则本次启动立即退出,不重复启动。
app.quit()
} else {
app.on('second-instance', (event, commandLine, workingDirectory) => {
// 监听到第二个实例被启动时,检测当前实例的主窗口,并显示出来取得焦点
if (myWindow) {
if (myWindow.isMinimized()) myWindow.restore()
myWindow.focus()
}
})
// 创建应用主窗口
app.whenReady().then(() => {
myWindow = createWindow()
})
}
将上面的核心代码与前文的 demo 代码合并在一起,常规的单实例 Electron 应用主进程代码就基本包含所有必要的逻辑了。如果还需要一些其他逻辑处理,可以很方便的添加。不过应该注意模块划分和文件划分,不要都在 main.js 中书写。
ipcMain 模块(主进程)
ipcMain 是一个 EventEmitter 的实例 ,用于在主进程中监听渲染进程发来的消息,同时它也可以给渲染进程回复消息。
Menu 和 MenuItem 模块(主进程)
Menu 和 MenuItem 是用来管理软件顶部菜单栏的内容的,可以通过 MenuItem 自定义每一项菜单的具体内容。
ipcRenderer(渲染进程)
ipcRender 也是一个 EventEmitter 的实例 ,用于在渲染进程中监听和发送消息。它可以将消息发给主进程和其他窗口以及当前页面的 webview 等。
remote 模块(渲染进程)
前面讲了,所有 Electron 提供的可用模块区分主进程和渲染进程。但是有的时候需要在渲染进程使用主进程模块的功能,这时就需要用到 remote 来辅助了。比如前面我们用到的 app 和 BrowserWindow 两个模块都是主进程使用的,使用方式为
// 主进程导入 app,BrowserWindow
const { app,BrowserWindow } = require('electron')
// 最新版已修改为
// const { app, BrowserWindow } = require('@electron/remote')
在渲染进程中我们可以按如下的方式使用,这样可以满足在某些场景下,需要在渲染进程直接打开新的窗口等。
// 渲染进程导入 app,BrowserWindow
const { app, BrowserWindow } = require('electron').remote
const win = new BrowserWindow({ width: 800, height: 600 })
win.loadURL('https://github.com')
还有很多其他 Electron 提供的模块可以满足我们大部分应用场景,如 shell 模块是在两个进程都可以使用的模块,提供与桌面集成相关的功能等。当然 Node.js 的模块 API 也是可以直接在主进程和渲染进程引入和使用的,比如文件操作模块 fs:
const fs = require('fs');
fs.readFile("d:xxx\record.txt", 'utf8', function (err, data) {
if (err) return console.log(err);
});
clipboard 模块(主进程&渲染进程)
clipboard 是 Electron 提供的一个用于读写剪切板的模块,在浏览器 Web 应用中读取用户剪切板内容是由诸多限制的,但在 Electron 中,通过此模块可以很方便的操作剪切板,这样就可以定制复制剪切粘贴等功能。这个模块在主进程和渲染进程都可以直接引入并使用。如下面代码表示设置和读取剪切板内容,当然不只是 Text,还可以是图片、Html、Markdown 等。
const { clipboard } = require('electron')
clipboard.writeText('hello i am a bit of text!')
const text = clipboard.readText()
console.log(text) // hello i am a bit of text!'
shell 模块(主进程&渲染进程)
shell 提供与桌面集成相关的功能,如在浏览器中打开一个 url,或者自动打开指定文件夹等。
应用生命周期
前面已经讲到主进程模块 App 控制着 Electron 应用的生命周期,并且知道了 window-all-closed(当所有窗口都已关闭时触发)。强调一下,所有窗口都关闭不代表应用退出,只能说渲染进程都关闭了,主进程还在运行,默认情况如果没有监听,Electron 会自动退出主进程,但如果我们监听了此事件,就需要我们主动退出。那么整个应用还有哪些有关生命周期的事件呢?
will-finish-launching
当应用程序完成基础的启动的时候被触发,在 Window 和 Linux 中,和‘ready’是相同的,在 Mac 中有一些区别。
ready
当 Electron 完成初始化时,发出一次。
window-all-closed
当所有窗口(渲染进程)都被关闭时触发,如上面说明。注意,如果用户强退或者在 Mac 系统按下了 Cmd + Q,或者程序主动调用 app.quit(),此事件将不会触发。所以可以在监听到此事件后调用 app.quit() 来退出整个程序。
before-quit
在程序关闭窗口前触发,调用 app.quit()会触发此事件。可以在监听中通过 event.preventDefault() 来阻止关闭窗口。
Windows 中因系统关机或注销不会触发此事件,所以如果依赖其做一些重要信息自动保存功能将会有风险。
will-quit
在应用关闭所有窗口后,即将退出程序时触发。(before-quit 是关闭窗口前先触发)。调用 app.quit()会触发此事件。可以在监听中通过 event.preventDefault() 来阻止退出。同样的,Windows 中因系统关机或注销不会触发此事件
quit
在应用程序退出时触发,此时已经到退出的最后阶段。同样的,Windows 中因系统关机或注销不会触发此事件
下图摘自网络,可参考
应用退出
如果是用户点击关闭或者其他操作退出应用,将会按流程触发上面的事件。如果程序需要主动退出,一般我们可以通过 app.quit() 或 app.exit() 来实现。
app.quit()
尝试关闭所有窗口 将依次触发 before-quit 和 will-quit 事件。也就是这个时候我们可以去取消退出的。
app.exit()
所有窗口都将立即被关闭,而不询问用户,而且 before-quit 和 will-quit 事件也不会被触发 。
补充:主进程和渲染进程通信方式之一
主进程和渲染进行属于不同的进程了,所以内存变量什么的都不能直接互通了,因此需要有两个进程的通信机制,electron 提供了多种通信方式,实际使用中一般也会根据实际场景去做一些二次封装,以便程序调用。
// 渲染进程发送
ipcRenderer.invoke('render-msg', {name: '张三', age:18})
// 主进程接收
ipcMain.handle('render-msg', (e, msg) => {
console.log('msg', msg)
})
总结
本节加深了主进程和渲染进程的概念理解,并且完善了主进程中核心逻辑的程序演示。分别梳理了各个模块的大致功能和调用方式,知道一些常用模块是用来干什么的。最后描述了 Electron 应用中关于生命周期的常用事件的监听以及程序退出机制。本节介绍的模块相对是零散的,主要是为了针对一些实战中常用的知识点,先形成一些初步了解,心里有一个印象即可,不需要去刻意记忆,后续将对核心知识点进行项目化结合演示,到时将不再那么陌生。
下一讲给大家介绍基于 Electron 开发时的本地调试方式,将学习到如何去调试基于多窗口的渲染进程和主进程。
思考
既然主进程和渲染进程是相对独立的运行进程,甚至从代码上都已经隔离。那么我们思考这个问题:运行时两个窗口是如何进行数据传输的,比如我在 A 窗口点击按钮“关闭其他窗口”,怎么能让其他窗口知道并且关闭自己呢? 浏览器中的全局变量在进程中又是否还能共享?