进程间通信

进程间通信 (IPC) 是在 Electron 中构建功能丰富的桌面应用程序的关键部分之一。 由于主进程和渲染器进程在 Electron 的进程模型具有不同的职责,因此 IPC 是执行许多常见任务的唯一方法,例如从 UI 调用原生 API 或从原生菜单触发 Web 内容的更改。

IPC 通道

在 Electron 中,进程使用 ipcMainipcRenderer 模块,通过开发人员定义的“通道”传递消息来进行通信。 这些通道是 任意 (您可以随意命名它们)和 双向 (您可以在两个模块中使用相同的通道名称)的。

在本指南中,我们将介绍一些基本的 IPC 模式,并提供具体的示例。您可以将这些示例作为您应用程序代码的参考。

了解上下文隔离进程

在开始实现细节之前,您应该熟悉使用 预加载脚本 在上下文隔离渲染器进程中导入 Node.js 和 Electron 模块的概念。

  • 有关 Electron 进程模型的完整概述,您可以阅读 进程模型文档
  • 有关使用 contextBridge 模块从预加载脚本暴露 API 的入门知识,请查看 上下文隔离教程

模式 1:渲染器进程到主进程(单向)

要将单向 IPC 消息从渲染器进程发送到主进程,您可以使用 ipcRenderer.send API 发送消息,然后使用 ipcMain.on API 接收。

通常使用此模式从 Web 内容调用主进程 API。 我们将通过创建一个简单的应用来演示此模式,可以通过编程方式更改它的窗口标题。

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

docs/fiddles/ipc/pattern-1 (23.0.0)Open in Fiddle

  • main.js
  • preload.js
  • index.html
  • renderer.js
  1. const {app, BrowserWindow, ipcMain} = require('electron')
  2. const path = require('path')
  3. function createWindow () {
  4. const mainWindow = new BrowserWindow({
  5. webPreferences: {
  6. preload: path.join(__dirname, 'preload.js')
  7. }
  8. })
  9. ipcMain.on('set-title', (event, title) => {
  10. const webContents = event.sender
  11. const win = BrowserWindow.fromWebContents(webContents)
  12. win.setTitle(title)
  13. })
  14. mainWindow.loadFile('index.html')
  15. }
  16. app.whenReady().then(() => {
  17. createWindow()
  18. app.on('activate', function () {
  19. if (BrowserWindow.getAllWindows().length === 0) createWindow()
  20. })
  21. })
  22. app.on('window-all-closed', function () {
  23. if (process.platform !== 'darwin') app.quit()
  24. })
  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI', {
  3. setTitle: (title) => ipcRenderer.send('set-title', title)
  4. })
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Hello World!</title>
  8. </head>
  9. <body>
  10. Title: <input id="title"/>
  11. <button id="btn" type="button">Set</button>
  12. <script src="./renderer.js"></script>
  13. </body>
  14. </html>
  1. const setButton = document.getElementById('btn')
  2. const titleInput = document.getElementById('title')
  3. setButton.addEventListener('click', () => {
  4. const title = titleInput.value
  5. window.electronAPI.setTitle(title)
  6. });

1. 使用 ipcMain.on 监听事件

在主进程中,使用 ipcMain.on API 在 set-title 通道上设置一个 IPC 监听器:

main.js (Main Process)

  1. const {app, BrowserWindow, ipcMain} = require('electron')
  2. const path = require('path')
  3. //...
  4. function handleSetTitle (event, title) {
  5. const webContents = event.sender
  6. const win = BrowserWindow.fromWebContents(webContents)
  7. win.setTitle(title)
  8. }
  9. function createWindow () {
  10. const mainWindow = new BrowserWindow({
  11. webPreferences: {
  12. preload: path.join(__dirname, 'preload.js')
  13. }
  14. })
  15. mainWindow.loadFile('index.html')
  16. }
  17. app.whenReady().then(() => {
  18. ipcMain.on('set-title', handleSetTitle)
  19. createWindow()
  20. }
  21. //...

上面的 handleSetTitle 回调函数有两个参数:一个 IpcMainEvent 结构和一个 title 字符串。 每当消息通过 set-title 通道传入时,此函数找到附加到消息发送方的 BrowserWindow 实例,并在该实例上使用 win.setTitle API。

进程间通信 - 图1info

请确保您为以下步骤加载了 index.htmlpreload.js 入口点!

2. 通过预加载脚本暴露 ipcRenderer.send

要将消息发送到上面创建的监听器,您可以使用 ipcRenderer.send API。 默认情况下,渲染器进程没有权限访问 Node.js 和 Electron 模块。 作为应用开发者,您需要使用 contextBridge API 来选择要从预加载脚本中暴露哪些 API。

在您的预加载脚本中添加以下代码,向渲染器进程暴露一个全局的 window.electronAPI 变量。

preload.js (Preload Script)

  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI', {
  3. setTitle: (title) => ipcRenderer.send('set-title', title)
  4. })

此时,您将能够在渲染器进程中使用 window.electronAPI.setTitle() 函数。

进程间通信 - 图2安全警告

出于 安全原因,我们不会直接暴露整个 ipcRenderer.send API。 确保尽可能限制渲染器对 Electron API 的访问。

3. 构建渲染器进程 UI

在 BrowserWindow 加载的我们的 HTML 文件中,添加一个由文本输入框和按钮组成的基本用户界面:

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Hello World!</title>
  8. </head>
  9. <body>
  10. Title: <input id="title"/>
  11. <button id="btn" type="button">Set</button>
  12. <script src="./renderer.js"></script>
  13. </body>
  14. </html>

为了使这些元素具有交互性,我们将在导入的 renderer.js 文件中添加几行代码,以利用从预加载脚本中暴露的 window.electronAPI 功能:

renderer.js (Renderer Process)

  1. const setButton = document.getElementById('btn')
  2. const titleInput = document.getElementById('title')
  3. setButton.addEventListener('click', () => {
  4. const title = titleInput.value
  5. window.electronAPI.setTitle(title)
  6. });

此时,您的演示应用应该已经功能齐全。 尝试使用输入框,看看 BrowserWindow 的标题会发生什么变化!

模式 2:渲染器进程到主进程(双向)

双向 IPC 的一个常见应用是从渲染器进程代码调用主进程模块并等待结果。 这可以通过将 ipcRenderer.invokeipcMain.handle 搭配使用来完成。

在下面的示例中,我们将从渲染器进程打开一个原生的文件对话框,并返回所选文件的路径。

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

docs/fiddles/ipc/pattern-2 (23.0.0)Open in Fiddle

  • main.js
  • preload.js
  • index.html
  • renderer.js
  1. const {app, BrowserWindow, ipcMain, dialog} = require('electron')
  2. const path = require('path')
  3. async function handleFileOpen() {
  4. const { canceled, filePaths } = await dialog.showOpenDialog()
  5. if (canceled) {
  6. return
  7. } else {
  8. return filePaths[0]
  9. }
  10. }
  11. function createWindow () {
  12. const mainWindow = new BrowserWindow({
  13. webPreferences: {
  14. preload: path.join(__dirname, 'preload.js')
  15. }
  16. })
  17. mainWindow.loadFile('index.html')
  18. }
  19. app.whenReady().then(() => {
  20. ipcMain.handle('dialog:openFile', handleFileOpen)
  21. createWindow()
  22. app.on('activate', function () {
  23. if (BrowserWindow.getAllWindows().length === 0) createWindow()
  24. })
  25. })
  26. app.on('window-all-closed', function () {
  27. if (process.platform !== 'darwin') app.quit()
  28. })
  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI',{
  3. openFile: () => ipcRenderer.invoke('dialog:openFile')
  4. })
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Dialog</title>
  8. </head>
  9. <body>
  10. <button type="button" id="btn">Open a File</button>
  11. File path: <strong id="filePath"></strong>
  12. <script src='./renderer.js'></script>
  13. </body>
  14. </html>
  1. const btn = document.getElementById('btn')
  2. const filePathElement = document.getElementById('filePath')
  3. btn.addEventListener('click', async () => {
  4. const filePath = await window.electronAPI.openFile()
  5. filePathElement.innerText = filePath
  6. })

1. 使用 ipcMain.handle 监听事件

在主进程中,我们将创建一个 handleFileOpen() 函数,它调用 dialog.showOpenDialog 并返回用户选择的文件路径值。 每当渲染器进程通过 dialog:openFile 通道发送 ipcRender.invoke 消息时,此函数被用作一个回调。 然后,返回值将作为一个 Promise 返回到最初的 invoke 调用。

进程间通信 - 图3关于错误处理

