September 2016

Volume 31 Number 9

Reactive Framework - Reactive Extensions による AJAX 対応の非同期 Web ページのビルド

Peter Vogel

以前のコラムでは、オブザーバー パターンを使用して、長時間実行されるタスクを管理する方法について説明しました (msdn.com/magazine/mt707526 参照)。そこでは、Windows アプリケーションで長時間実行されるプロセスのイベント シーケンスを管理するために、Microsoft Reactive Extensions (Rx) を使ってシンプルなメカニズムを実現する方法を紹介しました。

ただし、長時間実行されるタスクのイベント シーケンスを監視するためだけに Rx を使用しているのでは、このテクノロジを十分に活用しているとはいえません。Rx の長所は、イベント ベースのプロセスを他のプロセスと非同期に統合できるところにあります。例として、今回は Rx を使用して、Web ページのボタンをクリックすることで、Web サービスの非同期呼び出しを実行します (ボタン クリックは事実上、1 つのイベントから成るシーケンスです)。クライアント側の Web 環境で Rx を使用するには、JavaScript 用 Rx (RxJS) を使用します。

Rx は、さまざまなシナリオを抽象化し、LINQ のような使いやすいインターフェイスを使用してそれらのシナリオを扱う標準の方法を提供します。このインターフェイスにより、シンプルなビルディング ブロックからアプリケーションを構成できるようになります。Rx は、UI のイベントとバックエンド プロセスを統合しながら、これらを分離した状態に保つことができます。Rx により、UI を書き直すときでも、対応するバックエンドに変更を加える必要がありません (逆も同様です)。

また、HTML とコードを明確に分離することにも対応しており、事実上、特別な HTML マークアップなしに、データ バインディングを利用できるようになります。RxJS は、既存のクライアント側テクノロジ (jQuery など) を基盤にビルドします。Rx には他にもう 1 つのメリットがあります。それは、Rx の実装がすべて非常によく似たものになる傾向があることです。今回の RxJS コードは、以前のコラムで作成した Microsoft .NET Framework コードにそっくりです。つまり、1 つの Rx 環境で得たスキルを、あらゆる Rx 環境で活用できます。

RxJS の概要

RxJS は、アプリケーションを 2 つのグループに抽象化することで目的を達成します。1 つ目のグループのメンバーはオブザーバブルです。基本的に、イベントを発生させるものがすべてこれに該当します。RxJS には、オブザーバブルを作成するための演算子が豊富に用意されています。オブザーバブルには、イベントを発生しそうなものがすべて含まれます (たとえば、Rx では配列をイベント ソースに変換できます)。Rx の演算子により、イベントの出力をフィルター処理して変換できるようになります。オブザーバブルは、イベント ソースの処理パイプラインとも考えられます。

2 つ目のグループのメンバーはオブザーバーです。オブザーバーは、オブザーバブルから結果を受け取り、新しいイベント (とその関連データ)、イベントのエラー シーケンス、イベントの終了シーケンスというオブザーバブルの 3 つの通知に対する処理を提供します。RxJs でのオブザーバーとは、この 3 つの通知のうち 1 つ以上を処理する関数を含むオブジェクト、または通知ごとに 1 つ用意される単なる関数のコレクションです。

この 2 つのグループを結び付けるため、オブザーバブルは、そのパイプラインに対してオブザーバーを 1 つ以上サブスクライブします。

RxJS をプロジェクトに追加するには NuGet を使用します (NuGet ライブラリで、RxJS-All を探してください)。ただし、1 つ注意点があります。 TypeScript で構成されたプロジェクトに RxJS を初めて追加するときに、関連する TypeScript 定義ファイルも必要かどうかを問い合わせるメッセージが NuGet マネージャーによって表示されます。[はい] をクリックするとファイルが追加され、約 400 件の「定義が重複しています」エラーが発生します。それ以降、このオプションを受け入れないようにしています。

また、RxJS に便利なプラグインを提供する Rx サポート ライブラリが多数存在します。たとえば、RxJS-DOM (NuGet では RxJS-Bridges-HTML として提供されています) は、クライアント側での DOM イベントとの統合と、jQuery との統合、およびその両方を可能にします。RxJS でレスポンシブ Web アプリケーションを作成する場合は、このライブラリが不可欠です。

Rx のほとんどの JavaScript プラグインと同様、RxJS-DOM の新機能は、RxJS ライブラリの中核を成す Rx オブジェクトに基づいています。RxJS-DOM は、jQuery の同様の関数へのブリッジになるものなど、複数の便利な関数が含まれた DOM プロパティを Rx オブジェクトに追加します。たとえば、RxJS-DOM を使用して JSON オブジェクトを取得する AJAX 呼び出しを作成するには、以下のコードを使用します。

return Rx.DOM.getJSON("...url...")

Rx と RxJS-DOM を両方使用するのに必要なのは、以下の 2 つの script タグだけです。

<script src="~/Scripts/rx.all.js"></script>
<script src="~/Scripts/rx.dom.js"></script>

rx.all.js と rx.dom.js には両ライブラリの機能がすべて含まれているため、rx.all.js と rx.dom.js を両方とも使用すると重量級のソリューションになります。さいわい、Rx も RxJS-DOM も、その機能が複数の軽量のライブラリに分割されているため、ページに必要な機能が含まれたライブラリだけを組み込むことができます (GitHub のドキュメントを参照すれば、どの演算子がどのライブラリに属しているかがわかります)。

RESTful サービスを統合する

JavaScript アプリケーションでは、ユーザーがボタンをクリックすると、Web サービスに対して非同期呼び出しが行われ、そのサービスから結果を取得するのが代表的なシナリオです。RxJS ソリューションをビルドする最初の手順は、ボタンのクリック イベントをオブザーバブルに変換することです。これは、RxJS/DOM/jQuery を統合することで簡単に実行できます。たとえば、以下のコードでは jQuery を使用し、getButton の ID を指定してフォームの要素への参照を取得し、その要素のクリック イベントからオブザーバブルを作成しています。

var getCust = $('#getButton').get(0);
var getCustObsvble = Rx.DOM.fromEvent(getCust, "click");

関数 fromEvent により、任意の要素の任意のイベントからオブザーバブルを作成できます。ただし、RxJS には、クリック イベントを含め、さらに多くの「一般的な」イベント用のショートカットが複数用意されています。たとえば、以下のコードを使用して、ボタンのクリック イベントからオブザーバブルを作成することもできます。

var getCustObsvble = Rx.DOM.click(getCust);

このイベントを対象に、アプリケーションの処理を単純化する優れた処理パイプラインを作成することができます。たとえば、ユーザーがボタンをすばやく連続して 2 回クリックした場合は処理しないものとします (クリックの間隔が短かすぎる操作を防ぐためにボタンを無効にすることはできません)。一連のタイムアウト コードを記述しなくても、関数 debounce をパイプラインに追加して、2 秒以上の間隔空いたクリックのみを監視するように指定すれば、この状況に対処できます。

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000);

flatMapLatest を使用しても、このイベントの処理を一部実行できます。flatMapLatest により、変換関数をパイプラインに組み込めるようになります (LINQ の SelectMany のようなものです)。その基本関数 (flatMap) が、シーケンス内の各イベントの変換を実行します。関数 flatMapLatest はこれを 1 歩先に進めたもので、非同期処理に伴う一般的な問題に対処します。つまり、flatMapLatest では、変換関数が 2 回呼び出され、1 回目の非同期要求がまだ保留中の場合、1 回目の非同期処理をキャンセルします。

flatMapLatest を使用すると、ユーザーが 2 秒より長い間隔でボタンを 2 回クリックしても、flatMapLatest が前のイベントをまだ変換中の場合は、変換中のイベントはキャンセルされます。そのため、非同期処理のキャンセルを処理するコードを記述する必要がありません。

flatMapLatest を使用するには、まず変換関数を作成します。これを行うには、前に関数の内部で示した getJSON 呼び出しをラップするだけです。この変換関数はボタン クリックと関連付けられているため、ページのデータは一切必要ありません (実際のところ、この関数はイベントの入力を無視するため、「変換」と見なされることはほとんどありません)。

以下の関数は、Web API サービスに対して要求を行い、jQuery を使ってページの要素から顧客 ID を取得しています。

function getCustomer()
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + $("custId").val());
}

RxJS を使用すると、この要求のコールバックを指定する必要はありません。呼び出しの結果は、オブザーバブルからオブザーバーに自動的に渡されます。

この変換を今回の処理チェーンに統合するには、以下のように関数への参照を渡して、オブザーバーから flatMapLatest を呼び出すだけです。

var getCustObsvble = Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);

ここで、オブザーバブルに対する処理関数をサブスクライブする必要があります。サブスクライブでは最大 3 つの関数を割り当てることができます。新しいイベントを受け取ったときに通知を処理する関数、エラー通知について報告する関数、イベント シーケンスの最後に送信される通知を処理する関数の 3 つです。今回のクリック イベント処理パイプラインは、1 つのイベントから成るシーケンスを生成するように設計しているので、「シーケンスの終了」関数を用意する必要はありません。

今回必要な 2 つの関数を割り当てるコードは以下のようになります。

var getCustObsvble.subscribe(
  c   => $("#custName").val(c.FirstName),
  err => $("Message").text("Unable to retrieve Customer: "
    + err.description)
);

この関数のセットが他の場所でも役に立つと考える (または、サブスクライブ コードをシンプルにする) 場合は、オブザーバー オブジェクトを作成します。オブザーバー オブジェクトを使用する場合は、前述と同じ関数を onNext、onError、onComplete の各プロパティに割り当てるだけです。onComplete を処理する必要がないことから、オブザーバー オブジェクトは以下のようになります。

var custObservr = {
  onNext:  c   => $("#custName").val(c.FirstName),
  onError: err => $("#Message").text("Unable to retrieve Customer: "
     + err.description)
};

オブザーバー オブジェクトが独立した状態であれば、オブザーバーをサブスクライブするためにオブザーバブルで使用するコードはもっとシンプルになります。

var getCustObsvble.subscribe(custObservr);

すべてをまとめるにはページの ready 関数が必要です。この関数で、ボタンを取得して、そのボタンが非常に洗練されたオブザーバブルに変換されるパイプラインをアタッチし、そのパイプラインに対してオブザーバーをサブスクライブします。RxJS によって使いやすいインターフェイスが実装されるため、これはわずか 2 行のコードで実現できます。つまり、1 行の jQuery コードでボタン要素を取得し、もう 1 行の RxJS コードでパイプラインを構築してオブザーバーをサブスクライブします。ただし、パイプラインを構築してサブスクライブする RxJS コードをさらに 2 行に分けることで、次回のプログラミング時に ready 関数が読みやすくなります。

最終版の ready 関数は以下のようになります。

$(function () {
  var getCust = $('#getButton').get(0);
  var getCustObsvble =
    Rx.DOM.click(getCust).debounce(2000).flatMapLatest(getCustomer);
  getCustObsvble.subscribe(custObservr);
});

おまけに、クリック イベント、Web サービスの呼び出し、データ表示が統合されたこのプロセスは非同期に実行されます。こうしたことを実現するうえで必要な細々とした厄介ごとが、RxJS によってすべて抽象化されます。

イベント シーケンスを抽象化する

プロセスが抽象化 (および豊富な演算子が提供) されるため、Rx では多くのことを同じように扱えるようになります。その結果、コードの構造を大きく変えることなく、プロセスを大幅に変更できるようになります。

たとえば、RxJS-DOM は、1 つのイベント (ボタン クリック) を含むオブザーバブルを、連続的なイベントのシーケンス (マウスの移動など) を生成するオブザーバブルと同様に扱うため、UI が大きく変わる場合でも、コードをそれほど変更する必要がありません。たとえば、ユーザーがボタンをクリックして Web サービス要求をトリガーするのを止め、ユーザーが顧客 ID を入力したら即座に顧客データを取得するように変えるとします。

最初に変更が必要なのは、監視するイベントを含む要素です。今回の場合は、ページのボタンを、顧客 ID を保持するテキスト ボックスに変更することになります (要素を保持する変数の名前も変更します)。

var getCustId = $('#custId').get(0);

このテキスト ボックスのイベントはその数を問わずオブザーバブルに変換できます。たとえば、テキスト ボックスの blur イベントを使用すると、ユーザーがテキスト ボックスから離れた時点で Web サービスの呼び出しを行うことになります。ですが、もう少し応答性を高めようと思います。そこで、ユーザーが「十分な」文字数をテキスト ボックスに入力したら即座に顧客オブジェクトを取得するようにします。つまり、keyup イベントに切り替えます。このイベントによってイベント シーケンスを生成します。つまり、キーストロークごとにシーケンスを生成します。

この変更をパイプラインに加えると以下のようになります。

var getCustObsvble = Rx.DOM.keyup(getCustId)

ユーザーが文字を入力するにつれて、変換関数が複数回呼び出されることになります。実際、ユーザーの入力速度が Customer オブジェクトを取得する速さを上回っている場合、複数の要求が累積することになります。flatMapLatest を使って累積した要求をクリーンアップすることもできますが、もっと優れた解決方法があります。 今回の顧客注文管理システムでは、顧客 ID は必ず 4 文字になるとします。つまり、テキスト ボックスに 4 文字が入力された時点以外に変換関数を呼び出しても意味がありません (また、パイプラインでは顧客 ID を取得して完全な Customer オブジェクトを返すようになっているため、変換関数は実際に「変換」を実行します)。

こうした条件を実装するには、単純に Rx の化数 filter をパイプラインに追加します。この関数 filter の動作は、LINQ の Where 句に似ています。 関数 filter は別の関数 (セレクター関数) から渡す必要があります。このセレクター関数にはテストを組み込み、最新イベントに関するデータに基づいてブール値を返します。テストに合格したイベントのみがサブスクライブされたオブザーバーに渡されます。関数 filter には、シーケンスの最新イベント オブジェクトが自動的に渡されるため、このセレクター関数のテストでは、イベント オブジェクトの target プロパティを使ってテキスト ボックスの現在値を取得し、値の長さをチェックします。

パイプラインにはもう 1 つ変更を加えます。 関数 debounce を削除します。新しい UI では、関数 debounce から得られるメリットがほとんどないためです。このように UI の操作に大幅な変更を加えても、修正後のコードは前のバージョンのコードの構造と変わらず、オブザーバブルを作成してオブザーバーをサブスクライブします。

var getCustObsvble = Rx.DOM.keyup(getCustId)
                       .filter(e => e.target.value.length == 4)
                       .flatMapLatest(getCustomer);
getCustObsvble.subscribe(custObservr);

当然、関数 cust­Observr を変更する必要はまったくありません。 関数 cust­Observr は UI 変更の影響を受けません。

必要ならばもう 1 つ変更できます。今度は、変換関数に対する変更です。変換関数には常に 3 つのパラメーターが渡されます。イベント ソースがボタンのときは、これらのパラメーターを単純に無視していました。変換関数 flatMapLatest に渡される最初のパラメーターは、監視対象イベントのイベント オブジェクトです。このイベント オブジェクトを利用すれば、顧客 ID を取得していた jQuery コードを削除できます。そのコードの代わりに、イベント オブジェクトからテキスト ボックスの値を取得します。この変更を加えると、変換関数とページの特定の要素との関連付けがなくなり、コードの独立性がさらに高くなります。

新しい変換関数は以下のようになります。

function getCustomer(e)
{
  return Rx.DOM.getJSON("api/customerorders/customerbyid/"
    + e.target.value);
}

これが Rx 抽象化のメリットです。 1 つのイベントを生成する要素を、イベント シーケンスを生成する要素に変更するのに必要なのは、パイプラインを構成するコードを 1 行修正することだけです (また、変換関数を強化するチャンスにもなります)。これまでに加えてきたすべての変更の中で、最も問題を引き起こす可能性が高いのは、入力要素を保持する変数名の変更です。こうした変更を加えてみると、LINQ と同様、利用できる演算子に詳しくなることが Rx で成功を収める秘訣であることが分かります。

Web サービス呼び出しを抽象化する

Rx の抽象化により UI の変更は非常によく似たものになりますが、優れた抽象化はバックエンド プロセスにも同様の効果をもたらします。たとえば、顧客データだけではなく、顧客の注文も取得するようにページを変更するとどうなるでしょう。 興味深くするために、Customer オブジェクトの一部として注文を取得できるナビゲーション プロパティは存在しないものとします。つまり、もう 1 回呼び出しを行う必要があります。

Rx では、まず、顧客 ID を顧客の注文コレクションに変換する 2 つ目の変換関数を作成する必要があります。

function getCustomerOrders(e) {
  return Rx.DOM.getJSON("customerorders/ordersbycustomerid/"
    + e.target.value);
}

次に、この変換を用いる 2 つ目のオブザーバブルを作成する必要があります (このコードは、顧客オブザーバブルを作成するコードと非常によく似たものになります)。

var getOrdersObsvble = Rx.DOM.keyup(getCustId)
       .filter(e => e.target.value.length == 4)
       .flatMapLatest(getCustomerOrders);

ここまで読むと、これらのオブザーバブルを組み合わせて調整する作業は大変なことになると考えているのではないでしょうか。しかし、Rx ではすべてのオブザーバブルは非常によく似たものになるため、複数のオブザーバブルの出力を、1 つの (比較的シンプルな) オブザーバーが処理する 1 つのシーケンスにまとめることができます。

たとえば、複数のソースから注文を取得するオブザーバブルがいくつかある場合、Rx の関数 merge を使用してすべての注文を 1 つのシーケンスに結合し、このシーケンスに対して 1 つのオブザーバーをサブスクライブします。たとえば、以下のコードは、現在の注文を取得するオブザーバブル (getCurrentOrdersObsvble) と発行済みの注文を取得するオブザーバブル (getPostedOrdersObsvle) をまとめ、結果として得られるシーケンスを allOrdersObservr という 1 つのオブザーバーに渡しています。

Rx.Observable.merge(getCurrentOrdersObsvble, getPostedOrdersObsvble)
             .subscribe(allOrdersObservr);

ここで、さらに課題を増やします。Customer オブジェクトを取得するオブザーバブルと、顧客のすべての注文を取得するオブザーバブルを組み合わせてみましょう。さいわい、RxJS には combineLatest という、これを目的とする演算子があります。演算子 combineLatest は、2 つのオブザーバブルと 1 つの処理関数を受け取ります。combineLatest に渡す処理関数には、各シーケンスの最新結果が渡されます。この関数には、結果の結合方法を指定できます。今回の例の combineLatest には必要以上の機能が備わっています。それは各オブザーバブルから取得できる結果が 1 つしかないためです。つまり、一方のオブザーバブルからは Customer オブジェクトを取得し、もう一方のオブザーバブルからはすべての顧客の注文を取得します。

以下のコードでは、最初のオブザーバブルの Customer オブジェクトに新しいプロパティ (Orders) を追加し、2 番目のオブザーバブルの結果をそのプロパティに渡しています。最後に、CustOrdersObsrvr というオブザーバーをサブスクライブし、新しい Customer と Orders を組み合わせたオブジェクトを処理します。

Rx.Observable.combineLatest(getCustObsvble, getOrdersObsvble,
                           (c, ords) => {c.Orders = ord; return c;})
             .subscribe(CustOrdersObsrvr);

custOrdersObservr は、作成したこの新しいオブジェクトと以下のように連携できます。

var custOrdersObservr = {
  onNext: co => {
    // ...Code to update the page with customer and orders data...               
  },
  onError: err => $("#Message").text("Unable to retrieve Customer and Orders: "
    + err.description)
};

まとめ

アプリケーションのコンポーネントをわずか 2 種類のオブジェクト (オブザーバブルとオブザーバー) に抽象化し、オブザーバブルの結果を管理および変換するための豊富な演算子を提供することで、RxJS は非常に柔軟性の高い (そのうえメンテナンスが容易な)方法で Web アプリケーションを作成できるようにします。

もちろん、複雑なプロセスをシンプルなコードに抽象化する強力なライブラリを使用することには危険が付きものです。 アプリケーションで予期していなかった動作が行われた場合、抽象化によって取り除かれている個別のコード行がわからないため、問題に対するデバッグ作業が悪夢になる可能性があります。ただし、そうしたコードをすべて記述する必要がなくなるのも確かです。(おそらくですが) 抽象化層のバグは自分でコードを記述する場合よりも少なくなります。これは、強力さと可視性との間でのお決まりのトレードオフです。

RxJS に有利な評価を下すことになりますが、コードをそれほど変えずにアプリケーションを大幅に変更できることは、開発者にとって間違いなくメリットになります。


Peter Vogel は PH&V Information Services のシステム設計者兼社長です。PH&V は、UX 設計からオブジェクト モデリングやデータベース設計まで、包括的なコンサルティングを提供しています。連絡先は peter.vogel@phvis.com.(英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Stephen Cleary、James McCaffrey、および Dave Sexton に心より感謝いたします。
Stephen Cleary は、マルチスレッドと非同期プログラミングに 16 年間取り組み、最初の Community Technology Preview から Microsoft .NET Framework の非同期サポートを使ってきました。著書に、『Concurrency in C# Cookbook』(Oreilly & Associates、2014 年) があります。彼のホーム ページとブログは、stephencleary.com (英語) から利用できます。

Dr.James McCaffrey は、ワシントン州レドモンドの Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。Dr.McCaffrey の連絡先は jammc@microsoft.com (英語のみ) です。