2015.05.08
既存の JS コードを RxJS で書き換えてみる
Javascript のトレンドを追いかけている D.M. です。今回は RxJS を紹介したいと思います。
RxJS とは?
Javascript の関数型リアクティブプログラミング( Functional Reactive Programming, FRP )のためのフレームワークです。 Observer パターン に基づいて記述されています。全てのデータを時間軸で変化するリスト=非同期データストリームと考えることで、関数型のモデルとして抽象化し、簡潔な宣言的な実装を可能にしています。
RxJS が解決する問題
Javascript は UI 制御や Ajax などで非同期処理を実装することが非常に多い言語です。この処理が終わったらこっちの処理をお願いしいますという同期処理を記述しようとした場合にコールバック関数を利用しますが、記述が冗長で非常に見づらいソースコードになってしまう状況が散見されます。これまでこのコード冗長化と可読性低下の弊害は非常に大きな問題となっていましたが、決定打となる解決策が無い状況が続いていました。
関数型リアクティブプログラミングはこの Javascript コールバック地獄から解放し、ソースコードの記述量を少なくすることが可能となる画期的なフレームワークです。考え方自体は古くからあるものでしたが、近年の関数型記述への関心の高まりとともに徐々に注目を集めるようになってきました。 RxJS は新しい Javascript の実装を提案しています。
できること
以下のようなイベント処理を関数型のメソッドチェインで実装できます。
・クリックや値の入力などのイベントを監視する。(オブザーブ) ・イベントによって発生した値が処理対象かどうか判別する。(フィルター) ・入力された値を識別して何かの値に置き換えるなどの加工を行う。(マップ) ・フィルターやマップ等の処理後に画面描画などのメイン処理を実行する。(サブスクライブ)
できないこと
・ HTML の描画のためのライブラリではありません。逆に jQuery 等の描画系処理とは共存して利用できます。RxJS は描画の前後のデータフローの制御のみを行います。 ・ MVC 、 MVVM フレームワークではありません。 RxJS 自体の役割はもう少し低レイヤーなイベント処理制御に特化しています。 RxJS を基にした MVVM フレームワーク WebRX などがあります。また既存の MVVM フレームワークと共存させることも可能です。
ユースケース
実際想定されているユースケースは以下のようなものがあります。
・更新ボタンを押すと通信が発生する処理。非同期処理(ajax等のpromise)。 ・1箇所の値を変更すると複数の箇所を同時に変更する処理。散発的に発生するUIイベント(クリックなど)
※一般的な変数、ユーザー入力、プロパティ、キャッシュ、構造化されたオブジェクトなど変化する値ならなんでもストリームとして利用できます。
関数型リアクティブプログラミングについては多くの記事があります。詳細は以下の記事等を参考にしてください。
<参考 URL >
【翻訳】あなたが求めていたリアクティブプログラミング入門
http://ninjinkun.hatenablog.com/entry/introrxja
競合ライブラリ
関数型リアクティブプログラミングが可能な JS フレームワークは数種類あります。RxJS 以外では Beacon.js と Kefir.js が良く知られていて、それぞれ似ていると言われています。安定性や情報量は RxJS が優れているため、今回はこちらをチョイスします。
<参考 URL >
ECMAScript7を見据えた、JavaScript(TypeScript)で使えるFRPライブラリの比較調査
http://qiita.com/kondei/items/17e5d4867a0652911e52
既存の JS コードを RxJS で書き換えてみる
9199.jp のテストページに仕込んでみました。駅の周辺検索の県名セレクトボックスを選択すると路線のJSONを取得しますが、県名セレクトボックス選択時のイベント処理を RxJS で実装しています。
やりたいこと
・県名セレクトボックスを選択する Change イベントを監視し、選択された県のIDを利用した Ajax 通信を発行して、 HTML を再描画する処理を呼ぶ。 ・ Ajax 通信と HTML 描画は jQuery を利用する。(既存のまま)
動いている画面はこちらです。
http://9199.jp/js-test/rxjs-test.html
コードのポイント
今回利用した RxJS の関数は以下の通りです。
Rx.Observable.fromEvent
イベントの監視を登録します。ここではセレクトボックス HTML エレメントのオブジェクトの Change イベントを監視しています。
distinctUntilChanged
イベントの中身が前回と同じかどうか判定しています。 if 文のような処理を独自に実装したい場合は filter によって作成することができます。
map
イベント監視によりストリームには Change イベントのオブジェクトが流れてきます。そのイベントオブジェクトの中身を加工して、選択された県のIDを取得して続くメソッドチェインに渡しています。
flatmap
Ajax 通信を行いますが、複数の Ajax が同時に走るとリクエストごとにレスポンスのタイミングが異なります。この flatmap を使うと複数の非同期処理をまとめて扱うことが出来ます。
subscribe
メインの処理を記述します。ここでは HTML の再描画を render で行っています。
以下、実際のコードです。
<script src="http://cdnjs.cloudflare.com/ajax/libs/rxjs/2.2.26/rx.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/rxjs/2.2.26/rx.binding.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/rxjs/2.2.26/rx.async.js"></script> <script type="text/javascript"> //該当の県セレクトボックスのHTMLエレメントを取得する var prefSelectBox = document.querySelector("#stationpref"); //県セレクトボックスの Change イベントを監視する。 var prefSelectBoxStream = Rx.Observable.fromEvent( prefSelectBox, 'change' ); var selectedPref = 0; //Changeイベントの中身を読み込んで県コードを判定する関数 var idMapping = function( changeEvent ){ console.log("observe prefSelectBox onchange -> map"); console.log( changeEvent ); var idx = changeEvent.target.selectedIndex; console.log("selectedPref : " + changeEvent.target.options[idx].value ); selectedPref = changeEvent.target.options[idx].value; return selectedPref } //Ajax通信を行う関数 var ajax = function( selectedPref ) { console.log("observe prefSelectBox onchange -> flatMap"); var requestUrl = ""; var ua = $.browser; if(ua.mozilla) requestUrl = '/common/json/station/line_mozilla/' + selectedPref + '.json'; else requestUrl = '/common/json/station/line_ie/' + selectedPref + '.json'; // Ajax の正常完了の後に次の処理に移るように、ここでもオブザーブを設定する。(すなわちPromiseと同じことをしている) return Rx.Observable.create(function (observer) { jQuery.getJSON( requestUrl , function(response) { observer.onNext(response); } ) }); } //受け取ってAjax通信結果のJSONを基にHTML描画を行う関数 var render = function( responseJson ) { console.log("observe prefSelectBox onchange -> subscribe"); console.log( responseJson ); renderLineCode( responseJson ); } //実行は非常にシンプル prefSelectBoxStream .distinctUntilChanged() .map( idMapping ) .flatMap( ajax ) .subscribe( render ); //HTML描画箇所。ほぼ既存のコードそのままなのでゴリゴリしている。 var renderLineCode = function( json ){ line_box = document.getElementById("linecode"); station_box = document.getElementById("stationcode"); line_idx = line_box.options.length; staion_idx = station_box.options.length; for ( i = 0 ; i <= line_idx; i++ ) line_box.options[0] = null; for ( i = 0 ; i <= staion_idx; i++ ) station_box.options[0] = null; station_box.options[0] = new Option("駅名を選択", 0); if ( selectedPref == 0){ line_box.options[0] = new Option("路線を選択", 0); }else{ count = 0; line_box.options[count++] = new Option("路線を選択", "0"); $.each(json.line, function(i,l){ if( l ){ line_box.options[count++] = new Option(l.line_name,l.line_cd); } }); } } </script>
ハマったところ
RxJS は jQuery との親和性を考慮して作られています。ただこの 9199.jp は古いヴァージョンの jQuery が利用されていたので、一部の機能が動作しませんでした。たとえば古い jQuery ではイベント処理に bind ファンクションが使われますが、 RxJS ではイベント処理は on ファンクションでないと動きません。この問題を補うために独自に jQuery を拡張してポリフィルしました。
まとめと所見
・ RxJS は関数型リアクティブプログラミングのためのもので、非同期イベント処理の記述についてコールバック記述を無くすことができ、簡潔なコードを書くことができる。
はじめはギョギョっと思った関数型記述でしたが、慣れてしまえば簡潔で読みやすいものでした。部分的な導入もしやすいためこれからチャンスを見つけて使っていきたいと思います。
次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD