気がついたら今年が終わりそうです。おかしいな・・・
さて、最近とみにElectronが盛り上がってる様で、盛り上がってるものには乗ってみようということで、私の方でも何かやってみようかと思ってやってみました。
Electronとは
私が語るまでもないので、この辺の参考資料を。
Electronでアプリケーションを作ってみよう
Electronでシンプルなアプリケーションを構築〜配布するまでのまとめです
Electron を試す – 開発環境の構築 | アカベコマイリ
環境の構築や実践がよくまとめられています
https://github.com/atom/electron
本家のリポジトリです。ドキュメントを読むと色々とまとまってます。
RxJS
Reactive Programmingのためのライブラリです。
Rxなんとか系統のうち、JavaScriptでの実装です。これらのライブラリの中で、一番利用されているものです。
Angular2とかでも採用されるようですが、とりあえず今回はFluxのライブラリとして利用しています。
React + RxJS
すでにReact.jsとBacon.jsを組み合わせては作ってみたことがありますが、今回はReactとRxJSを組み合わせてみました。
これについても色々とfluxライブラリなどもあるようですが、今回はそういった枠組みは作らずにとりあえず作ってみました。
まだ作ってる最中ですが、Electronがらみというよりも周辺環境周りでどハマりしたのでその辺を書こうかと思います。
Electron上でのユニットテスト
Electron上でアプリケーションを書くことの一番のメリットは、Chrome(Webkit)以外の確認が不要で、しかもNodeのライブラリを使い放題(メインプロセス限定)というのがあります。
せっかく作るんであれば、ユニットテストとかも色々と試してみたいものです。
今回はMocha+Power Assertという鉄板の組み合わせではなく、 https://github.com/substack/tape を使ってみました。
ついでにこれだけではなく、 https://github.com/juliangruber/tape-run も使ってみました。
さて、ElectronはMain Process、Renderer Processと分離しています。この中で、ipcなどのライブラリをrequireしたりしています。
Main Process と Renderer Processのテスト
この二つが分離していることがあり、まとめてテストすると色々めんどくさいです。また、Main ProcessではRenderer Processとまとめて実行すると、以下のような問題があって実行できないケースが多くあります。
- MainでBrowserify/Webpackできないモジュールが含まれてると落ちる
- ipcモジュールのインターフェースがそもそもMainとRendererで違う
- etc...
これらがあるため、テスト自体もMain/Rendererで分離してそれぞれ実行するようにしています。
Mainの方は
$ $(npm bin)/tape ./test/main/**/*_test.js | faucet
のようにして、普通に実行できます。ただし、ipcモジュールはElectronのMain Processとして動かさなければ動きません。なので、今の所は機能も少ないのでその辺は無視しています。
Renderer Process側は、Electron上で動かすっていうだけで、ブラウザで動かすこととほとんど変わりません。
$ browserify -t baberivy -t react-templatify ./test/renderer/**/*_test.js | tape-run | faucet
ただし、tape-runを経由すると、これまたipcモジュールがうまく動作しません。そのため、ipcモジュールが必要な場所については、ipcがただのEventEmitterであるということで、以下のようなモックを用意しています。
import sinon from 'sinon';
import EventEmitter from 'events';
/**
* Make stubbed ipc object.
*
* Resulting this has very simple stubbing imiplemented methods.
*/
export default function createIpc() {
const ipc = new EventEmitter();
ipc.send = function(name, ...args) {
ipc.emit(name, {
sender: {
send(name, ...args) {
ipc.emit(name, ...args);
}
}
}, ...args);
};
return ipc;
};
後は require('remote')
を一切使わないように、Main Process側の力が必要な場合は全てipcを経由させています。
RxJSをStore/Dispatcherとして使う
Flux様のアーキテクチャにしていはいますが、実際にはViewとAction、StoreとBinderというわけ方をしています。
Binderは、ReduxでいうReducerに相当するもので、Actionから提供されるsubjectをsubscribeして、そこからくるアクションを捌くことを目的としています。Storeは、ちょっと色々やっていますが、基本的にはobservableと現時点の値をセットで持っているだけです。
この様に分けたのは、以前Bacon.jsでやったときはひとつのモジュールにAction/Binderに相当するものまで全て詰め込んだ状態だったので、若干テストが書きにくい、というかsubjectがグローバルと変わらない状態になってしまってのいたので、今回はエントリポイント以外ではグローバルは使わない、という縛りにしてやったところ、こういう形になりました。
ただ、Actionは若干手を抜いてシンプルなモジュール形式になっています。staticメソッドしかないクラスでも変わらんやん・・・って思ったので。
import R from 'ramda';
import Rx from 'rx';
import keyMirror from 'keymirror';
export const ACTIONS = keyMirror({
REQUEST_DIRECTORY: null,
RECEIVE_DIRECTORY: null
});
export let subject = new Rx.ReplaySubject(1);
export function dispose() {
subject.dispose();
subject = new Rx.ReplaySubject(1);
}
export function requestDirectory(path, pane) {
subject.onNext({key: ACTIONS.REQUEST_DIRECTORY, path, pane});
}
...
そしてBinderはこんな形です。
import R from 'ramda';
import {IPCKeys, Pane} from 'sxfiler/common/Constants';
import * as Actions from 'sxfiler/renderer/actions/Directory';
export default class Directory {
constructor() {
this._dispose = null;
this._ipcDispose = {};
}
receiveDirectory(ipc, store, action) {
let {path, fileList, pane} = action;
let data = {fileList, path};
store.update((value) => mergeState(value, data, pane));
}
requestDirectory(ipc, store, action) {
let {path, pane} = action;
let data = {path};
store.update((value) => mergeState(value, data, pane));
ipc.send(IPCKeys.REQUEST_FILES_IN_DIRECTORY, path, pane);
}
bind(ipc, store) {
const A = Actions.ACTIONS;
this.dispose();
// Setup binding with observable of directory actions.
this._dispose = Actions.subject.subscribe((action) => {
let {key} = action;
switch (key) {
case A.RECEIVE_DIRECTORY:
this.receiveDirectory(ipc, store, action);
break;
case A.REQUEST_DIRECTORY:
this.requestDirectory(ipc, store, action);
break;
default:
break;
}
});
}
...
}
やってること自体は、Actionのsubjectをsubscribeして、actionのイベントごとに処理を割り振る、という形です。
それぞれの処理を別メソッドにしてるのは、単体で実行させやすくするためですが、基本的にはテスト時でもActionを実行すればそれで済むので、こうしてます。
Storeは事実上Subjectをラップしただけのものですので割愛しますが、こういう形にとりあえず落ち着いています。
ただし、この形式には、 ソースコードの分量が増えまくる というかなりの難題もあり、正直めんどくさいです。
RxJSとReactの相性
相性自体は、Immutability がどちらも推奨されているので、基本的には問題ありません。
どちらかというと、RxJSの使い方を調べる方が遥かに大変です。(メソッドがあまりにも多すぎる・・・)
それと、RxJSのObservableはデフォルトでは 同期処理 である、ということも把握しておく必要があるかもしれません。
それがわかっていれば、後は粛々と書いていく感じにできます。
Electron上でのFlux(様)アーキテクチャ
これについては他により高度な話を記述している方もいらっしゃるのでそっちを見た方がいいですが、基本的にBrowserWindowが一枚だけ、というのであれば、採用した方が見通しはたちやすくなるとは思います。
ただし、fluxでもそうですが、Action/Dispatcher/Storeと色々と分散するので、逆に参照するファイルが多くなって全体像が掴みにくくなるというのもまた事実だと思います。
なので、時と場合に応じて、モジュールベースのStore+Actionだけ提供する、というのも十分にありです。特に一人で始めたものの場合、めんどくさいと続きません。
まとまってないですが、今回はこの辺で。