Node.js
flux
reactjs
atom-shell
Electron

Electron, React, Fluxを使ってアプリケーションを作った

More than 3 years have passed since last update.

Electron勉強会 #1で発表した、Electron, React, Fluxを使ってアプリケーションを作る話について解説する。

位置づけとしてはElectronのremoteでプロセス間通信を高レベルに扱うの続きにあたる。


概要

ElectronアプリケーションにFluxアーキテクチャを利用することは、レンダラプロセスの内部で処理が解決する場合において有効である。レンダラプロセス間で状態を共有したい場合、メインプロセスとレンダラプロセスでFluxアーキテクチャを二重化することでこの課題を解決できる。

しかしユーザ操作によってレンダラプロセスが終了された場合、Fluxアーキテクチャそのままでは正しくActionを通知することができない。複数のレンダラプロセスを立ち上げることに適応できるよう、Fluxアーキテクチャに変更を加えなければならなかった。


成果物

MisumiRize/GHCache

日頃GitHubでコードを眺めていることが多いので、GitHubでコードをみながらバックグラウンドでgit cloneを実行するアプリケーションを作った。


Fluxアーキテクチャの有効性

通常のweb開発でフロントエンドをFluxアーキテクチャで実装することは、Electronで言えばレンダラプロセス上にFluxアーキテクチャを実装することに等しい。単一のBrowserWindowを立ち上げ、その中でFluxアーキテクチャが完結するのであれば、ベーシックなFluxアーキテクチャで事足りる。

言い換えれば、メインプロセスの存在やFluxセット同士のやりとりは、Fluxアーキテクチャの射程から外れている。例えば、あるレンダラプロセスにおけるFluxセットのイベントが、別のレンダラプロセス上に存在するFluxセットへ影響を与えることをベタに記述してしまうと、レンダラプロセス間の関係性が複雑になってしまう。これでは、「イベントの流れを一方通行にしてシンプルにする」というFluxアーキテクチャのメリットが薄れてしまう。

また、レンダラプロセス上にFluxセットを配置するということは、Fluxセットの有効期間がレンダラプロセス側でJavaScriptが実行されてから、レンダラプロセスが破棄されるまでに縛られることを意味している。レンダラプロセスのライフサイクルより長い寿命でオブジェクトを保持したい場合、別のアプローチが求められる。


remoteモジュールとFluxの二重化

つまり、Fluxセットにおいて状態を保持するStoreをメインプロセス側に持つことが、レンダラプロセスのライフサイクルという制約から状態を解放するための方針となる。メインプロセス側に状態を保てば、アプリケーションが終了するまでその状態は有効になる。

これを実現する方法がremoteモジュールである。具体的な内容については前回分で説明したので省略するが、remoteモジュールを使えばレンダラプロセスからメインプロセス側のモジュールを透過的に扱えるようになる。StoreではDispatcherにリスナを登録しているので、複雑化を避けるためにDispatcherもメインプロセス側に置き、Dispatcher-Storeの組み合わせがメインプロセス上に存在するよう変更する。

もちろん、これまでのようにレンダラプロセス側にDispatcher-Storeの組み合わせを置くことも可能である。指針としては、レンダラプロセスをまたいで共有したい状態や、アプリケーションの全体として保持したい状態をメインプロセス側のDispatcherへと伝達し、レンダラプロセス単位で揮発する状態をレンダラプロセス側のDispatcherへと伝送するようにする。

レンダラプロセス上に存在するViewからは必要に応じてレンダラプロセス側、メインプロセス側のStoreと配線するようにする。それとは別に、メインプロセス側に存在するStoreから通知されるイベントに基づき、メインプロセス側で新しくBrowserWindowを立ち上げる仕組みが、レンダラプロセスにおけるView的な役割として必要な場合もある。また、メインプロセス側にもAction的な要素が存在し、例えばGlobalShortcutで登録したホットキーイベントなどはこれにあたる。

electron_flux_2.png

こうして、メインプロセス、レンダラプロセスでFluxセットが二重化された上図のような構成となる。注意すべきは、情報の伝達を一方向にするというFluxアーキテクチャのセオリーを守ることと、情報の伝達をシンプルに保つためにもメインプロセス側のFluxセットはレンダラプロセス側のFluxセットに触らないようにすることである。これらの点に注意しておけば、複数のレンダラプロセスを立ち上げても関係性が複雑になりにくいと感じた。

electron_flux_3.png


Flux on Electronの問題点

remoteは前回紹介したとおり、レンダラプロセス側のオブジェクトをメインプロセス側に登録したままでレンダラプロセスを破棄した場合、正しくないプロセス間通信に対する参照が残りつづけるという問題がある。二重化したFluxセットでは、レンダラプロセス側のViewからメインプロセス側のStoreに登録したリスナがBrowserWindowを閉じても残留し、Storeのイベントが伝送された際にプロセス間通信が失敗してエラーが発生してしまう。

そもそもベーシックなFluxアーキテクチャは、以下の条件の上に成り立っていると考えられる。


  • 単一のレンダラプロセス上で動作し、レンダラプロセス終了時にはFluxセットが全て破棄される

  • 状態に関わるユーザ操作は全てキャプチャ可能であり、ユーザ操作のデフォルトの挙動はキャンセル可能である

特に後者の条件は、ブラウザ上のFluxセットとElectron上の二重化FluxでView (BrowserWindow)を破棄する際、違いが顕著に現れる。ブラウザ上では、View破棄を実行するユーザ操作は一旦ActionとしてFluxアーキテクチャの情報伝送サイクルに乗り、最終的にViewへ情報が到達してから反映、破棄される。いっぽうElectronでBrowserWindowを閉じてしまうと、ユーザ操作をActionとして受け入れる前にBrowserWindow破棄が実行されてしまう。この操作はOS側の実装に依るものなので、デフォルトの挙動をキャンセルすることはできない。

これらの条件が前提とされていたのは、Fluxアーキテクチャがブラウザ上で動かすことを想定されているからであり、基盤がデスクトップアプリケーションとなった以上なんらかの変更を加えなければ適応できない。


BrowserWindowをFluxアーキテクチャの要素にする

BrowserWindowを閉じるイベントに対してリスナを登録することはできるが、BrowserWindowで発生するイベントそのものを横取りすることはできない。つまり、BrowserWindowの機能がFluxアーキテクチャにおけるViewの代替物に相当すると捉えることは不十分であると考えられる。

そこでBrowserWindowをラップして、Storeからの通知を受け付けて生成、破棄に伴ってDispatcherへとイベントを通知する、ちょうどViewとActionを組み合わせたような役割を持たせることにした。そして、BrowserWindow破棄に伴ってDispatcherへとイベントを通知することで、メインプロセス側のStoreにレンダラプロセス側から仕掛けられたリスナを解除する仕組みを新しく作成した。

electron_flux_5.png

メインプロセス上の流れだけを記述すると、上図のようになる。レンダラプロセスをラップしたものをここでは便宜的にActivityと呼ぶことにした。Activityの由来はAndroidのActivityで、BrowserWindowの破棄をライフサイクル上のイベントとして捉えるところが似ているように感じたので、そのように呼んでいる。

最後にActivity内部や他の要素も付け加えて関係図を描くと、次のようになる。今回のアプリケーションはこのような構成で作成した。

electron_flux_6.png


雑感

Electronはまだ登場して日も浅く、ハウツーはさることながら、大規模なアプリケーションを作成するノウハウは全くと言っていいほど蓄積されていない。しかしElectronではSPAとも違った難しさがあるので、なんらかの設計パターンを作りコードの劣化を食い止める仕組みを用意しない限り、早晩破綻してしまうであろう。

今回はFluxアーキテクチャを改変してElectronに応用できる構成を考えてみた。もちろんこれは仮説のひとつであり、この構成にもなんらかの問題点があると考えられる。実際に動いている大規模Electronアプリケーションやそのユースケースを参考に、よりブラッシュアップさせてゆきたい。