6. 主进程与渲染进程

一、前言

前面一些章节我们主要学习了窗口的基本使用。

在此基础上,其实你就已经能够开始开发一些简单的“浏览器应用”了。

之所以将其称为“浏览器应用”,是因为到目前为止,我们依旧只能写网页开发写的那些东西而已。

比如,一个最基本的软件应该可以在页面上通过点击按钮或者其它交互方式,达到删除、读取、修改文件的能力吧?

但目前为止我们是做不到的这一点的,因为你现在依旧只能在页面中写网页开发的那点东西,也就是csshtmljs,其中的js也只有最基本的网页API可以让你调用。

为了能够做到这一点,我们就需要了解Electron关于主进程与渲染进程的关系。

二、主进程与渲染进程

虽然听名字感觉很高大上,但实际上就是两种划分了使用权限的进程而已,如果不知道进程是什么,可以参考这篇文章:进程与线程

其中的主进程,其实指的就是我们前面写的那个入口文件,main.js或者index.js,这取决于你配置文件中main填的什么。

渲染进程,其实就是我们的页面,也就是你创建窗口时,执行窗口内容的其实就是一个进程,只不过被命名了渲染进程,因为它就是用来渲染界面的。

而两者的区别就在于,在主进程中,也就是入口文件中,你拥有最高权限,可以访问任意内容,比如你就可以在该进程中对电脑磁盘上的文件数据做任意的操作。

而在渲染进程中,你就只能使用基本浏览器提供的API(html页面文件中引入js代码),操作页面,而无法直接操作电脑文件。

按照官方说法,这是为了安全,因为正如前面我们所看到的,加载页面非常简单,直接在主进程中调用一个loadFile函数就行了,甚至还可以加载网页(loadURL)。

而这就会导致渲染进程中的代码很可能不可控。

vscode这种支持插件的软件,是需要将其它开发者开发的代码加载到本程序的,如果有恶意程序员滥用这种权限,就会造成非常严重的数据安全问题。

所以总结来说就是:主进程拥有所有权限,而渲染进程只拥有操作页面的权限。

三、进程通信

但正如前面所说,我们的界面是在渲染进程中的。

而我们想要实现通过点击按钮来增删文件,用单一进程是肯定无法实现的:

  1. 你的点击按钮事件是发生在渲染进程中的,而渲染进程没有操作文件的权限。
  2. 主进程虽然有操作文件的权限,但没办法绘制界面让你用,也就是主进程监听不到用户的行为。

对于这个问题,官方给出的解决方案就是进程间通信:

  1. 渲染进程监听用户在界面上的行为。
  2. 如果用户想要操作本地文件等高权限的行为,那么渲染进程就通知主进程一声。
  3. 主进程监听到渲染进程的信息,就去执行,再将结果返回给渲染进程。
  4. 渲染进程得到结果,再将结果提示给用户。

也就是稍稍绕了一个弯而已,但为了安全,这种行为也是必要的。

为什么这样就安全了呢?

因为事件的处理程序是需要作者提前写好的,也就是在入口文件中,你需要提前写好你想要处理哪些来自渲染进程的事件,包括如何处理,都是你自己说了算。

即使加载了第三方的渲染进程代码进来,能调用的也只有你暴露出来的方法而已。

所以归根结底,这样做的目的是将程序的安全问题完全交给开发作者而已,并不是就绝对安全了。

比如,你在主进程中主动暴露出来了一个叫做delete的删除事件处理消息,并且加载了一个恶意程序,它可能就会疯狂调用你暴露出来的这个接口,将用户电脑上的数据全部删除干净,这就是最严重的安全漏洞!

进程通信的简称为IPCInter-Process Communication缩写,官网以及一些博客中很常见)。

四、基本使用

官方文档可以点击这里查看。

简单来说就是,主进程可以使用一个叫做ipcMain的模块,而渲染进程可以使用一个叫做ipcRenderer的模块,来实现两者之间的通信。

为了方便,最好新建一个专门执行渲染进程代码的文件:

image-20230929101049852

还是前面的那个项目,只是在该文件中新建了一个叫做render.js的文件,并且在该页面文件(index.html)中引入了它而已:

<button id="test">test</button>
<script src="./render.js"></script>

这是前端开发的基础知识,这里不再过多赘述。

然后当我们主进程中创建窗口、并加载这个页面之后,由于render.js文件被引入到了该文件之中,所以这个文件就会在渲染进程中被执行。

同时为了后面能让渲染进程发送消息给主进程,我们还创建一个按钮test

