Browse Source

docs: add IPC doc (#32059)

* docs: add IPC doc

* fix: use "string" primitive

* use 'string' ipcrenderer

* use "number" primitive

* Update docs/tutorial/ipc.md

Co-authored-by: Jeremy Rose <[email protected]>

* Update docs/tutorial/ipc.md

Co-authored-by: Jeremy Rose <[email protected]>

* add code sample

Co-authored-by: Jeremy Rose <[email protected]>
Erick Zhao 3 years ago
parent
commit
e9a43be9be

+ 24 - 42
docs/api/ipc-main.md

@@ -1,3 +1,10 @@
+---
+title: "ipcMain"
+description: "Communicate asynchronously from the main process to renderer processes."
+slug: ipc-main
+hide_title: false
+---
+
 # ipcMain
 
 > Communicate asynchronously from the main process to renderer processes.
@@ -9,7 +16,9 @@ process, it handles asynchronous and synchronous messages sent from a renderer
 process (web page). Messages sent from a renderer will be emitted to this
 module.
 
-## Sending Messages
+For usage examples, check out the [IPC tutorial].
+
+## Sending messages
 
 It is also possible to send messages from the main process to the renderer
 process, see [webContents.send][web-contents-send] for more information.
@@ -21,36 +30,6 @@ process, see [webContents.send][web-contents-send] for more information.
   coming from frames that aren't the main frame (e.g. iframes) whereas
   `event.sender.send(...)` will always send to the main frame.
 
-An example of sending and handling messages between the render and main
-processes:
-
-```javascript
-// In main process.
-const { ipcMain } = require('electron')
-ipcMain.on('asynchronous-message', (event, arg) => {
-  console.log(arg) // prints "ping"
-  event.reply('asynchronous-reply', 'pong')
-})
-
-ipcMain.on('synchronous-message', (event, arg) => {
-  console.log(arg) // prints "ping"
-  event.returnValue = 'pong'
-})
-```
-
-```javascript
-// In renderer process (web page).
-// NB. Electron APIs are only accessible from preload, unless contextIsolation is disabled.
-// See https://www.electronjs.org/docs/tutorial/process-model#preload-scripts for more details.
-const { ipcRenderer } = require('electron')
-console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"
-
-ipcRenderer.on('asynchronous-reply', (event, arg) => {
-  console.log(arg) // prints "pong"
-})
-ipcRenderer.send('asynchronous-message', 'ping')
-```
-
 ## Methods
 
 The `ipcMain` module has the following method to listen for events:
@@ -59,7 +38,7 @@ The `ipcMain` module has the following method to listen for events:
 
 * `channel` string
 * `listener` Function
-  * `event` IpcMainEvent
+  * `event` [IpcMainEvent][ipc-main-event]
   * `...args` any[]
 
 Listens to `channel`, when a new message arrives `listener` would be called with
@@ -69,7 +48,7 @@ Listens to `channel`, when a new message arrives `listener` would be called with
 
 * `channel` string
 * `listener` Function
-  * `event` IpcMainEvent
+  * `event` [IpcMainEvent][ipc-main-event]
   * `...args` any[]
 
 Adds a one time `listener` function for the event. This `listener` is invoked
@@ -93,8 +72,8 @@ Removes listeners of the specified `channel`.
 ### `ipcMain.handle(channel, listener)`
 
 * `channel` string
-* `listener` Function<Promise\<void> | any>
-  * `event` IpcMainInvokeEvent
+* `listener` Function<Promise\<void&#62; | any&#62;
+  * `event` [IpcMainInvokeEvent][ipc-main-invoke-event]
   * `...args` any[]
 
 Adds a handler for an `invoke`able IPC. This handler will be called whenever a
@@ -104,14 +83,14 @@ If `listener` returns a Promise, the eventual result of the promise will be
 returned as a reply to the remote caller. Otherwise, the return value of the
 listener will be used as the value of the reply.
 
-```js
-// Main process
+```js title='Main Process'
 ipcMain.handle('my-invokable-ipc', async (event, ...args) => {
   const result = await somePromise(...args)
   return result
 })
+```
 
-// Renderer process
+```js title='Renderer Process'
 async () => {
   const result = await ipcRenderer.invoke('my-invokable-ipc', arg1, arg2)
   // ...
@@ -130,7 +109,7 @@ provided to the renderer process. Please refer to
 ### `ipcMain.handleOnce(channel, listener)`
 
 * `channel` string
-* `listener` Function<Promise\<void> | any>
+* `listener` Function<Promise\<void&#62; | any&#62;
   * `event` IpcMainInvokeEvent
   * `...args` any[]
 
@@ -146,13 +125,16 @@ Removes any handler for `channel`, if present.
 ## IpcMainEvent object
 
 The documentation for the `event` object passed to the `callback` can be found
-in the [`ipc-main-event`](structures/ipc-main-event.md) structure docs.
+in the [`ipc-main-event`][ipc-main-event] structure docs.
 
 ## IpcMainInvokeEvent object
 
 The documentation for the `event` object passed to `handle` callbacks can be
-found in the [`ipc-main-invoke-event`](structures/ipc-main-invoke-event.md)
+found in the [`ipc-main-invoke-event`][ipc-main-invoke-event]
 structure docs.
 
+[IPC tutorial]: ../tutorial/ipc.md
 [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
-[web-contents-send]: web-contents.md#contentssendchannel-args
+[web-contents-send]: ../api/web-contents.md#contentssendchannel-args
+[ipc-main-event]:../api/structures/ipc-main-event.md
+[ipc-main-invoke-event]:../api/structures/ipc-main-invoke-event.md

+ 16 - 9
docs/api/ipc-renderer.md

@@ -1,3 +1,10 @@
+---
+title: "ipcRenderer"
+description: "Communicate asynchronously from a renderer process to the main process."
+slug: ipc-renderer
+hide_title: false
+---
+
 # ipcRenderer
 
 > Communicate asynchronously from a renderer process to the main process.
@@ -9,7 +16,7 @@ methods so you can send synchronous and asynchronous messages from the render
 process (web page) to the main process. You can also receive replies from the
 main process.
 
-See [ipcMain](ipc-main.md) for code examples.
+See [IPC tutorial](../tutorial/ipc.md) for code examples.
 
 ## Methods
 
@@ -70,7 +77,7 @@ throw an exception.
 > them. Attempting to send such objects over IPC will result in an error.
 
 The main process handles it by listening for `channel` with the
-[`ipcMain`](ipc-main.md) module.
+[`ipcMain`](./ipc-main.md) module.
 
 If you need to transfer a [`MessagePort`][] to the main process, use [`ipcRenderer.postMessage`](#ipcrendererpostmessagechannel-message-transfer).
 
@@ -98,7 +105,7 @@ throw an exception.
 > them. Attempting to send such objects over IPC will result in an error.
 
 The main process should listen for `channel` with
-[`ipcMain.handle()`](ipc-main.md#ipcmainhandlechannel-listener).
+[`ipcMain.handle()`](./ipc-main.md#ipcmainhandlechannel-listener).
 
 For example:
 
@@ -124,11 +131,11 @@ If you do not need a response to the message, consider using [`ipcRenderer.send`
 * `channel` string
 * `...args` any[]
 
-Returns `any` - The value sent back by the [`ipcMain`](ipc-main.md) handler.
+Returns `any` - The value sent back by the [`ipcMain`](./ipc-main.md) handler.
 
 Send a message to the main process via `channel` and expect a result
 synchronously. Arguments will be serialized with the [Structured Clone
-Algorithm][SCA], just like [`window.postMessage`][], so prototype chains will not be
+Algorithm][SCA], just like [`window.postMessage`], so prototype chains will not be
 included. Sending Functions, Promises, Symbols, WeakMaps, or WeakSets will
 throw an exception.
 
@@ -140,13 +147,13 @@ throw an exception.
 > Electron's IPC to the main process, as the main process would have no way to decode
 > them. Attempting to send such objects over IPC will result in an error.
 
-The main process handles it by listening for `channel` with [`ipcMain`](ipc-main.md) module,
+The main process handles it by listening for `channel` with [`ipcMain`](./ipc-main.md) module,
 and replies by setting `event.returnValue`.
 
 > :warning: **WARNING**: Sending a synchronous message will block the whole
 > renderer process until the reply is received, so use this method only as a
 > last resort. It's much better to use the asynchronous version,
-> [`invoke()`](ipc-renderer.md#ipcrendererinvokechannel-args).
+> [`invoke()`](./ipc-renderer.md#ipcrendererinvokechannel-args).
 
 ### `ipcRenderer.postMessage(channel, message, [transfer])`
 
@@ -158,7 +165,7 @@ Send a message to the main process, optionally transferring ownership of zero
 or more [`MessagePort`][] objects.
 
 The transferred `MessagePort` objects will be available in the main process as
-[`MessagePortMain`](message-port-main.md) objects by accessing the `ports`
+[`MessagePortMain`](./message-port-main.md) objects by accessing the `ports`
 property of the emitted event.
 
 For example:
@@ -197,7 +204,7 @@ the host page instead of the main process.
 ## Event object
 
 The documentation for the `event` object passed to the `callback` can be found
-in the [`ipc-renderer-event`](structures/ipc-renderer-event.md) structure docs.
+in the [`ipc-renderer-event`](./structures/ipc-renderer-event.md) structure docs.
 
 [event-emitter]: https://nodejs.org/api/events.html#events_class_eventemitter
 [SCA]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

+ 0 - 0
docs/fiddles/communication/two-processes/.keep


+ 0 - 27
docs/fiddles/communication/two-processes/asynchronous-messages/index.html

@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="UTF-8">
-  </head>
-  <body>
-    <div>
-      <div>
-        <h1>Asynchronous messages</h1>
-        <i>Supports: Win, macOS, Linux <span>|</span> Process: Both</i>
-        <div>
-          <div>
-            <button id="async-msg">Ping</button>
-            <span id="async-reply"></span>
-          </div>
-          <p>Using <code>ipc</code> to send messages between processes asynchronously is the preferred method since it will return when finished without blocking other operations in the same process.</p>
-
-          <p>This example sends a "ping" from this process (renderer) to the main process. The main process then replies with "pong".</p>
-        </div>
-      </div>
-    </div>
-    <script>
-      // You can also require other files to run in this process
-      require('./renderer.js')
-    </script>
-  </body>
-</html>

+ 0 - 29
docs/fiddles/communication/two-processes/asynchronous-messages/main.js

@@ -1,29 +0,0 @@
-const { app, BrowserWindow, ipcMain } = require('electron')
-
-let mainWindow = null
-
-function createWindow () {
-  const windowOptions = {
-    width: 600,
-    height: 400,
-    title: 'Asynchronous messages',
-    webPreferences: {
-      nodeIntegration: true
-    }
-  }
-
-  mainWindow = new BrowserWindow(windowOptions)
-  mainWindow.loadFile('index.html')
-
-  mainWindow.on('closed', () => {
-    mainWindow = null
-  })
-}
-
-app.whenReady().then(() => {
-  createWindow()
-})
-
-ipcMain.on('asynchronous-message', (event, arg) => {
-  event.sender.send('asynchronous-reply', 'pong')
-})

+ 0 - 12
docs/fiddles/communication/two-processes/asynchronous-messages/renderer.js

@@ -1,12 +0,0 @@
-const { ipcRenderer } = require('electron')
-
-const asyncMsgBtn = document.getElementById('async-msg')
-
-asyncMsgBtn.addEventListener('click', () => {
-  ipcRenderer.send('asynchronous-message', 'ping')
-})
-
-ipcRenderer.on('asynchronous-reply', (event, arg) => {
-  const message = `Asynchronous message reply: ${arg}`
-  document.getElementById('async-reply').innerHTML = message
-})

+ 0 - 27
docs/fiddles/communication/two-processes/synchronous-messages/index.html

@@ -1,27 +0,0 @@
-<!DOCTYPE html>
-<html>
-  <head>
-    <meta charset="UTF-8">
-  </head>
-  <body>
-    <div>
-      <div>
-        <h1>Synchronous messages</h1>
-        <i>Supports: Win, macOS, Linux <span>|</span> Process: Both</i>
-        <div>
-          <div>
-            <button id="sync-msg">Ping</button>
-            <span id="sync-reply"></span>
-          </div>
-          <p>You can use the <code>ipc</code> module to send synchronous messages between processes as well, but note that the synchronous nature of this method means that it <b>will block</b> other operations while completing its task.</p>
-          
-          <p>This example sends a synchronous message, "ping", from this process (renderer) to the main process. The main process then replies with "pong".</p>
-        </div>
-      </div>
-    </div>
-    <script>
-      // You can also require other files to run in this process
-      require('./renderer.js')
-    </script>
-  </body>
-</html> 

+ 0 - 29
docs/fiddles/communication/two-processes/synchronous-messages/main.js

@@ -1,29 +0,0 @@
-const { app, BrowserWindow, ipcMain } = require('electron')
-
-let mainWindow = null
-
-function createWindow () {
-  const windowOptions = {
-    width: 600,
-    height: 400,
-    title: 'Synchronous Messages',
-    webPreferences: {
-      nodeIntegration: true
-    }
-  }
-
-  mainWindow = new BrowserWindow(windowOptions)
-  mainWindow.loadFile('index.html')
-
-  mainWindow.on('closed', () => {
-    mainWindow = null
-  })
-}
-
-app.whenReady().then(() => {
-  createWindow()
-})
-
-ipcMain.on('synchronous-message', (event, arg) => {
-    event.returnValue = 'pong'
-})

+ 0 - 9
docs/fiddles/communication/two-processes/synchronous-messages/renderer.js

@@ -1,9 +0,0 @@
-const { ipcRenderer } = require('electron')
-
-const syncMsgBtn = document.getElementById('sync-msg')
-
-syncMsgBtn.addEventListener('click', () => {
-    const reply = ipcRenderer.sendSync('synchronous-message', 'ping')
-    const message = `Synchronous message reply: ${reply}`
-    document.getElementById('sync-reply').innerHTML = message
-})

+ 14 - 0
docs/fiddles/ipc/pattern-1/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Hello World!</title>
+  </head>
+  <body>
+    Title: <input id="title"/>
+    <button id="btn" type="button">Set</button>
+    <script src="./renderer.js"></script>
+  </body>
+</html>

+ 30 - 0
docs/fiddles/ipc/pattern-1/main.js

@@ -0,0 +1,30 @@
+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()
+})

+ 5 - 0
docs/fiddles/ipc/pattern-1/preload.js

@@ -0,0 +1,5 @@
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+    setTitle: (title) => ipcRenderer.send('set-title', title)
+})

+ 6 - 0
docs/fiddles/ipc/pattern-1/renderer.js

@@ -0,0 +1,6 @@
+const setButton = document.getElementById('btn')
+const titleInput = document.getElementById('title')
+setButton.addEventListener('click', () => {
+    const title = titleInput.value
+    window.electronAPI.setTitle(title)
+});

+ 14 - 0
docs/fiddles/ipc/pattern-2/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Dialog</title>
+  </head>
+  <body>
+    <button type="button" id="btn">Open a File</button>
+    File path: <strong id="filePath"></strong>
+    <script src='./renderer.js'></script>
+  </body>
+</html>

+ 32 - 0
docs/fiddles/ipc/pattern-2/main.js

@@ -0,0 +1,32 @@
+const {app, BrowserWindow, ipcMain} = 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('dialog:openFile', handleFileOpen)
+  createWindow()
+  app.on('activate', function () {
+    if (BrowserWindow.getAllWindows().length === 0) createWindow()
+  })
+})
+
+app.on('window-all-closed', function () {
+  if (process.platform !== 'darwin') app.quit()
+})

+ 5 - 0
docs/fiddles/ipc/pattern-2/preload.js

@@ -0,0 +1,5 @@
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI',{
+  openFile: () => ipcRenderer.invoke('dialog:openFile')
+})

+ 7 - 0
docs/fiddles/ipc/pattern-2/renderer.js

@@ -0,0 +1,7 @@
+const btn = document.getElementById('btn')
+const filePathElement = document.getElementById('filePath')
+
+btn.addEventListener('click', async () => {
+  const filePath = await window.electronAPI.openFile()
+  filePathElement.innerText = filePath
+})

+ 13 - 0
docs/fiddles/ipc/pattern-3/index.html

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

+ 48 - 0
docs/fiddles/ipc/pattern-3/main.js

@@ -0,0 +1,48 @@
+const {app, BrowserWindow, Menu} = 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()
+})

+ 5 - 0
docs/fiddles/ipc/pattern-3/preload.js

@@ -0,0 +1,5 @@
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+  handleCounter: (callback) => ipcRenderer.on('update-counter', callback)
+})

+ 8 - 0
docs/fiddles/ipc/pattern-3/renderer.js

@@ -0,0 +1,8 @@
+const counter = document.getElementById('counter')
+
+window.electronAPI.handleCounter((event, value) => {
+    const oldValue = Number(counter.innerText)
+    const newValue = oldValue + value
+    counter.innerText = newValue
+    event.reply('counter-value', newValue)
+})

+ 571 - 0
docs/tutorial/ipc.md

@@ -0,0 +1,571 @@
+---
+title: Inter-Process Communication
+description: Use the ipcMain and ipcRenderer modules to communicate between Electron processes
+slug: ipc
+hide_title: false
+---
+
+# Inter-Process Communication
+
+Inter-process communication (IPC) is a key part of building feature-rich desktop applications
+in Electron. Because the main and renderer processes have different responsibilities in
+Electron's process model, IPC is the only way to perform many common tasks, such as calling a
+native API from your UI or triggering changes in your web contents from native menus.
+
+## IPC channels
+
+In Electron, processes communicate by passing messages through developer-defined "channels"
+with the [`ipcMain`] and [`ipcRenderer`] modules. These channels are
+**arbitrary** (you can name them anything you want) and **bidirectional** (you can use the
+same channel name for both modules).
+
+In this guide, we'll be going over some fundamental IPC patterns with concrete examples that
+you can use as a reference for your app code.
+
+## Understanding context-isolated processes
+
+Before proceeding to implementation details, you should be familiar with the idea of using a
+[preload script] to import Node.js and Electron modules in a context-isolated renderer process.
+
+* For a full overview of Electron's process model, you can read the [process model docs].
+* For a primer into exposing APIs from your preload script using the `contextBridge` module, check
+out the [context isolation tutorial].
+
+## Pattern 1: Renderer to main (one-way)
+
+To fire a one-way IPC message from a renderer process to the main process, you can use the
+[`ipcRenderer.send`] API to send a message that is then received by the [`ipcMain.on`] API.
+
+You usually use this pattern to call a main process API from your web contents. We'll demonstrate
+this pattern by creating a simple app that can programmatically change its window title.
+
+For this demo, you'll need to add code to your main process, your renderer process, and a preload
+script. The full code is below, but we'll be explaining each file individually in the following
+sections.
+
+```fiddle docs/fiddles/ipc/pattern-1
+```
+
+### 1. Listen for events with `ipcMain.on`
+
+In the main process, set an IPC listener on the `set-title` channel with the `ipcMain.on` API:
+
+```javascript {6-10,22} title='main.js (Main Process)'
+const {app, BrowserWindow, ipcMain} = require('electron')
+const path = require('path')
+
+//...
+
+function handleSetTitle (event, title) {
+  const webContents = event.sender
+  const win = BrowserWindow.fromWebContents(webContents)
+  win.setTitle(title)
+}
+
+function createWindow () {
+  const mainWindow = new BrowserWindow({
+    webPreferences: {
+      preload: path.join(__dirname, 'preload.js')
+    }
+  })
+  mainWindow.loadFile('index.html')
+}
+
+app.whenReady().then(() => {
+  ipcMain.on('set-title', handleSetTitle)
+  createWindow()
+}
+//...
+```
+
+The above `handleSetTitle` callback has two parameters: an [IpcMainEvent] structure and a
+`title` string. Whenever a message comes through the `set-title` channel, this function will
+find the BrowserWindow instance attached to the message sender and use the `win.setTitle`
+API on it.
+
+:::info
+Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
+:::
+
+### 2. Expose `ipcRenderer.send` via preload
+
+To send messages to the listener created above, you can use the `ipcRenderer.send` API.
+By default, the renderer process has no Node.js or Electron module access. As an app developer,
+you need to choose which APIs to expose from your preload script using the `contextBridge` API.
+
+In your preload script, add the following code, which will expose a global `window.electronAPI`
+variable to your renderer process.
+
+```javascript title='preload.js (Preload Script)'
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+    setTitle: (title) => ipcRenderer.send('set-title', title)
+})
+```
+
+At this point, you'll be able to use the `window.electronAPI.setTitle()` function in the renderer
+process.
+
+:::caution Security warning
+We don't directly expose the whole `ipcRenderer.send` API for [security reasons]. Make sure to
+limit the renderer's access to Electron APIs as much as possible.
+:::
+
+### 3. Build the renderer process UI
+
+In our BrowserWindow's loaded HTML file, add a basic user interface consisting of a text input
+and a button:
+
+```html {11-12} title='index.html'
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Hello World!</title>
+  </head>
+  <body>
+    Title: <input id="title"/>
+    <button id="btn" type="button">Set</button>
+    <script src="./renderer.js"></script>
+  </body>
+</html>
+```
+
+To make these elements interactive, we'll be adding a few lines of code in the imported
+`renderer.js` file that leverages the `window.electronAPI` functionality exposed from the preload
+script:
+
+```javascript title='renderer.js (Renderer Process)'
+const setButton = document.getElementById('btn')
+const titleInput = document.getElementById('title')
+setButton.addEventListener('click', () => {
+    const title = titleInput.value
+    window.electronAPI.setTitle(title)
+});
+```
+
+At this point, your demo should be fully functional. Try using the input field and see what happens
+to your BrowserWindow title!
+
+## Pattern 2: Renderer to main (two-way)
+
+A common application for two-way IPC is calling a main process module from your renderer process
+code and waiting for a result. This can be done by using [`ipcRenderer.invoke`] paired with
+[`ipcMain.handle`].
+
+In the following example, we'll be opening a native file dialog from the renderer process and
+returning the selected file's path.
+
+For this demo, you'll need to add code to your main process, your renderer process, and a preload
+script. The full code is below, but we'll be explaining each file individually in the following
+sections.
+
+```fiddle docs/fiddles/ipc/pattern-2
+```
+
+### 1. Listen for events with `ipcMain.handle`
+
+In the main process, we'll be creating a `handleFileOpen()` function that calls
+`dialog.showOpenDialog` and returns the value of the file path selected by the user. This function
+is used as a callback whenever an `ipcRender.invoke` message is sent through the `dialog:openFile`
+channel from the renderer process. The return value is then returned as a Promise to the original
+`invoke` call.
+
+:::caution A word on error handling
+Errors thrown through `handle` in the main process are not transparent as they
+are serialized and only the `message` property from the original error is
+provided to the renderer process. Please refer to
+[#24427](https://github.com/electron/electron/issues/24427) for details.
+:::
+
+```javascript {6-13,25} title='main.js (Main Process)'
+const { BrowserWindow, dialog, ipcMain } = 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(() => {
+  ipcMain.handle('dialog:openFile', handleFileOpen)
+  createWindow()
+})
+//...
+```
+
+:::tip on channel names
+The `dialog:` prefix on the IPC channel name has no effect on the code. It only serves
+as a namespace that helps with code readability.
+:::
+
+:::info
+Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
+:::
+
+### 2. Expose `ipcRenderer.invoke` via preload
+
+In the preload script, we expose a one-line `openFile` function that calls and returns the value of
+`ipcRenderer.invoke('dialog:openFile')`. We'll be using this API in the next step to call the
+native dialog from our renderer's user interface.
+
+```javascript title='preload.js (Preload Script)'
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+  openFile: () => ipcRenderer.invoke('dialog:openFile')
+})
+```
+
+:::caution Security warning
+We don't directly expose the whole `ipcRenderer.invoke` API for [security reasons]. Make sure to
+limit the renderer's access to Electron APIs as much as possible.
+:::
+
+### 3. Build the renderer process UI
+
+Finally, let's build the HTML file that we load into our BrowserWindow.
+
+```html {10-11} title='index.html'
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Dialog</title>
+  </head>
+  <body>
+    <button type="button" id="btn">Open a File</button>
+    File path: <strong id="filePath"></strong>
+    <script src='./renderer.js'></script>
+  </body>
+</html>
+```
+
+The UI consists of a single `#btn` button element that will be used to trigger our preload API, and
+a `#filePath` element that will be used to display the path of the selected file. Making these
+pieces work will take a few lines of code in the renderer process script:
+
+```javascript title='renderer.js (Renderer Process)'
+const btn = document.getElementById('btn')
+const filePathElement = document.getElementById('filePath')
+
+btn.addEventListener('click', async () => {
+  const filePath = await window.electronAPI.openFile()
+  filePathElement.innerText = filePath
+})
+```
+
+In the above snippet, we listen for clicks on the `#btn` button, and call our
+`window.electronAPI.openFile()` API to activate the native Open File dialog. We then display the
+selected file path in the `#filePath` element.
+
+### Note: legacy approaches
+
+The `ipcRenderer.invoke` API was added in Electron 7 as a developer-friendly way to tackle two-way
+IPC from the renderer process. However, there exist a couple alternative approaches to this IPC
+pattern.
+
+:::warning Avoid legacy approaches if possible
+We recommend using `ipcRenderer.invoke` whenever possible. The following two-way renderer-to-main
+patterns are documented for historical purposes.
+:::
+
+:::info
+For the following examples, we're calling `ipcRenderer` directly from the preload script to keep
+the code samples small.
+:::
+
+#### Using `ipcRenderer.send`
+
+The `ipcRenderer.send` API that we used for single-way communication can also be leveraged to
+perform two-way communication. This was the recommended way for asynchronous two-way communication
+via IPC prior to Electron 7.
+
+```javascript title='preload.js (Preload Script)'
+// You can also put expose this code to the renderer
+// process with the `contextBridge` API
+const { ipcRenderer } = require('electron')
+
+ipcRenderer.on('asynchronous-reply', (_event, arg) => {
+  console.log(arg) // prints "pong" in the DevTools console
+})
+ipcRenderer.send('asynchronous-message', 'ping')
+```
+
+```javascript title='main.js (Main Process)'
+ipcMain.on('asynchronous-message', (event, arg) => {
+  console.log(arg) // prints "ping" in the Node console
+  // works like `send`, but returning a message back
+  // to the renderer that sent the original message
+  event.reply('asynchronous-reply', 'pong')
+})
+```
+
+There are a couple downsides to this approach:
+
+* You need to set up a second `ipcRenderer.on` listener to handle the response in the renderer
+process. With `invoke`, you get the response value returned as a Promise to the original API call.
+* There's no obvious way to pair the `asynchronous-reply` message to the original
+`asynchronous-message` one. If you have very frequent messages going back and forth through these
+channels, you would need to add additional app code to track each call and response individually.
+
+#### Using `ipcRenderer.sendSync`
+
+The `ipcRenderer.sendSync` API sends a message to the main process and waits _synchronously_ for a
+response.
+
+```javascript title='main.js (Main Process)'
+const { ipcMain } = require('electron')
+ipcMain.on('synchronous-message', (event, arg) => {
+  console.log(arg) // prints "ping" in the Node console
+  event.returnValue = 'pong'
+})
+```
+
+```javascript title='preload.js (Preload Script)'
+// You can also put expose this code to the renderer
+// process with the `contextBridge` API
+const { ipcRenderer } = require('electron')
+
+const result = ipcRenderer.sendSync('synchronous-message', 'ping')
+console.log(result) // prints "pong" in the DevTools console
+```
+
+The structure of this code is very similar to the `invoke` model, but we recommend
+**avoiding this API** for performance reasons. Its synchronous nature means that it'll block the
+renderer process until a reply is received.
+
+## Pattern 3: Main to renderer
+
+When sending a message from the main process to a renderer process, you need to specify which
+renderer is receiving the message. Messages need to be sent to a renderer process
+via its [`WebContents`] instance. This WebContents instance contains a [`send`][webcontents-send] method
+that can be used in the same way as `ipcRenderer.send`.
+
+To demonstrate this pattern, we'll be building a number counter controlled by the native operating
+system menu.
+
+For this demo, you'll need to add code to your main process, your renderer process, and a preload
+script. The full code is below, but we'll be explaining each file individually in the following
+sections.
+
+```fiddle docs/fiddles/ipc/pattern-3
+```
+
+### 1. Send messages with the `webContents` module
+
+For this demo, we'll need to first build a custom menu in the main process using Electron's `Menu`
+module that uses the `webContents.send` API to send an IPC message from the main process to the
+target renderer.
+
+```javascript {11-26} title='main.js (Main Process)'
+const {app, BrowserWindow, Menu} = 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')
+}
+//...
+
+```
+
+For the purposes of the tutorial, it's important to note that the `click` handler
+sends a message (either `1` or `-1`) to the renderer process through the `counter` channel.
+
+```javascript
+click: () => mainWindow.webContents.send('update-counter', -1)
+```
+
+:::info
+Make sure you're loading the `index.html` and `preload.js` entry points for the following steps!
+:::
+
+### 2. Expose `ipcRenderer.on` via preload
+
+Like in the previous renderer-to-main example, we use the `contextBridge` and `ipcRenderer`
+modules in the preload script to expose IPC functionality to the renderer process:
+
+```javascript title='preload.js (Preload Script)'
+const { contextBridge, ipcRenderer } = require('electron')
+
+contextBridge.exposeInMainWorld('electronAPI', {
+    onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
+})
+```
+
+After loading the preload script, your renderer process should have access to the
+`window.electronAPI.onUpdateCounter()` listener function.
+
+:::caution Security warning
+We don't directly expose the whole `ipcRenderer.on` API for [security reasons]. Make sure to
+limit the renderer's access to Electron APIs as much as possible.
+:::
+
+:::info
+In the case of this minimal example, you can call `ipcRenderer.on` directly in the preload script
+rather than exposing it over the context bridge.
+
+```javascript title='preload.js (Preload Script)'
+const { ipcRenderer } = require('electron')
+
+window.addEventListener('DOMContentLoaded', () => {
+    const counter = document.getElementById('counter')
+    ipcRenderer.on('update-counter', (_event, value) => {
+        const oldValue = Number(counter.innerText)
+        const newValue = oldValue + value
+        counter.innerText = newValue
+    })
+})
+```
+
+However, this approach has limited flexibility compared to exposing your preload APIs
+over the context bridge, since your listener can't directly interact with your renderer code.
+:::
+
+### 3. Build the renderer process UI
+
+To tie it all together, we'll create an interface in the loaded HTML file that contains a
+`#counter` element that we'll use to display the values:
+
+```html {10} title='index.html'
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="UTF-8">
+    <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
+    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
+    <title>Menu Counter</title>
+  </head>
+  <body>
+    Current value: <strong id="counter">0</strong>
+    <script src="./renderer.js"></script>
+  </body>
+</html>
+```
+
+Finally, to make the values update in the HTML document, we'll add a few lines of DOM manipulation
+so that the value of the `#counter` element is updated whenever we fire an `update-counter` event.
+
+```javascript title='renderer.js (Renderer Process)'
+const counter = document.getElementById('counter')
+
+window.electronAPI.onUpdateCounter((_event, value) => {
+    const oldValue = Number(counter.innerText)
+    const newValue = oldValue + value
+    counter.innerText = newValue
+})
+```
+
+In the above code, we're passing in a callback to the `window.electronAPI.onUpdateCounter` function
+exposed from our preload script. The second `value` parameter corresponds to the `1` or `-1` we
+were passing in from the `webContents.send` call from the native menu.
+
+### Optional: returning a reply
+
+There's no equivalent for `ipcRenderer.invoke` for main-to-renderer IPC. Instead, you can
+send a reply back to the main process from within the `ipcRenderer.on` callback.
+
+We can demonstrate this with slight modifications to the code from the previous example. In the
+renderer process, use the `event` parameter to send a reply back to the main process through the
+`counter-value` channel.
+
+```javascript title='renderer.js (Renderer Process)'
+const counter = document.getElementById('counter')
+
+window.electronAPI.onUpdateCounter((event, value) => {
+  const oldValue = Number(counter.innerText)
+  const newValue = oldValue + value
+  counter.innerText = newValue
+  event.reply('counter-value', newValue)
+})
+```
+
+In the main process, listen for `counter-value` events and handle them appropriately.
+
+```javascript title='main.js (Main Process)'
+//...
+ipcMain.on('counter-value', (_event, value) => {
+  console.log(value) // will print value to Node console
+})
+//...
+```
+
+## Pattern 4: Renderer to renderer
+
+There's no direct way to send messages between renderer processes in Electron using the `ipcMain`
+and `ipcRenderer` modules. To achieve this, you have two options:
+
+* Use the main process as a message broker between renderers. This would involve sending a message
+from one renderer to the main process, which would forward the message to the other renderer.
+* Pass a [MessagePort] from the main process to both renderers. This will allow direct communication
+between renderers after the initial setup.
+
+## Object serialization
+
+Electron's IPC implementation uses the HTML standard
+[Structured Clone Algorithm][sca] to serialize objects passed between processes, meaning that
+only certain types of objects can be passed through IPC channels.
+
+In particular, DOM objects (e.g. `Element`, `Location` and `DOMMatrix`), Node.js objects
+backed by C++ classes (e.g. `process.env`, some members of `Stream`), and Electron objects
+backed by C++ classes (e.g. `WebContents`, `BrowserWindow` and `WebFrame`) are not serializable
+with Structured Clone.
+
+[context isolation tutorial]: context-isolation.md
+[security reasons]: ./context-isolation.md#security-considerations
+[`ipcMain`]: ../api/ipc-main.md
+[`ipcMain.handle`]: ../api/ipc-main.md#ipcmainhandlechannel-listener
+[`ipcMain.on`]: ../api/ipc-main.md
+[IpcMainEvent]: ../api/structures/ipc-main-event.md
+[`ipcRenderer`]: ../api/ipc-renderer.md
+[`ipcRenderer.invoke`]: ../api/ipc-renderer.md#ipcrendererinvokechannel-args
+[`ipcRenderer.send`]: ../api/ipc-renderer.md
+[MessagePort]: ./message-ports.md
+[preload script]: process-model.md#preload-scripts
+[process model docs]: process-model.md
+[sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
+[`WebContents`]: ../api/web-contents.md
+[webcontents-send]: ../api/web-contents.md#contentssendchannel-args