Warning: Undefined variable $position in /home/pystyles/pystyle.info/public_html/wp/wp-content/themes/lionblog/functions.php on line 4897

Electron – チュートリアルその6 プロセス間通信の方法

Electron – チュートリアルその6 プロセス間通信の方法

概要

Electron において、プロセス間通信 (IPC) を使用して、メインプロセスとレンダラープロセスでデータをやり取りする方法について解説します。

本記事で紹介するコード全体は以下を参照ください。

electron-examples/06_electron-ipc-renderer-to-main-async at main

レンダラープロセス起点でやり取りする (非同期通信)

非同期でレンダラープロセスからメインプロセスにデータを送り、メインプロセスで受信し、レンダラープロセスに結果を返すサンプルについて紹介します。Electron では、複数のプロセス間が混在しても区別できるように、プロセス間通信を行う API を使用する際にチャンネル名を指定します。

レンダラープロセス起点でやり取りする

1. プリロードスクリプトを作成する

  1. ipcRenderer.send() を使用して、チャンネル sendAsync"ping" というデータをレンダラープロセスからメインプロセスに対して送信する関数 send() を作成します。
  2. ipcRenderer.on() を使用して、チャンネル sendAsync でデータを受信した場合のコールバック関数を登録する関数 recieved() を作成します。
  3. contextBridge.exposeInMainWorld() を使用して、これらの関数をレンダラープロセスから Electron プロパティを通してアクセスできるようにします。

preload.ts

import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";

contextBridge.exposeInMainWorld("Electron", {
  // Renderer -> Main
  send: (...args: any[]) => ipcRenderer.send("sendAsync", ...args),
  // Main -> Renderer
  recieved: (listener: (...args: any[]) => void) => {
    ipcRenderer.on("sendAsync", (event: IpcRendererEvent, ...args: any[]) =>
      listener(...args)
    );
  },
});

型アノテーションで少し見づらいですが、それを削除すると以下のようになります。

contextBridge.exposeInMainWorld("Electron", {
  // Renderer -> Main
  send: (...args) => ipcRenderer.send("sendAsync", ...args),
  // Main -> Renderer
  recieved: (listener) => ipcRenderer.on("sendAsync", (even, ...args) => listener(...args));,
});

2. メインプロセスでデータを受信した場合の処理を作成する

ipcMain.on() でチャンネル sendAsync のデータを受信した場合のコールバック関数を登録します。 WebContents.send() を使用して、チャンネル sendAsync"pong" というデータをメインプロセスからレンダラープロセスに対して送信します。 送信元の WebContents オブジェクトは、event.sender で参照できます。

main.ts

import { app, BrowserWindow, ipcMain, IpcMainEvent } from "electron";
import * as path from "path";

const createWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });
  win.webContents.openDevTools();
  win.loadFile("index.html");
};

ipcMain.on("sendAsync", (event: IpcMainEvent, ...args: any[]) => {
  console.log(`[Main] ${args} received.`);

  // Main -> Renderer
  event.sender.send("sendAsync", "pong");
});

app.whenReady().then(createWindow);

3. レンダラープロセスで応答を受信した場合の処理を作成する

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <button onclick="Electron.send('ping')">Send To Main</button>
    <p id="recieve"></p>
  </body>
</html>
<script>
  Electron.recieved((...args) => {
    console.log(`[Renderer] ${args} received.`);
    document.getElementById("recieve").textContent = JSON.stringify(args);
  });
</script>

データを受け取った場合に実行する関数を作成し、プリロードスクリプト内で定義した Electron.recieved() で登録します。

レンダラープロセス起点でやり取りする (同期通信)

同期でレンダラープロセスからメインプロセスにデータを送り、メインプロセスで受信し、レンダラープロセスに結果を返すサンプルについて紹介します。

レンダラープロセス起点でやり取りする

1. プリロードスクリプトを作成する

  1. ipcRenderer.sendSync() を使用して、チャンネル sendSync"ping" というデータをレンダラープロセスからメインプロセスに対して送信する関数 send() を作成します。この関数は、同期処理なので、メインプロセス側からの応答を待ち、それを返します。
  2. contextBridge.exposeInMainWorld() を使用して、この関数をレンダラープロセスから Electron プロパティを通してアクセスできるようにします。

preload.ts

import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("Electron", {
  send: () => ipcRenderer.sendSync("sendSync", "ping"),
});

2. メインプロセスでデータを受信した場合の処理を作成する

ipcMain.on() でチャンネル sendSync のデータを受信した場合のコールバック関数を登録します。 返り値は、event.returnValue に設定します。

main.ts

import { app, BrowserWindow, ipcMain, IpcMainEvent } from "electron";
import * as path from "path";

const createWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });
  win.webContents.openDevTools();
  win.loadFile("index.html");
};

ipcMain.on("sendSync", (event: IpcMainEvent, ...args: any[]) => {
  console.log(`[Main] ${args} received.`);

  // Main -> Renderer
  event.returnValue = "pong";
});

app.whenReady().then(createWindow);

3. レンダラープロセスで応答を受信した場合の処理を作成する

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <button onclick="send()">Send To Main</button>
    <p id="recieve"></p>
  </body>
</html>
<script>
  function send() {
    const ret = Electron.send();
    document.getElementById("recieve").textContent = JSON.stringify(ret);
    console.log(`[Renderer] ${ret} received.`);
  }
</script>

Electron.send() でデータを送り、応答を受け取り、HTML 上で表示します。

レンダラープロセス起点でやり取りする (非同期通信/invoke)

非同期通信で、レンダラープロセスからメインプロセスにデータを送り、メインプロセスでそれを受信して、レンダラープロセスに結果を返すサンプルの別の書き方を紹介します。

レンダラープロセス起点でやり取りする

1. プリロードスクリプトを作成する

  1. ipcRenderer.invoke() を使用して、チャンネル invoke"ping" というデータをレンダラープロセスからメインプロセスに対して送信する関数 send() を作成します。この関数は、メインレンダラーと通信し、応答を受け取る Promise オブジェクトを返します。
  2. contextBridge.exposeInMainWorld() を使用して、この関数をレンダラープロセスから Electron プロパティを通してアクセスできるようにします。

preload.ts

import { contextBridge, ipcRenderer } from "electron";

contextBridge.exposeInMainWorld("Electron", {
  send: (...args: any[]) => ipcRenderer.invoke("invoke", ...args),
});

2. メインプロセスでデータを受信した場合の処理を作成する

ipcMain.handle() で、チャンネル invoke のデータを受信した場合のコールバック関数を登録します。 このコールバック関数の返り値は、Promise オブジェクトの履行ハンドラーに渡す値になります。

main.ts

import { app, BrowserWindow, ipcMain, IpcMainInvokeEvent } from "electron";
import * as path from "path";

const createWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });
  win.webContents.openDevTools();
  win.loadFile("index.html");
};

ipcMain.handle("invoke", (event: IpcMainInvokeEvent, ...args: any[]) => {
  console.log(`[Main] ${args} received.`);

  // Main -> Renderer
  return "pong";
});

app.whenReady().then(createWindow);
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <button onclick="clicked()">Send To Main</button>
    <p id="recieve"></p>
  </body>
</html>
<script>
  function clicked() {
    Electron.send().then((args) => {
      document.getElementById("recieve").textContent = JSON.stringify(args);
      console.log(`[Renderer] ${args} received.`);
    });
  }
</script>

3. レンダラープロセスで応答を受信した場合の処理を作成する

データを受け取った場合に実行する関数を定義し、Electron.send() でデータ送信処理を行う Promise オブジェクトを作成します。Promise.then() の履行ハンドラーで、メインレンダラーから値を受け取った際の処理を記述します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <button onclick="clicked()">Send To Main</button>
    <p id="recieve"></p>
  </body>
</html>
<script>
  function clicked() {
    Electron.send().then((...args) => {
      console.log(`[Renderer] ${args} received.`);

      document.getElementById("recieve").textContent = JSON.stringify(args);
    });
  }
</script>

これは、async/await で以下のように書き直せます。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <button onclick="clicked()">Send To Main</button>
    <p id="recieve"></p>
  </body>
</html>
<script>
  async function clicked() {
    const args = await Electron.send();
    document.getElementById("recieve").textContent = JSON.stringify(args);
    console.log(`[Renderer] ${args} received.`);
  }
</script>

メインプロセス起点でやり取りする (非同期通信)

非同期通信で、メインプロセスからレンダラープロセスにデータを送り、レンダラープロセスでそれを受信して、メインプロセスに結果を返すサンプルについて紹介します。

メインプロセス起点でやり取りする

1. プリロードスクリプトを作成する

  1. ipcRenderer.on() を使用して、チャンネル sendAsync でデータを受信した場合のコールバック関数を登録する関数 recieved() を作成します。
  2. ipcRenderer.send() を使用して、チャンネル sendAsync"pong" というデータをレンダラープロセスからメインプロセスに対して送信する関数 send() を作成します。
  3. contextBridge.exposeInMainWorld() を使用して、これらの関数をレンダラープロセスから Electron プロパティを通してアクセスできるようにします。

preload.ts

import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron";

contextBridge.exposeInMainWorld("Electron", {
  // Renderer -> Main
  send: (...args: any[]) => ipcRenderer.send("sendAsync", ...args),
  // Main -> Renderer
  recieved: (listener: (...args: any[]) => void) => {
    ipcRenderer.on("sendAsync", (event: IpcRendererEvent, ...args: any[]) =>
      listener(...args)
    );
  },
});

2. メインプロセスでデータを送信する処理を作成する

  1. win.webContents.send() でチャンネル sendAsync"ping" というデータをメインプロセスからレンダラープロセスに対して送信します。送信は、ページの表示が完了してから送るため、"did-finish-load" イベント後に実行します。
  2. ipcMain.on() でチャンネル sendAsync のデータを受信した場合のコールバック関数を登録します。

main.ts

import { app, BrowserWindow, ipcMain, IpcMainEvent } from "electron";
import * as path from "path";

const createWindow = () => {
  const win = new BrowserWindow({
    webPreferences: {
      preload: path.join(__dirname, "preload.js"),
    },
  });
  win.webContents.openDevTools();
  win.loadFile("index.html");

  win.webContents.on("did-finish-load", () => {
    // Main -> Renderer
    console.log(`[Main] send "ping".`);
    win.webContents.send("sendAsync", "ping");
  });
};

ipcMain.on("sendAsync", (event: IpcMainEvent, ...args: any[]) => {
  console.log(`[Main] ${args} received.`);
});

app.whenReady().then(createWindow);

3. レンダラープロセスでデータを受信した場合の処理を作成する

データを受け取った場合に実行する関数を定義し、プリロードスクリプトで定義した Electron.recieved() で登録します。 データを受信したら、応答をメインプロセスに返します。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <p id="recieve"></p>
  </body>
</html>
<script>
  Electron.recieved((...args) => {
    console.log(`[Renderer] ${args} received.`);
    document.getElementById("recieve").textContent = JSON.stringify(args);

    // Renderer -> Main
    Electron.send("pong");
  });
</script>