其目的就是,当我们按下这个按钮时,渲染进程向主进程发送一个请求,然后主进程再给渲染进程一个回应。

首先是渲染进程主进程发送消息的过程,按理来说,我们可能就会像下面这样写代码:

image-20230929103046294

但这样是错误的,原因也是前面提到过的,渲染进程只能访问浏览器本身提供的api,它没有权限访问nodejs模块,包括我们这里的electron模块,因为它同样也是一个我们下载下来的node模块。

这个时候怎么办?怎么感觉有点无解呀!

而这就是前面预处理脚本preload.js发挥作用的时候了,它同样处于渲染进程内,但给了它更多的权限。比如它就可以直接访问到node模块。

所以一般来说,它的目的就是向渲染进程暴露接口的。

image-20230929105822158

上面就是一次完整的渲染进程主进程发送消息的过程。

同时为了好看,我将主进程入口文件index.js中的代码精简了很多,只保留了创建窗口的部分。

此时三个文件的代码如下:

index.js:

const { app, BrowserWindow,ipcMain } = require('electron');
const path = require('path');

//创建窗口的函数
const createWindow = () => {
  // 创建窗口
  const win1 = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  });
  // 加载页面
  win1.loadFile(path.join(__dirname, 'index.html'));
};
app.on('ready', createWindow);

//处理渲染进程发送来的消息:test-msg
ipcMain.on('test-msg', (event, msg)=>{
  console.log('test-msg:',msg);
})

preload.js:

const {contextBridge,ipcRenderer} =require('electron');
//向渲染进程中暴露接口
contextBridge.exposeInMainWorld('electronAPI', {
    testmsg: (msg) => ipcRenderer.send('test-msg', msg)
})

render.js:

//得到按钮
const btn=document.querySelector('#test');
//给按钮添加一个点击事件
btn.addEventListener('click',() => {
    //调用由preload.js暴露出来的接口
    window.electronAPI.testmsg('用户按下了按钮!');
});

下面再来分别详细解释一下它们的含义。

首先是第一步,加载预处理脚本,这个就是固定步骤,在创建窗口的时候用了一个叫做webPreferences的属性,这个属性同时又是一个对象,然后在这个对象里面再添加一个叫做preload的属性,也就是这个窗口要加载的预处理脚本的路径。

然后是第二步,我们想要自定义监听一个事件,然后处理一些事情,这就用到了ipcMain模块,而且用的还是一个我们应该已经很熟悉的函数了:on

//处理渲染进程发送来的消息:test-msg
ipcMain.on('test-msg', (event, msg)=>{
  console.log('test-msg:',msg);
})

以前用on函数,我们填写的都是官方已经写好的事件,而现在,我们就可以自己自定义任意事件了,也就是自己随便取个名字。

比如我这里取的就是test-msg

然后它的回调函数,其第一个参数就是发送来的事件对象,这个后面再提,如果你想要获取这个事件发送来的参数,就可以继续在后面填参数名就行了。

比如我这里想让渲染进程给我发送一个字符串,我在主进程中来打印这个字符,所以我就多写了一个叫做msg的参数,这也是任意的。

写好自定义消息的回调函数之后,来到预处理脚本处:

//向渲染进程中暴露接口
contextBridge.exposeInMainWorld('electronAPI', {
    testmsg: (msg) => ipcRenderer.send('test-msg', msg)
})

想要暴露给渲染进程API,就需要用到这个叫contextBridge的模块,直译过来就是上下文桥梁,理解一下就是连接渲染进程的桥梁。

然后调用它上面的一个叫做exposeInMainWorld的函数,它的作用就是暴露方法、属性等等一切东西给渲染进程

它有两个参数,其中第一个参数就是暴露给渲染进程的对象名称(在渲染进程中,可以通过全局对象window上访问到),其后就是这个对象上的东西,对象嘛,自然得用大括号{}传递。了。

然后在这个对象中,这里只写了一个属性:testmsg,并且这个属性是一个函数,而且写的还是一个箭头函数:

(msg) => ipcRenderer.send('test-msg', msg)

这个箭头函数的唯一用处就是,使用ipcRenderer模块,调用其上的send函数,向主进程发送我们想要触发的消息。

第一个参数就是消息名,也就是前面我们在主进程中监听的消息事件名:test-msg

后面的参数就是任意的了,因为我们这个自定义的事件处理函数需要一个参数,所以这里就传递了一个参数msg,这个参数就是从外面传递进这个箭头函数的了。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux