通过前面的学习,我们知道在Electron中,主进程和渲染进程是被换分成不同的功能的,主进程有完整的Node环境,而渲染进程默认是运行的是浏览器环境。这样划分的好处是:

  1. 安全性:渲染进程运行在沙盒环境中,限制了对底层操作系统的访问和敏感资源的操作。直接在渲染进程中开启 Node.js 环境可能会带来潜在的安全风险,因为 Node.js 具有强大的功能和系统级访问权限,可能会被恶意利用或不当使用。
  2. 性能:渲染进程主要负责显示用户界面,处理用户交互和渲染页面。将 Node.js 环境直接放在渲染进程中可能会对性能产生负面影响,因为 Node.js 的运行环境相对较重,可能会消耗大量的内存和 CPU 资源,从而影响渲染进程的响应性能和用户体验。
  3. 分离关注点:主进程和渲染进程在 Electron 中有不同的职责和关注点。主进程负责管理应用程序的生命周期、系统级功能和与底层操作系统的交互,而渲染进程负责处理用户界面和与用户的交互。通过进程间通信,可以保持这种分离,使代码更易于维护、调试和扩展。

但是我们知道,开发的时候,我们很可能需要在渲染进程中进行一些只能在主进程中运行的操作。这就需要进程间通信了,在渲染进程中需要的时候,可以给主进程发信息,让主进程帮忙处理。

我们之前说过一种直接给渲染进程开启node环境,使用remote模块的方式,并不十分推荐。

预加载脚本(preload

Electron 不推荐在渲染进程开启Nodejs 环境,那也就意味着我们无法在渲染进程中使用NodeJS API,但有时候我们又真的很需要使用NodeJS API。“Preload 脚本”就是用来解决这个问题的。

用大白话来说就是:我们可以在主进程创建窗口的时候,指定一些脚本(内容是我们定的),这些脚本将来是在渲染进程中使用的,但是先于网页内容加载,由于是在主进程的时候就预加载了,所以能使用NodeJS API。

从 Electron 20 开始,预加载脚本默认 沙盒化 ,不再拥有完整 Node.js 环境的访问权,实际上,这意味着我们只拥有一个 require 函数,这个函数只能访问一组有限的 API。

可用的 API详细信息
Electron 模块仅限渲染进程模块
Node.js 模块eventstimersurl
Polyfilled 的全局模块BufferprocessclearImmediatesetImmediate

目前我使用的electron 的版本是 v25。下面写个示例体验一下:

  1. 创建一个预加载脚本 p1.js,我们可以在里面编写我们的代码,此脚本中可以使用Nodejs API。

    // p1.js
    const { contextBridge } = require('electron')
    // 将xxxx对象暴露给渲染进程中的全局对象,以便在渲染进程中直接访问
    contextBridge.exposeInMainWorld('xxxx', {
        name: '如花',
        run: function () {
            return '小子你跑的真快'
        }
    })
    contextBridge.exposeInMainWorld(apiKey, apiObject)

    此方法将指定的 apiObject 对象暴露给渲染进程中的全局对象,以便在渲染进程中直接访问。apiKey 是一个字符串,用于在全局对象中创建一个属性,该属性将指向 apiObject

  2. 创建窗口的时候,指定preload配置

    // main.js
    const win = new BrowserWindow({
      width: 800,
      height: 600,
      webPreferences: {
        // 配置预加载脚本(这里需要是个绝对路径)
        preload: path.join(__dirname, 'p1.js')
      },
    })
    win.loadFile('index.html')
  3. 在渲染进程中使用预加载脚本暴露的方法

    <!--index.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>您好,世界</title>
    </head>
    <body>
        <h1>您好,世界!</h1>
        <script>
            // 调用预处理脚本中暴露的数据
            console.log(xxxx.name);
            console.log(xxxx.run());
        </script>
    </body>
    </html>

参考文档:https://www.electronjs.org/zh/docs/latest/tutorial/tutorial-preload

进程间通信(IPC

在 Electron 中,使用 ipcMainipcRenderer 模块来处理进程间通信。

  • 在主进程中,可以使用 ipcMain 模块监听事件,通过 ipcMain.on() 方法注册事件处理程序,接收渲染进程发送的消息,并通过 event.sender.send() 方法向渲染进程发送回复。
  • 在渲染进程中,可以使用 ipcRenderer 模块发送消息,通过 ipcRenderer.send() 方法发送消息给主进程,并使用 ipcRenderer.on() 方法监听主进程发送的消息。
由于渲染进程中默认无法使用NodeJS API,也就无法使用 require 导入模块,所以我们需要将 ipcRenderer 模块的相关内容在预处理脚本中暴露,才能在渲染进程中使用。

渲染进程向主进程通信(单向)

// main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path');

app.on('ready', () => {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, 'p1.js')
        },
    });
    // 监听 fromSon 事件(频道)
    ipcMain.on('fromSon', function (event, arg1) {
        console.log(arg1);
        // 在主进程中设置窗口的标题
        win.setTitle(arg1)
    })
    win.loadFile('index.html')
})
// p1.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('elecAPI', {
    // 定义sendToFather方法,该方法可以在渲染进程中使用
    sendToFather: function (val) {
        // 使用ipcRenderer.send()方法向主进程指定频道发送信息
        ipcRenderer.send('fromSon', val)
    }
})
<!-- index.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>您好,世界</title>
</head>
<body>
    <h1>您好,世界!</h1>
    <button id="btn">发送</button>
    <script>
        let btn = document.getElementById('btn');
        btn.onclick = function () {
            // 调用预处理脚本中定义的方法,向主进程发送数据
            elecAPI.sendToFather('来自渲染进程的问候')
        }
    </script>
</body>
</html>

上面的代码中,我们在主进程中使用 ipcMain.on()方法监听 fromSon 频道(事件)。在渲染进程中使用 ipcRenderer.send() 方法向fromSon 频道发送数据。

渲染进程和主进程双向通信

这可以通过 ipcRenderer.invokeipcMain.handle 搭配使用来完成双向通信。

  • ipcRenderer.invoke() 方法允许渲染进程向主进程发送请求,并等待主进程返回结果。
  • ipcMain.handle() 方法可以为指定频道注册处理函数,这个处理函数可以接收请求的参数并执行相应的操作,然后返回一个结果给渲染进程。
// main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path');

app.on('ready', () => {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, 'p1.js')
        },
    });
    // 在主进程中监听 fromSon 事件,并为它绑定个处理函数。
    // 在处理函数中return的值就是返回给渲染进程的数据。
    ipcMain.handle('fromSon', function (event, arg1) {
        console.log(arg1);
        return '您的问候已收到,这是我的回复'
    })
    win.loadFile('index.html')
})
// p1.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('elecAPI', {
    // 定义sendToFather方法,该方法可以在渲染进程中使用
    sendToFather: function (val) {
        // 使用ipcRenderer.invoke()方法向主进程指定频道发送信息,它会返回一个Promise
        return ipcRenderer.invoke('fromSon', val)
    }
})
<!-- index.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>您好,世界</title>
</head>
<body>
    <h1>您好,世界!</h1>
    <button id="btn">发送</button>
    <script>
        let btn = document.getElementById('btn');
        btn.onclick = async function () {
            // 调用预处理脚本中定义的方法,向主进程发送数据,并接收返回值
            let res = await elecAPI.sendToFather('来自渲染进程的问候')
            console.log(res);
        }
    </script>
</body>
</html>

主进程向渲染进程通信(单向)

将消息从主进程发送到渲染进程时,需要指定是哪一个渲染进程接收消息。 消息需要通过该渲染进程的 WebContents 实例发送到渲染进程。 此 WebContents 实例包含一个 send 方法,其使用方式与 ipcRenderer.send 相同。

// main.js
const { app, BrowserWindow } = require('electron')
const path = require('path');

app.on('ready', () => {
    const win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            preload: path.join(__dirname, 'p1.js')
        },
    });
    win.loadFile('index.html')

    // 启动5秒后,向渲染进程发送数据
    setTimeout(function () {
        // 获取渲染进程的 WebContents,用使用它的send方法向渲染进程发送数据
        win.webContents.send('toSon', '来自主进程的问候')
    }, 5000)
})
// p1.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('elecAPI', {
    // 定义 fromFather方法,该方法可以在渲染进程中使用
    fromFather: function (callback) {
        // 使用ipcRenderer.on() 方法接收指定频道传来的数据,并用我们传入的处理函数处理它
        ipcRenderer.on('toSon', callback)
    }
})
<!-- index.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>您好,世界</title>
</head>
<body>
    <h1>您好,世界!</h1>
    <script>
        // 使用预处理脚本中定义的函数,间接监听从主进程中传开的数据
        elecAPI.fromFather(function (event, arg1) {
            console.log(arg1);
        })
    </script>
</body>
</html>

渲染进程之间的通信

目前没有直接的方法可以进行渲染进程之间的通信,不过可以将主进程作为渲染进程之间的消息代理。 这需要将消息从一个渲染进程发送到主进程,然后主进程将消息转发到另一个渲染进程。或者使用第三方存储方案(如:localStorage、数据库等)进行中转

参考文档:

https://www.electronjs.org/zh/docs/latest/tutorial/ipc

https://www.electronjs.org/zh/docs/latest/api/ipc-main

https://www.electronjs.org/zh/docs/latest/api/ipc-renderer

Last modification:March 26, 2024
如果觉得我的文章对你有用,请随意赞赏