欢迎来到Electron入门教程的第三期教程,这一节非常重要!进程间通信(IPC)是在Electron中构建功能丰富的桌面应用程序的关键部分。因为主进程和渲染进程在Electron的进程模型中有不同的职责,IPC是执行许多常见任务的唯一方式,比如从UI调用本地API或从本地菜单触发web内容的更改。下面就来详细介绍3种常见的通信方式。

✧ 渲染进程向主进程的单向通信

在Electron中,进程通过开发人员定义的“通道”与ipcMain模块和ipcRenderer模块进行通信。这些通道是任意的(您可以任意命名它们)和双向的(您可以为两个模块使用相同的通道名称)。要从渲染进程向主进程发送单向IPC消息,可以再预渲染脚本preload.js里使用ipcRenderer发送API发送消息,然后在main.js里用ipcMain.on接收。你通常使用这个模式从你的web内容中调用一个主进程API。我们将通过创建一个简单的应用程序来演示这种模式,该应用程序可以通过编程方式更改窗口的标题。

下面我们用代码来演示一下这个过程,下面是案例的所有代码:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>进程通信</title>
</head>
<body>
Title: <input id="title"/>
<button id="btn" type="button">Set</button>
<script src="./index.js"></script>
</body>
</html>

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

index.js

const setButton = document.getElementById('btn')
const titleInput = document.getElementById('title')
setButton.addEventListener('click', () => {
    const title = titleInput.value
    window.electronAPI.setTitle(title)
});

main.js

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

function createWindow () {
    const mainWindow = new BrowserWindow({
        webPreferences: {
            preload: path.join(__dirname, 'preload.js')
        }
    })

    ipcMain.on('set-title', (event, title) => {
        const webContents = event.sender
        const win = BrowserWindow.fromWebContents(webContents)
        win.setTitle(title)
    })

    mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
    createWindow()

    app.on('activate', function () {
        if (BrowserWindow.getAllWindows().length === 0) createWindow()
    })
})

app.on('window-all-closed', function () {
    if (process.platform !== 'darwin') app.quit()
})

运行效果如下(GIF有点慢,别介意):

electron和python双向通信 electron_electron和python双向通信


下面对代码的一些要点进行讲解:

1.在主进程中监听事件

在主进程中,我们使用ipcMainset-title通道上设置一个IPC监听器,这个set-title是我们在预渲染脚本preload.js里面定义的接口通道。

ipcMain.on('set-title', (event, title) => {
        const webContents = event.sender
        const win = BrowserWindow.fromWebContents(webContents)
        win.setTitle(title)
    })

每当消息通过set-title通道传入时,此函数将找到附加到消息发送者的BrowserWindow实例,并使用win.setTitle设置应用窗口的标题。

2.在预加载脚本里面通过定义接口通道
要向上面创建的侦听器发送消息,您可以使用ipcRenderer。发送API。默认情况下,渲染器进程没有Node.js或Electron模块访问。作为应用程序开发人员,您需要使用contextBridge 从预加载脚本中选择要公开哪些API。此时,您将能够在呈现过程中使用window.electronAPI.setTitle()函数。

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
    setTitle: (title) => ipcRenderer.send('set-title', title)
})

✧ 渲染进程与主进程的双向通信

双向IPC的一个常见应用是从渲染进程代码中调用主进程模块并等待结果。这可以通过使用ipcRenderer.invoke来实现,调用ipcMain.handle配对。在下面的例子中,我们将从渲染进程中打开一个选择本地文件对话框,并返回所选文件的路径。

下面是案例涉及的所有代码:

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>进程通信</title>
</head>
<body>
<button type="button" id="btn">Open a File</button>
File path: <strong id="filePath"></strong>
<script src='./index.js'></script>
</body>
</html>

index.js

const btn = document.getElementById('btn')
const filePathElement = document.getElementById('filePath')

btn.addEventListener('click', async () => {
  const filePath = await window.electronAPI.openFile()
  filePathElement.innerText = filePath
})

main.js

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

async function handleFileOpen() {
  const { canceled, filePaths } = await dialog.showOpenDialog()
  if (canceled) {
    return ""
  } else {
    return filePaths[0]
  }
}

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })
  mainWindow.loadFile('index.html')
}

app.whenReady().then(() => {
  ipcMain.handle('openFileDialog', handleFileOpen)
  createWindow()
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI',{
  openFile: () => ipcRenderer.invoke('openFileDialog')
})

运行效果演示:

electron和python双向通信 electron_前端_02


下面对代码的一些要点进行讲解:

1.在主进程定义事件处理函数,并监听ICP接口的调用

在主进程中,我们将创建一个调用dialog模块的showOpenDialog方法的函数handleFileOpen(),用于返回用户选择的文件路径的值。在应用准备好之后,里面调用ipcMain.handle()来监听渲染进程里的ipcRenderer.invoke('openFileDialog')里定义的openFileDialog。当index.js里面调用window.electronAPI.openFile()时,会触发openFileDialog,进而被主进程监听处理后,返回结果。

2. 调用通过预加载脚本定义接口
在预加载脚本中,我们公开了一个单行openFile函数,它调用并返回ipcRederer .invoke('openFileDialog')
在index.js代码片段中,我们监听对#btn按钮的点击,并调用window.electronAPI.openFile() 来激活本地的openFile对话框。然后在#filePath元素中显示选定的文件路径。

3. ipcRenderer.invoke的替代
ipcRenderer.invoke()有两种替代方式:
(1)ipcRenderer.send()

preload.js

const { ipcRenderer } = require('electron')

ipcRenderer.on('asynchronous-reply', (_event, arg) => {
  console.log(arg)
  // 会打印pong
})
ipcRenderer.send('asynchronous-message', 'ping')

main,js

ipcMain.on('asynchronous-message', (event, arg) => {
  console.log(arg)
  //会答应ping
  event.reply('asynchronous-reply', 'pong')
})

(1) ipcRenderer.sendSync()

preload.js

const { ipcRenderer } = require('electron')

const result = ipcRenderer.sendSync('synchronous-message', 'ping')
console.log(result)
// 会打印pong

main.js

const { ipcMain } = require('electron')
ipcMain.on('synchronous-message', (event, arg) => {
  console.log(arg)
  // 会打印ping
  event.returnValue = 'pong'
})

此代码的结构与调用模型非常相似,但出于性能原因,我们建议避免使用此API。它的同步特性意味着它将阻塞呈现程序进程,直到接收到应答。

✧ 主进程向渲染进程的单向通信

当从主进程向渲染进程发送消息时,您需要指定哪个渲染程序正在接收消息。消息需要通过主进程的WebContents实例发送到渲染进程。这个WebContents实例包含一个sent方法,可以像ipcReender .send那样使用它。为了演示这个通信模式,将构建一个由菜单栏控制的数字计数器。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>进程通信</title>
</head>
<body>
Current value: <strong id="counter">0</strong>
<script src='./index.js'></script>
</body>
</html>

preload.js

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
  handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})

index.js

const counter = document.getElementById('counter')

window.electronAPI.handleCounter((event, value) => {
    const oldValue = Number(counter.innerText)
    const newValue = oldValue + value
    counter.innerText = newValue
    event.sender.send('counter-value', newValue)
})

main.js

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

function createWindow () {
  const mainWindow = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
      {
        click: () => mainWindow.webContents.send('update-counter', 1),
        label: 'Increment',
      },
      {
        click: () => mainWindow.webContents.send('update-counter', -1),
        label: 'Decrement',
      }
      ]
    }

  ])

  Menu.setApplicationMenu(menu)
  mainWindow.loadFile('index.html')

  // Open the DevTools.
  mainWindow.webContents.openDevTools()
}

app.whenReady().then(() => {
  ipcMain.on('counter-value', (_event, value) => {
    console.log(value) // will print value to Node console
  })
  createWindow()
  
  app.on('activate', function () {
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

运行效果演示:

electron和python双向通信 electron_node.js_03


对部分代码讲解:

我们首先需要在主流程中使用Electron的Menu模块构建一个自定义菜单,从主进程向目标渲染器发送IPC消息。单击处理程序通过计数器通道向呈现程序进程发送消息(1或-1)。

const menu = Menu.buildFromTemplate([
    {
      label: app.name,
      submenu: [
        {
          click: () => mainWindow.webContents.send('update-counter', 1),
          label: 'Increment',
        },
        {
          click: () => mainWindow.webContents.send('update-counter', -1),
          label: 'Decrement',
        }
      ]
    }
  ])
  Menu.setApplicationMenu(menu)

就像前面渲染到主进程的例子一样,我们在预加载脚本preload.js中使用contextBridgeipcRederer模块向渲染进程公开IPC功能:

const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('electronAPI', {
    handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
})

✧ 渲染进程之间的通信

在Electron中,没有直接的方法在渲染进程之间使用ipcMainipRenderer模块发送消息,而且这种通信方式其实也非常少用。要做到这一点,你可以使用主进程作为渲染程序之间的消息代理。这将涉及到从一个渲染器向主进程发送消息,主进程将把消息转发给另一个渲染器,这里就不做演示了。