SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

Electronのプロセス間の通信

MainProcess(BrowserProcess)とRendererProcess間の通信方法についてメモ。

プロセス間の通信方法は何通りかありますが、順番に見ていきます。

remoteモジュール使ってMainProcess側としてモジュールを読み込む方法

remote経由でのrequire。これもある意味プロセスをまたいだ処理、ということで。。

MainProcess用のモジュールをRendererProcessから使いたい時や、重たい処理をするコードをreqireするときに使ったりします。
今までも普通に書いてたけど、こういうヤツです。

index.ts(RendererProcessのコード)

var remote = require('remote');
var app = remote.require('app');
var BrowserWindow = remote.require('browser-window');

remote.require(xxxx)とすると、そのモジュールはMainProcess側のプロセスで動作します。
また、こうすることで、「app」や「browser-window」のようなMainProcess用のモジュールも、RendererProcessから利用することができるようになります。

RendererProcess⇒MainProcessへの通信(ipcモジュール)

今度はより汎用的なプロセス間の通信をしてみます。
まずはRendererProcessで何かの操作をしたら、そのタイミングでMainProcessに通知してみます。

RendererProcessとMainProcessの間で汎用的なプロセス間通信を行うには、「ipc」モジュールを利用します。
ipcモジュールでRendererProcess⇒MainProcessの通信をするときには、同期/非同期の二通りの通信方法が用意されてます。

非同期な通信

非同期な通信では、送信側からでipc.sendメソッドでメッセージを送り、受信側ではipc.onでハンドラを登録してメッセージ受信時の動作を定義します。

こんな感じ。
ここでは、htmlで適当にボタンを用意しておき、onclickでhello()メソッドを呼ぶようにしておきます。
そして、hello()メソッドでは、Renderer⇒Mainへとasync-messageというメッセージを送り、Mainプロセスでは処理が完了したら、RendererProcessへasync-replyというメッセージで処理の完了を通知しています。

送信側

index.ts(RendererProcessのコード)

var ipc:any = require('ipc');

function hello(){
    ipc.send('async-message', 'ping');
}

// MainProcessでの処理完了通知をハンドルするため
ipc.on('async-reply', function(arg) {
  alert(arg);
});
受信側

main.ts(MainProcessのコード)

ipc.on('async-message', (event, arg) => {
  console.log(arg);
  event.sender.send('async-reply', 'pong');
});

同期的な通信

同期的な通信を行う場合、MainProcess側での処理が完了するまでRendererProcessがブロックされてしまうため、基本的に非推奨となってます。
なので、あまり使うことはないかと思いますが、一応使い方をメモだけしときます。

同期通信では、処理の完了の返し方が違うので注意。
MainPrcessでevent.returnValueプロパティに値をセットするだけで、同期通信の完了通知となります。

index.ts(RendererProcessのコード)

var ipc:any = require('ipc');

function hello(){
    var result = ipc.sendSync('sync-message', 'ping');
    alert(result);
}

main.ts(MainProcessのコード)

ipc.on('sync-message', (event, arg) => {
  console.log(arg);
  event.returnValue = 'pong';
});

MainProcess⇒RendererProcessへの通信(webContents.sendメソッド)

今度はMainProcessからRendererProcessへとメッセージを送ってみます。
例えば、MainProcessでメニューとか作った時に、メニューのclickイベントでRendererProcess側に何かメッセージを送って、処理をする、といった場面で使えるかと思います。

RendererProcessへメッセージを送るには、目的のBrowserWindowのインスタンスのwebContentsプロパティを取得し、webContents.sendメソッドでメッセージを送ります。
受信側は、先ほどの例と同じようにipc.onで受け付けます。

また、この通信方法では、同期的な通信はサポートされておらず、非同期な通信のみサポートされています。
同期的な処理はデッドロックの原因になったり、なにかと問題を起こすので、同期通信は非サポートとのこと。

送信側

sendメソッドで送るだけ。RendererProcessに何か引数をつけて通知することもできます。 main.ts(MainProcessのコード)

  mainWindow.webContents.send('menu-clicked', 'メッセージの引数');
受信側

onで登録するコールバック関数には、sendで指定した引数が渡ってきます。 index.ts(RendererProcessのコード)

ipc.on('menu-clicked', (msg) => {
    alert(msg);
});

サンプル

こんな内容のサンプルです。

  • MainProcessでアプリケーションメニューを作る
  • そのメニューの項目をクリックしたら、クリックした項目に応じたメッセージをRendererProcessに通知
  • RendererProcess側では、メッセージを受け取ったら、その内容をalertで表示

f:id:minami_SC:20151103123441p:plain:w300

全体のコードはこんな感じ。

main.ts(MainProcessのコード)

import app = require('app');
import BrowserWindow = require('browser-window');
import Menu = require('menu');
import ipc = require('ipc');
require('crash-reporter').start();

// メインウィンドウの参照をグローバルに持っておく。
var mainWindow: GitHubElectron.BrowserWindow = null;

var menu = Menu.buildFromTemplate([
  {
    label: 'File',
    submenu: [
      {label: 'New File', click: () => onMenuClicked('New File')},
      {label: 'Save', click: () => onMenuClicked('Save')}
    ]
  },
  {
    label: 'Edit',
    submenu: [
      {label: 'Copy', click: () => onMenuClicked('Copy')},
      {label: 'Paste', click: () => onMenuClicked('Paste')},
    ]
  }
]);
Menu.setApplicationMenu(menu);

// すべてのウィンドウが閉じられた際の動作
app.on('window-all-closed', function() {
  // OS X では、ウィンドウを閉じても一般的にアプリ終了はしないので除外。
  if (process.platform != 'darwin') {
    app.quit();
  }
});

app.on('ready', function() {
  // 新規ウィンドウ作成
  mainWindow = new BrowserWindow({ width: 800, height: 600 });
  // index.htmlを開く
  mainWindow.loadUrl('file://' + __dirname + '/index.html');

  // ウィンドウが閉じられたら、ウィンドウへの参照を破棄する。
  mainWindow.on('closed', function() {
    mainWindow = null;
  });
});

function onMenuClicked(itemName: string){
  // クリックされた項目の情報を、RendererProcessに通知
  mainWindow.webContents.send('menu-clicked', itemName);
}

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello World!</title>
    <script src="index.js"></script>
  </head>
  <body>
    <h1>Hello World!</h1>
    <hr/>
  </body>
</html>

index.ts(RendererProcessのコード)

var ipc:any = require('ipc');
ipc.on('menu-clicked', (msg) => {
    alert(msg + ' clicked!!');
});