Electron ipc (プロセス間通信) ipcMain, ipcRenderer 使い方

remoteも使えなくなっていて、このipcがElectronを始めたら最初に分からなくなるところでした。 やっと理解できたので、プロセス間の図解とメソッドの組合わせの早わかり表も使って ipcMain とipcRendererの使い方をできるだけ分かりやすく書きました。

『Electron WindowsでインストールからHello World, ビルドまで』のコードに追加しています。 

2つのプロセス

Electronには2つのプロセスがあります。 ひとつはメインプロセス。 ふたつめはレンダラープロセス。(ファイル名は自由につけれますが、以下、メインプロセス用はmain.js、レンダラープロセス用はrenderer.js, プレローダはpreload.jsとします。)

メインプロセス

  • ベースとなるプロセス。
  • main.js*内でelectronから生成するapp介してアプリケーションのライフサイクルを管理。(起動時、クローズ時挙動等)
  • 通常1個しかない。
  • 各ウィンドウの管理。
  • Node.JsやElectronに使用できる。
  • ipcMainを使用。

レンダラープロセス

  • ウィンドウ、画面(window, documentオブジェクト)の操作用プロセス。
  • 私が使っている名前ではrenderer.jsの事。
  • ウィンドウ数と同じ個数存在する。
  • ウィンドウ・画面表示操作。
  • 画面からのイベント(クリック、入力等)取込み。
  • Node.JsやElectronを使用できない。
  • ipcRendererを使用。

2つのプロセスは断絶されている(isolated)

メインプロセスとレンダラープロセスは断絶(isolated)いる。 画面を操作するレンダラープロセスはセキュリティリスクとならない様に、Node.JsやElectronを使用できなくなっている。 しかし、レンダラプロセス(renderer.js)が検知したクリック等で外部に通信したい時など、Node.Jsの使用が必要な場合がある。 その時にレンダラープロセスはメインプロセスに内部プロセス間通信をして、メインプロセスでNode.Jsを走らせてもらうようにしなければなならい。(内部プロセス間通信:Inter Process Communication、以下ipc) そのipcに使うのがメインプロセスではipcMainでレンダラープロセスはipcRendererで、これらはElectronのモジュールです。

プレローダ(preload.js)

しかし、レンダラープロセスはElectronを使用できません。 そこでこのプレローダが必要になるのです。 プレローダは、

  • Node.JsにもElectronにもwindowオブジェクトやdocumentオブジェクトにもアクセスできます。
  • レンダラープロセスより先に読み込まれます。
  • Electronを使用できないレンダラープロセス用にipcRendererモジュールを使ってAPIを用意します。
  • レンダラープロセスは用意されたipcRendererのAPIを使ってメインプロセスとipcします。(下の図のような感じ)
ipcMain and ipcRenderer

3つのipc通信

各通信はipcRendererとipcMainが対(ペア)となって通信する。

  1. 双方向通信
    • 最初はレンダラーからipcRenderer.invokeのAPIで送信
    • メインはipcMain.handleで受取、returnでオブジェクトを返信(Promiseなども可能)
  2. レンダラーからメインの片方向通信
    • レンダラーはipcRenderer.sendのAPIで送信
    • メインはipcMain.onで受取
  3. メインからレンダラーの片方向通信
    • メインからはwebContents.sendで送信
    • レンダラーはipcRenderer.onのAPIで受取

channelとevent

ウィンドウが複数ある場合や、非同期で時間がかかかる処理の間に複数の通信が入り乱れる可能性もあるので、それらの通信の識別はchannelとeventで管理される。

  1. channel
    • 同じchannel名でipcRendererとipcMianで対になって通信する。
    • ipcRendererとipcMainにchannel名をプログラムで記述しなければならない。
    • 異なるウィンドウに同じchannel名を使用しても構わないが、ウィンドウ毎に処理が同じな『ファイルを開く』等の様な場合でない時は、eventオブジェクトで識別するか、ユニークなchannel名をとする方がよい。
  2. event
    • eventオブジェクトでいろいろな値を持っているが、ここではipcに関する内容だけにします。
    • ウィンドウが複数ある場合、どのウィンドウから通信が来てどのウィンドウに返せばいいのか分かるように、ipcRenderer.invokeではevent内にウィンドウ識別情報を入れてeventオブジェクトを送信する。
    • eventオブジェクトは自動的に送られる為、記述する必要はない。
    • eventオフジェクトのプロパティ値を取得する事は出来る。