在主进程中通过 handle 引发的错误是不透明的,因为它们被序列化了,并且只有原始错误的 message 属性会提供给渲染器进程。 详情请参阅 [#24427](https://github.com/electron/electron/issues/24427)。

main.js (Main Process)

  1. const { BrowserWindow, dialog, ipcMain } = require('electron')
  2. const path = require('path')
  3. //...
  4. async function handleFileOpen() {
  5. const { canceled, filePaths } = await dialog.showOpenDialog()
  6. if (canceled) {
  7. return
  8. } else {
  9. return filePaths[0]
  10. }
  11. }
  12. function createWindow () {
  13. const mainWindow = new BrowserWindow({
  14. webPreferences: {
  15. preload: path.join(__dirname, 'preload.js')
  16. }
  17. })
  18. mainWindow.loadFile('index.html')
  19. }
  20. app.whenReady(() => {
  21. ipcMain.handle('dialog:openFile', handleFileOpen)
  22. createWindow()
  23. })
  24. //...

进程间通信 - 图4关于通道名称

IPC 通道名称上的 dialog: 前缀对代码没有影响。 它仅用作命名空间以帮助提高代码的可读性。

进程间通信 - 图5info

请确保您为以下步骤加载了 index.htmlpreload.js 入口点!

2. 通过预加载脚本暴露 ipcRenderer.invoke

在预加载脚本中,我们暴露了一个单行的 openFile 函数,它调用并返回 ipcRenderer.invoke('dialog:openFile') 的值。 我们将在下一步中使用此 API 从渲染器的用户界面调用原生对话框。

preload.js (Preload Script)

  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI', {
  3. openFile: () => ipcRenderer.invoke('dialog:openFile')
  4. })

进程间通信 - 图6安全警告

出于 安全原因,我们不会直接暴露整个 ipcRenderer.invoke API。 确保尽可能限制渲染器对 Electron API 的访问。

3. 构建渲染器进程 UI

最后,让我们构建加载到 BrowserWindow 中的 HTML 文件。

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Dialog</title>
  8. </head>
  9. <body>
  10. <button type="button" id="btn">Open a File</button>
  11. File path: <strong id="filePath"></strong>
  12. <script src='./renderer.js'></script>
  13. </body>
  14. </html>

用户界面包含一个 #btn 按钮元素,将用于触发我们的预加载 API,以及一个 #filePath 元素,将用于显示所选文件的路径。 要使这些部分起作用,需要在渲染器进程脚本中编写几行代码:

renderer.js (Renderer Process)

  1. const btn = document.getElementById('btn')
  2. const filePathElement = document.getElementById('filePath')
  3. btn.addEventListener('click', async () => {
  4. const filePath = await window.electronAPI.openFile()
  5. filePathElement.innerText = filePath
  6. })

在上面的代码片段中,我们监听 #btn 按钮的点击,并调用 window.electronAPI.openFile() API 来激活原生的打开文件对话框。 然后我们在 #filePath 元素中显示选中文件的路径。

注意:对于旧方法

ipcRenderer.invoke API 是在 Electron 7 中添加的,作为处理渲染器进程中双向 IPC 的一种开发人员友好的方式。 但这种 IPC 模式存在几种替代方法。

进程间通信 - 图7如果可能,请避免使用旧方法

我们建议尽可能使用 ipcRenderer.invoke 。 出于保留历史的目地,记录了下面双向地渲染器到主进程模式。

进程间通信 - 图8info

对于以下示例,我们将直接从预加载脚本调用 ipcRenderer,以保持代码示例短小。

使用 ipcRenderer.send

我们用于单向通信的 ipcRenderer.send API 也可用于双向通信。 这是在 Electron 7 之前通过 IPC 进行异步双向通信的推荐方式。

preload.js (Preload Script)

  1. // 您也可以使用 `contextBridge` API
  2. // 将这段代码暴露给渲染器进程
  3. const { ipcRenderer } = require('electron')
  4. ipcRenderer.on('asynchronous-reply', (_event, arg) => {
  5. console.log(arg) // 在 DevTools 控制台中打印“pong”
  6. })
  7. ipcRenderer.send('asynchronous-message', 'ping')

main.js (Main Process)

  1. ipcMain.on('asynchronous-message', (event, arg) => {
  2. console.log(arg) // 在 Node 控制台中打印“ping”
  3. // 作用如同 `send`,但返回一个消息
  4. // 到发送原始消息的渲染器
  5. event.reply('asynchronous-reply', 'pong')
  6. })

这种方法有几个缺点:

  • 您需要设置第二个 ipcRenderer.on 监听器来处理渲染器进程中的响应。 使用 invoke,您将获得作为 Promise 返回到原始 API 调用的响应值。
  • 没有显而易见的方法可以将 asynchronous-reply 消息与原始的 asynchronous-message 消息配对。 如果您通过这些通道非常频繁地来回传递消息,则需要添加其他应用代码来单独跟踪每个调用和响应。

