More than 1 year has passed since last update.

Electronでipcを使ってプロセス間通信を行うの続き。

概要

Electronではメインプロセスと個別のBrowserWindow上で走るプロセス(レンダラプロセス)の間はプロセス間通信でメッセージのやりとりができ、ipcモジュールとして提供されている。

remoteではプロセス間通信ををラップした高レベルな機能が提供されており、レンダラプロセスからメインプロセスのAPIを、あたかもレンダラプロセスのAPIとして存在しているかのように扱うことができる。

前提

BrowserWindow(レンダラプロセス)を立ち上げてHTMLを表示する部分までは終了しているものとする。

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

app.on('ready', function() {
  var currentWindow = new BrowserWindow({});
  currentWindow.loadUrl('file://' + __dirname + '/index.html');
});

だいたいこれぐらいの環境を想定。

remoteを使う

例えば、BrowserWindowはメインプロセスのみにAPIとして提供されているので、レンダラプロセスで走っているJavaScriptコードから新しくBrowserWindowを立ち上げることは通常できない。

remoteを使えば、制約を回避してレンダラプロセスから新しいBrowserWindowを起動することができる。これはメインプロセス上に存在するbrowser-windowモジュールをレンダラプロセスからremoteを使って呼ぶことで実現されている。

var remote = require('remote');
var BrowserWindow = remote.require('browser-window');
var newWindow = new BrowserWindow({});

レンダラプロセス上における同一性

もちろんremote経由でrequireしたものととレンダラプロセスから直接requireしたものでは別々のものとして扱われる。内部で状態を保持するものであれば別個で保持される。

increment.js
var count = 0;
module.exports = function() {
  return ++count;
};
index.html
var increment = require('./increment');
var incrementRemote = require('remote').require('./increment');

console.log(increment === incrementRemote);  // => false

console.log(increment());  // => 1
console.log(increment());  // => 2

console.log(incrementRemote());  // => 1

remoteでrequireする場合のパス解決は、package.jsonにmainとして指定したファイルが基点になり、レンダラプロセスとは別に解決が行われる。ディレクトリ構成を組んである場合注意が必要。

remote経由でrequireしたものはレンダラプロセスと別に状態を保持しているので、レンダラプロセス側の状態が変更されても、その変更は影響を発生させない。

レンダラプロセス側で保持している状態はBrowserWindow(レンダラプロセス)ごとに別個となっており、そのBrowserWindowを閉じれば解放される。一方でremoteからrequireした、メインプロセスが保持する状態は、requireのキャッシュを削除して明示的に解放するかアプリケーションを終了するまで残りつづける。

remote利用中の注意点

レンダラプロセス側のオブジェクトをメインプロセスから参照させた状態でレンダラプロセスを終了させた場合、オブジェクトは消滅するが参照は残りつづける。プリミティブ型は問題なく使える。

bad-reference.js
var internal = {};

module.exports = {
  put: function(key, value) {
    internal[key] = value;
  },

  keys: function() {
    return Object.keys(internal);
  },

  get: function(key) {
    return internal[key];
  }
};
index.html
var badReference = require('./bad-reference');
var foo = function() {
  console.log('bar');
};

badReference.put('foo', foo);

BrowserWindowを閉じた後でkeysを呼び出すと、登録したfooがキーに残っているのが確認できる。このfooを取得してもundefinedでもnullでもなく、無効なプロセス間通信に対する参照が取得されてしまい、一見そのオブジェクトが存在しているかのように見えてしまう。

例えばこれが問題になるのはEventEmitterへのリスナ登録である。

bad-listener.js
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
index.html
var badListener = require('remote').require('./bad-listener');

badListener.on('some-event', function() {
  console.log('an event emitted');
});

BrowserWindowを閉じたあとでメインプロセス側からEventEmitterのイベントを発火させると、内部的には無効なリスナへの参照が残ってしまっているので、プロセス間通信の失敗としてエラーが発生してしまう。エラーを発生させないためにも、適切なタイミングでリスナの解除を行わなければならない。

雑感

remoteを使ってメインプロセスのコンテキストでrequireすることの目的は複数存在する。

  • レンダラプロセスからレンダラプロセス内部のライフサイクルを越えるAPIにアクセスする
  • レンダラプロセスをまたいだアプリケーション全体の状態にアクセスする

どちらも乱用するとメインプロセスとレンダラプロセスの責務が曖昧になり、うっかりでメインプロセスに無効な参照を残したままとなってしまいかねない。前述した目的からも複数のBrowserWindowを立ち上げることが想定されるので、ネイティブアプリケーション的な落とし穴には注意したい。