基本例

ここから下のコードのファイル構成は『Electron WindowsでインストールからHello World, ビルドまで』の構成と同じです。 また、ここから下のコードはその記事のコードに追記しています。

これから作るボタンの表示の左スペースを入れる為に、/my_electron/src/myCss.cssを以下の様に作ります。(このファイルは無くても動作には関係しません)

myCss.css
button{
    margin-left: 10px;
}

index.html

  1. myCss.cssを読み込むリンクを付けます
  2. ボタンを付けます。
/my_electron/src/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self' https://stackpath.bootstrapcdn.com">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4"  crossorigin="anonymous">
    <link rel="stylesheet" href="myCss.css">

    <title>My electron</title>
</head>

<body>
    <div>
        <h1>Hello World!</h1>
        <button type="button" class="btn-sm btn-primary" id="test">ボタン</button>
    </div>
    <script src="./renderer.js"></script>
</body>
</html>

preload.html

  1. contextBridgeとipcRendererを読込
  2. contextBridge.exposeInMainWorldでmyApiを作る。
  3. メソッドはapi1にipcRenderer.invoke、
    api2にipcRenderer.send, api3にipcRenderer.onを作る。
  4. api1、api2、api3にchannel名を付ける。 api1のipcRenderer.invokeのは、ipcMain.handler(channel=’channel_ichiri’, )にデータを送るので、ipcRenderer.invokeのchannel名も’channel_ichiri’としている。
  5. contextBridge.exposeInMainWorld()メソッド内のオブジェクトはメインワールドのグローバルwindowオブジェクトに読み込まれるので、myApi.api1()は、window.myApi.api1()と呼び出せる。contextBridge.exposeInMainWorld(apiKey, api)
  6. このpreload.jsはmain.jsでメインウィンドウのwindowオブジェクトに読み込まれる。
  7. このpreload.jsはindex.html内やjsファイルとして見えず、windowオブジェクトのapiとしてブラックボックス化できる。(悪用されにくい)
/my_electron/src/preload.js
const {contextBridge ,ipcRenderer} = require('electron');
contextBridge.exposeInMainWorld(
    'myApi', {
      api1: async (...args) => await ipcRenderer.invoke('channel_ichiri', ...args),
      api2: async (...args) => await ipcRenderer.send('channel_name1', ...args),
      api3: (callback) => ipcRenderer.on('channel_name2', (event, ...args)=>callback(event, ...args))
    })
console.log('this is from preload');

renderer.js

  1. メインウィンドウが読み込まれた時、onLoadを実行するようにする。
  2. onLoad内で、document(index.html)内の、id=’test’の要素がクリックされたら、sendByApiの関数を呼び出すように、イベントリスナ―を設定する。 (id=’test’はindex.html内のButton。)
  3. sendByApiにpreload.jsで用意したmyApi.api1()を使用するようにwindow.myApi.api1()を設定する。 api1()はipcRenderer.invoke(channel=’channel_ichiri’,)で送るデータは’received from renderer!’とする。
  4. そして、invokeはメイン側でhandleで受取、返り値を返す事ができるので、非同期を扱うawaitで待って、resultを得てconsole.logで出力する。
/my_electron/src/renderer.js
console.log('this is from renderer');
document.write("<P>It's a beautiful day, today!</p>");

window.addEventListener('DOMContentLoaded', onLoad);

function onLoad() {
    document.querySelector('#test').addEventListener('click', () => {
        sendByApi();
    });
};

async function sendByApi(){
    result = await window.myApi.api1('received from renderer!');
    console.log(result);
}

main.js

  1. ipcMainを読み込む。
  2. mainWindowの設定(セキュリティ強化)。
  3. ipcRenderer.invoke()からの通信を受取るipcMain.handle()を記述する。 channelはrenderer.jsに書いた’ichiri_channel’にして、受け取ったデータはconsoleに出力して、文字列 ‘return from main’を返す。
/my_electron/src/main.js
const { app, BrowserWindow, ipcMain } = require("electron");
const path = require('path');

let mainWindow;
app.disableHardwareAcceleration();
const createWindow = () => {
  // メインウィンドウの作成
  mainWindow = new BrowserWindow({
    width: 1200,
    height: 800,
    webPreferences: {
      // プリロードスクリプトは、レンダラープロセスが読み込まれる前に実行され
      // レンダラーのグローバル(window や document など)と Node.js 環境の両方にアクセスできます。
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation:true,  // セキュリティ強化
      nodeIntegration: false, // セキュリティ強化
      sandbox:true            // セキュリティ強化
    },
  });

  // メインウィンドウに表示するhtmlパスを指定
  mainWindow.loadFile("index.html");

  // DevToolの起動
  mainWindow.webContents.openDevTools();

  // メインウィンドウが閉じられたときの処理
  mainWindow.on("closed", () => {
    mainWindow = null;
  });
};

//  初期化が完了した時の処理
app.whenReady().then(() => {
  createWindow();

  // アプリケーションがアクティブになった時の処理(Macだと、Dockがクリックされた時)
  app.on("activate", () => {
    // メインウィンドウが無い場合、メインウィンドウを作成する
    if (BrowserWindow.getAllWindows().length === 0) {
      createWindow();
    }
  });
});

// 全てのウィンドウが閉じたときの処理
app.on("window-all-closed", () => {
  // macOSの時以外はアプリケーションを終了
  if (process.platform !== "darwin") {
    app.quit();
  }
});

ipcMain.handle('channel_ichiri', async (event, ...args) => {
  console.log(...args);
  return 'return from main';
})

試してみる。

コマンドライン
> npm start
//或いは
> npx electron ./src

以下の様に『ボタン』が表示されているので、クリックすると、main.jsのipcMain.handle()からの返信の’return from main’が表示される。

そしてVscodeのTerminalには、renderer.jsのipcRenderer.handle()で送った’received from renderer!’を受取って表示している。

今回は一番簡単な文字列を送受信したが、ファイルや関数やオブジェクト等も送受信する事も出来る。

返信が出来る通信は、ipcRenderer.invokeで発信して、ipcMain.handleで返信する通信方法が基本で、レンダラープロセスからしか開始できない。 もしメインプロセスから通信したい場合は、webContents.sendからipcRenderer.onに送る。 この場合、メインプロセスからレンダラープロセスの片方向通信となる。

メインから片方向通信

  • win.webContents.sendで送り、ipcRenderer.onで受取る。 このwinはmain.jsの記述によって変わる。 今回のmain.jsではmainWindowと記述してあるので、mainWindow.webContents.sendとなる。
  • ipcRenderer.onはpreloader.jsのapi3で記述してあり、channel名は’channel_name2’と記述してあり、webContents.sendのchannelに’channel_name2’を入れる。

以下コードをmain.jsの末尾に追加。

/my_electron/src/main.js
setInterval(()=>{
  let now = new Date();
  let myTime = now.toLocaleTimeString()
  if (app.isReady()){
    mainWindow.webContents.send('channel_name2',myTime);
  }
} ,1000);
  • setInterval(,1000)で1秒ごとに現在時刻時間を送る。
  • app.isReady()は、ウィンドウを閉じているプロセスの間にwebContents.sendとならない様にしている。

以下コードをrenderer.jsの末尾に追加。

/my_electron/src/renderer.js
window.myApi.api3((event, data)=>{
    console.log(data);
});
  • preloader.js内のapi3がwebContents.sendを受けるipcRenderer.onなので、window.myApi.api3を使用する。 window.myApi.api3はipcRenderer.onというwebContents.sendからの受信イベントでcallbackが実行される。
  • eventは使っていないけど、eventとdataを記述してdataを分離しないと、構造化されeventもdataも一体のオブジェクトになって表示される。

コマンドラインからnpm startで実行してみよう。

この通り、1秒ごとに現在時刻が表示される。

ipcメソッド 早わかり表

組合わせと、用途が分かりにくかったので、分かりやすいよう表にまとめました。

レンダラープロセス側 (プレロードのAPIとして)通信方向メインプロセス側説明
[tooltips keyword='ipcRenderer.invoke' content = 'listener(event, ...args)
channel: string
...args: any[ ]']
(channel, ...args)

非同期  

[tooltips keyword='ipcMain.handle' content = 'listener(event, ...args) channel: string listerner: Function<Promise<void>  |  any>      event: IpcMainInvokeEvent      ...args: any[ ]']
(channel, listener)

非同期  

  • ipcMain.handleは受取り後、ipcRenderer.invokeに返信できる。
  • 返信にPromiseも使える。
  • ipcRenderer.sendSyncはレンダラープロセスもブロック(止めてしまう)為、最終手段で、通常はinvokeを使う。
[tooltips keyword='ipcRenderer.send' content = 'listener(event, ...args)
channel: string
...args: any[ ]']
(channel, ...args)

非同期  

[tooltips keyword='ipcMain.on' content = 'listener(event, ...args) channel: string listerner: Function      event: IpcMainEvent      ...args: any[ ]']
(channel, listener)

非同期  

  • ipcMain.onは返信できない。
  • ipcRenderer.sendは送信だけで受取れない。
  • ipcRenderer.sendで送って、何か受取りたい時は、メインプロセスからwin.webContents.sendで送信し、ipcRenderer.onで受取る事が必要。
[tooltips keyword='ipcRenderer.on' content = 'listener(event, ...args)
channel: string
listerner: Function
     event: IpcRendererEvent
     ...args: any[ ]']
(channel, listener)

非同期  

[tooltips keyword='win.webContents.send' content = 'listener(event, ...args)
channel: string
...args: any[ ]']
(channel, ...args)

非同期  

  • win.webContents.sendのwinはウィンドウのインスタンス。 どのウィンドウのレンダラープロセスに送信するか指定する。
  • win.webContents.sendは送信のみで受取れない。

ipcRenderer.invoke, ipcRenderer.send, win.webContents.sendメソッド共通

  • プロトタイプチェーンは含まれない。
  • 関数、Promise、Symbol、WeakMap、WeakSet の送信は、例外エラーになる。 但し、ipcMain.handleはPromiseを返信出来る。
  • DOMやElectronオブジェクトも例外エラーになる。

その他

  • メインプロセスはDOMオブジェクトを扱えないので、ImageBitmap, File, DOMMatrix等をメインプロセスにに送れない。

その他メモ

  • ipcMain.onはPromiseのfunctionを使えないので ファイル保存など時間がかかる処理に使えない。
  • ipcMain.onはipcRenderer.sendからの片方向の受信用で返信しない。
  • ipcMain.onの相手はipcRenderer.sendで、ipcMain.handleの相手はipcRenderer.invoke
  • 軽い処理にはsendとon、時間がかかる処理にはinvokeとhandle。
  • メインからのレンダラーの重い処理からの返信の場合は、webContents.sendでipcRenderer.onに送って、レンダラーの処理が終わったら、ipcRenderer.sendでipcMain.onに結果を送る。
  • 全てのipcRendererはpreload.jsでAPI定義して、window.Apiで呼び出して使用する。

エラー処理の場合

async function sendByApi(){
    result = await window.myApi.api1('received from renderer!').catch((err) => {
                 console.log(err);
             });
    console.log(result);
}

参考サイト

最初に理解するきっかけを作ってくれたサイトです。 図入りで分かりやすいです。

Electron | IPCによるプロセス間通信(ipcMain, ipcRenderer, 設定) - わくわくBank
IPCモジュールを利用すると「Main Process」と「Renderer Process」間で通信できるようになります。ここでは、IPCモジュールの基本的な利用方法を確認します。また、nodeIntegrationなどのセキュリティ周り...
ElectronでのIPCの例(send, sendSync, invoke, etc.)
ElectronでIPCを同期的に行うときは、sendSyncでなく、invokeを使う。 ということを言いたくて書いていたらIPCの例みたいになったけどまあいいや。 sendSync sendSyncは実装の見た目は簡潔になるが、レンダラ

詳しくは以下が良く分かります

コメント