使用 ipcRenderer.sendSync

ipcRenderer.sendSync API 向主进程发送消息,并 同步 等待响应。

main.js (Main Process)

  1. const { ipcMain } = require('electron')
  2. ipcMain.on('synchronous-message', (event, arg) => {
  3. console.log(arg) // 在 Node 控制台中打印“ping”
  4. event.returnValue = 'pong'
  5. })

preload.js (Preload Script)

  1. // 您也可以使用 `contextBridge` API
  2. // 将这段代码暴露给渲染器进程
  3. const { ipcRenderer } = require('electron')
  4. const result = ipcRenderer.sendSync('synchronous-message', 'ping')
  5. console.log(result) // 在 DevTools 控制台中打印“pong”

这份代码的结构与 invoke 模型非常相似,但出于性能原因,我们建议避免使用此 API。 它的同步特性意味着它将阻塞渲染器进程,直到收到回复为止。

模式 3:主进程到渲染器进程

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

为了演示此模式,我们将构建一个由原生操作系统菜单控制的数字计数器。

对于此演示,您需要将代码添加到主进程、渲染器进程和预加载脚本。 完整代码如下,我们将在后续章节中对每个文件进行单独解释。

docs/fiddles/ipc/pattern-3 (23.0.0)Open in Fiddle

  • main.js
  • preload.js
  • index.html
  • renderer.js
  1. const {app, BrowserWindow, Menu, ipcMain} = require('electron')
  2. const path = require('path')
  3. function createWindow () {
  4. const mainWindow = new BrowserWindow({
  5. webPreferences: {
  6. preload: path.join(__dirname, 'preload.js')
  7. }
  8. })
  9. const menu = Menu.buildFromTemplate([
  10. {
  11. label: app.name,
  12. submenu: [
  13. {
  14. click: () => mainWindow.webContents.send('update-counter', 1),
  15. label: 'Increment',
  16. },
  17. {
  18. click: () => mainWindow.webContents.send('update-counter', -1),
  19. label: 'Decrement',
  20. }
  21. ]
  22. }
  23. ])
  24. Menu.setApplicationMenu(menu)
  25. mainWindow.loadFile('index.html')
  26. // Open the DevTools.
  27. mainWindow.webContents.openDevTools()
  28. }
  29. app.whenReady().then(() => {
  30. ipcMain.on('counter-value', (_event, value) => {
  31. console.log(value) // will print value to Node console
  32. })
  33. createWindow()
  34. app.on('activate', function () {
  35. if (BrowserWindow.getAllWindows().length === 0) createWindow()
  36. })
  37. })
  38. app.on('window-all-closed', function () {
  39. if (process.platform !== 'darwin') app.quit()
  40. })
  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI', {
  3. handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
  4. })
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Menu Counter</title>
  8. </head>
  9. <body>
  10. Current value: <strong id="counter">0</strong>
  11. <script src="./renderer.js"></script>
  12. </body>
  13. </html>
  1. const counter = document.getElementById('counter')
  2. window.electronAPI.handleCounter((event, value) => {
  3. const oldValue = Number(counter.innerText)
  4. const newValue = oldValue + value
  5. counter.innerText = newValue
  6. event.sender.send('counter-value', newValue)
  7. })

1. 使用 webContents 模块发送消息

对于此演示,我们需要首先使用 Electron 的 Menu 模块在主进程中构建一个自定义菜单,该模块使用 webContents.send API 将 IPC 消息从主进程发送到目标渲染器。

main.js (Main Process)

  1. const {app, BrowserWindow, Menu, ipcMain} = require('electron')
  2. const path = require('path')
  3. function createWindow () {
  4. const mainWindow = new BrowserWindow({
  5. webPreferences: {
  6. preload: path.join(__dirname, 'preload.js')
  7. }
  8. })
  9. const menu = Menu.buildFromTemplate([
  10. {
  11. label: app.name,
  12. submenu: [
  13. {
  14. click: () => mainWindow.webContents.send('update-counter', 1),
  15. label: 'Increment',
  16. },
  17. {
  18. click: () => mainWindow.webContents.send('update-counter', -1),
  19. label: 'Decrement',
  20. }
  21. ]
  22. }
  23. ])
  24. Menu.setApplicationMenu(menu)
  25. mainWindow.loadFile('index.html')
  26. }
  27. //...

出于本教程的目的,请务必注意, click 处理函数通过 update-counter 通道向渲染器进程发送消息(1-1)。

  1. click: () => mainWindow.webContents.send('update-counter', -1)

进程间通信 - 图9info

请确保您为以下步骤加载了 index.htmlpreload.js 入口点!

2. 通过预加载脚本暴露 ipcRenderer.on

与前面的渲染器到主进程的示例一样,我们使用预加载脚本中的 contextBridgeipcRenderer 模块向渲染器进程暴露 IPC 功能:

preload.js (Preload Script)

  1. const { contextBridge, ipcRenderer } = require('electron')
  2. contextBridge.exposeInMainWorld('electronAPI', {
  3. onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
  4. })

加载预加载脚本后,渲染器进程应有权访问 window.electronAPI.onUpdateCounter() 监听器函数。

进程间通信 - 图10安全警告

出于 安全原因,我们不会直接暴露整个 ipcRenderer.on API。 确保尽可能限制渲染器对 Electron API 的访问。

进程间通信 - 图11info

在这个最小示例中,您可以直接在预加载脚本中调用 ipcRenderer.on ,而不是通过 context bridge 暴露它。

preload.js (Preload Script)

  1. const { ipcRenderer } = require('electron')
  2. window.addEventListener('DOMContentLoaded', () => {
  3. const counter = document.getElementById('counter')
  4. ipcRenderer.on('update-counter', (_event, value) => {
  5. const oldValue = Number(counter.innerText)
  6. const newValue = oldValue + value
  7. counter.innerText = newValue
  8. })
  9. })

但是,与通过 context bridge 暴露预加载 API 相比,此方法的灵活性有限,因为监听器无法直接与渲染器代码交互。

3. 构建渲染器进程 UI

为了将它们联系在一起,我们将在加载的 HTML 文件中创建一个接口,其中包含一个 #counter 元素,我们将使用该元素来显示值:

index.html

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="UTF-8">
  5. <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
  6. <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
  7. <title>Menu Counter</title>
  8. </head>
  9. <body>
  10. Current value: <strong id="counter">0</strong>
  11. <script src="./renderer.js"></script>
  12. </body>
  13. </html>

最后,为了更新 HTML 文档中的值,我们将添加几行 DOM 操作的代码,以便在每次触发 update-counter 事件时更新 #counter 元素的值。

renderer.js (Renderer Process)

  1. const counter = document.getElementById('counter')
  2. window.electronAPI.onUpdateCounter((_event, value) => {
  3. const oldValue = Number(counter.innerText)
  4. const newValue = oldValue + value
  5. counter.innerText = newValue
  6. })

在上面的代码中,我们将回调传递给从预加载脚本中暴露的 window.electronAPI.onUpdateCounter 函数。 第二个 value 参数对应于我们传入 webContents.send 函数的 1-1,该函数是从原生菜单调用的。

可选:返回一个回复

对于从主进程到渲染器进程的 IPC,没有与 ipcRenderer.invoke 等效的 API。 不过,您可以从 ipcRenderer.on 回调中将回复发送回主进程。

我们可以对前面例子的代码进行略微修改来演示这一点。 在渲染器进程中,使用 event 参数,通过 counter-value 通道将回复发送回主进程。

renderer.js (Renderer Process)

  1. const counter = document.getElementById('counter')
  2. window.electronAPI.onUpdateCounter((event, value) => {
  3. const oldValue = Number(counter.innerText)
  4. const newValue = oldValue + value
  5. counter.innerText = newValue
  6. event.sender.send('counter-value', newValue)
  7. })

在主进程中,监听 counter-value 事件并适当地处理它们。

main.js (Main Process)

  1. //...
  2. ipcMain.on('counter-value', (_event, value) => {
  3. console.log(value) // 将打印到 Node 控制台
  4. })
  5. //...

模式 4:渲染器进程到渲染器进程

没有直接的方法可以使用 ipcMainipcRenderer 模块在 Electron 中的渲染器进程之间发送消息。 为此,您有两种选择:

  • 将主进程作为渲染器之间的消息代理。 这需要将消息从一个渲染器发送到主进程,然后主进程将消息转发到另一个渲染器。
  • 从主进程将一个 MessagePort 传递到两个渲染器。 这将允许在初始设置后渲染器之间直接进行通信。

对象序列化

Electron 的 IPC 实现使用 HTML 标准的 结构化克隆算法 来序列化进程之间传递的对象,这意味着只有某些类型的对象可以通过 IPC 通道传递。

特别是 DOM 对象(例如 ElementLocationDOMMatrix),Node.js 中由 C++ 类支持的对象(例如 process.envStream 的一些成员)和 Electron 中由 C++ 类支持的对象(例如 WebContentsBrowserWindowWebFrame)无法使用结构化克隆序列化。