RxJSで検索バーを構築する方法

_作成者はhttps://www.brightfunds.org/organizations/mozilla-foundation[Mozilla Foundation]を選択して、https://do.co/w4do-cta [Write for DOnations]プログラムの一環として寄付を受け取りました。

前書き

https://en.wikipedia.org/wiki/Reactive_programming [リアクティブプログラミング]は、_非同期データストリーム_に関するパラダイムです。プログラミングモデルでは、すべてが時間の経過とともに広がるデータのストリームであると見なされます。 これには、キーストローク、HTTPリクエスト、印刷されるファイル、さらには非常に短い間隔で時間を計られているとみなされる配列の要素が含まれます。 言語では非同期データが一般的であるため、JavaScriptに最適です。

RxJSは、https://www.javascript.com/ [JavaScript]のリアクティブプログラミング用の一般的なライブラリです。 ReactiveXは、RxJSの傘下にあり、https://www.java.com [Java]、https://www.python.org/のような他の多くの言語で拡張されています。 [Python]、https://isocpp.org/ [C ++]、https://developer.apple.com/swift/ [Swift]、およびhttps://www.dartlang.org/[Dart]。 RxJSは、AngularやReactなどのライブラリでも広く使用されています。

RxJSの実装は、一定の期間にわたってデータを処理し、認識できる連鎖機能に基づいています。 これは、引数とコールバックのリストを受け取る関数以外の何ものでもなく、RxJSのほぼすべての側面を実装し、そうするように指示されたときにそれらを実行できることを意味します。 RxJSを取り巻くコミュニティはこの重労働を行っており、その結果、クリーンで保守可能なコードを記述するために任意のアプリケーションで直接使用できるAPIが得られました。

このチュートリアルでは、RxJSを使用して、リアルタイムの結果をユーザーに返す機能豊富な検索バーを構築します。 また、HTMLとCSSを使用して、検索バーをフォーマットします。 最終結果は次のようになります。

画像:https://assets.digitalocean.com/articles/CART-62307/introduction_search.gif [検索バーのデモ]

検索バーと同じくらい一般的で一見シンプルなものには、さまざまなチェックが必要です。 このチュートリアルでは、RxJSがかなり複雑な一連の要件を管理しやすく理解しやすいコードに変換する方法を示します。

前提条件

このチュートリアルを始める前に、次のものが必要です。

  • http://atom.io [Atom]、https://code.visualstudio.com/ [Visual Studio Code]、https://www.sublimetext.com/ [などのJavaScript構文の強調表示をサポートするテキストエディター崇高なテキスト]。 これらのエディターは、Windows、macOS、およびLinuxで使用できます。

  • HTMLとJavaScriptを一緒に使用することに関する知識。 詳細については、https://www.digitalocean.com/community/tutorials/how-to-add-javascript-to-html [JavaScriptをHTMLに追加する方法]をご覧ください。

  • JSONデータ形式に精通していること。https://www.digitalocean.com/community/tutorials/how-to-work-with-json-in-javascript [JavaScriptでJSONを使用する方法]で詳細を確認できます。 。

チュートリアルの完全なコードは、https://github.com/do-community/RxJS-Search-Bar [Github]で入手できます。

ステップ1-検索バーの作成とスタイル設定

このステップでは、HTMLとCSSを使用して検索バーを作成およびスタイルします。 コードはhttps://getbootstrap.com/[Bootstrap]のいくつかの一般的な要素を使用して、ページの構造化とスタイル設定のプロセスを高速化し、カスタム要素の追加に集中できるようにします。 _Bootstrap_は、タイポグラフィ、フォーム、ボタン、ナビゲーション、グリッド、その他のインターフェイスコンポーネントなどの一般的な要素のテンプレートを含むCSSフレームワークです。 また、アプリケーションはhttps://daneden.github.io/animate.css/[Animate.css]を使用して、検索バーにアニメーションを追加します。

まず、 `+ nano `またはお気に入りのテキストエディターで ` search-bar.html +`という名前のファイルを作成することから始めます。

nano search-bar.html

次に、アプリケーションの基本構造を作成します。 次のHTMLを新しいファイルに追加します。

search-bar.html

<!DOCTYPE html>
<html>

 <head>
   <title>RxJS Tutorial</title>
   <!-- Load CSS -->

   <!-- Load Rubik font -->

   <!-- Add Custom inline CSS -->

 </head>

 <body>
     <!-- Content -->

     <!-- Page Header and Search Bar -->

     <!-- Results -->

     <!-- Load External RxJS -->

     <!-- Add custom inline JavaScript -->
     <script>

     </script>
 </body>

</html>

Bootstrapライブラリ全体のCSSが必要なため、BootstrapおよびAnimate.cssのCSSをロードしてください。

`+ Load CSS`コメントの下に次のコードを追加します。

search-bar.html

...
<!-- Load CSS -->


...

このチュートリアルでは、https://fonts.google.com/ [Google Fonts]ライブラリのhttps://fonts.google.com/specimen/Rubik[Rubik]というカスタムフォントを使用して、検索バーのスタイルを設定します。 `+ Load Rubik font +`コメントの下に強調表示されたコードを追加して、フォントをロードします。

search-bar.html

...
<!-- Load Rubik font -->

...

次に、 `+ Add Custom inline CSS`コメントの下のページにカスタムCSSを追加します。 これにより、ページの見出し、検索バー、および結果が読みやすく、使いやすくなります。

search-bar.html

...
<!-- Add Custom inline CSS -->














































...

すべてのスタイルが揃ったので、 `+ Page Header and Search Bar +`コメントの下にヘッダーと入力バーを定義するHTMLを追加します。

search-bar.html

...
<!-- Content -->
<!-- Page Header and Search Bar -->

















...

これは、Bootstrapのグリッドシステムを使用して、ページヘッダーと検索バーを構成します。 検索バーに「+ search-input +」識別子を割り当てました。これは、チュートリアルの後半でリスナーにバインドするために使用します。

次に、検索結果を表示する場所を作成します。 「+ Results Of」コマンドの下で、「+ response-list 」識別子を使用して「 div」を作成し、チュートリアルの後半で結果を追加します。

search-bar.html

...
<!-- Results -->







...

この時点で、 `+ search-bar.html`ファイルは次のようになります。

search-bar.html

<!DOCTYPE html>
<html>

 <head>
   <title>RxJS Tutorial</title>
   <!-- Load CSS -->
   <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.2.1/css/bootstrap.min.css" integrity="sha384-GJzZqFGwb1QTTN6wy59ffF1BuGJpLSa9DkKMp0DgiMDm4iYMj70gZWKYbI706tWS" crossorigin="anonymous">
   <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.7.0/animate.min.css" />

   <!-- Load Rubik font -->
   <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet">

   <!-- Add Custom inline CSS -->
   <style>
     body {
       background-color: #f5f5f5;
       font-family: "Rubik", sans-serif;
     }

     .search-container {
       margin-top: 50px;
     }
     .search-container .search-heading {
       display: block;
       margin-bottom: 50px;
     }
     .search-container input,
     .search-container input:focus {
       padding: 16px 16px 16px;
       border: none;
       background: rgb(255, 255, 255);
       box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1) !important;
     }

     .results-container {
       margin-top: 50px;
     }
     .results-container .list-group .list-group-item {
       background-color: transparent;
       border-top: none !important;
       border-bottom: 1px solid rgba(236, 229, 229, 0.64);
     }

     .float-bottom-right {
       position: fixed;
       bottom: 20px;
       left: 20px;
       font-size: 20px;
       font-weight: 700;
       z-index: 1000;
     }
     .float-bottom-right .info-container .card {
       display: none;
     }
     .float-bottom-right .info-container:hover .card,
     .float-bottom-right .info-container .card:hover {
       display: block;
     }
   </style>
 </head>

 <body>
     <!-- Content -->
     <!-- Page Header and Search Bar -->
     <div class="container search-container">
       <div class="row justify-content-center">
         <div class="col-md-auto">
           <div class="search-heading">
             <h2>Search for Materials Published by Author Name</h2>
             <p class="text-right">powered by <a href="https://www.crossref.org/">Crossref</a></p>
           </div>
         </div>
       </div>
       <div class="row justify-content-center">
         <div class="col-sm-8">
           <div class="input-group input-group-md">
             <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus>
           </div>
         </div>
       </div>
     </div>

     <!-- Results -->
     <div class="container results-container">
       <div class="row justify-content-center">
         <div class="col-sm-8">
           <ul id="response-list" class="list-group list-group-flush"></ul>
         </div>
       </div>
     </div>

     <!-- Load RxJS -->

     <!-- Add custom inline JavaScript -->
     <script>

     </script>
 </body>

</html>

このステップでは、HTMLとCSSを使用して検索バーの基本構造をレイアウトしました。 次のステップでは、検索語を受け入れて結果を返すJavaScript関数を作成します。

ステップ2-JavaScriptの記述

検索バーがフォーマットされたので、このチュートリアルの後半で作成するRxJSコードの基盤として機能するJavaScriptコードを作成する準備ができました。 このコードはRxJSと連携して、検索語を受け入れ、結果を返します。

このチュートリアルでBootstrapとJavaScriptが提供する機能は必要ないので、それらをロードするつもりはありません。 ただし、RxJSを使用します。 `+ Load RxJS +`コメントの下に以下を追加して、RxJSライブラリをロードします。

search-bar.html

...
<!-- Load RxJS -->

...

これで、結果が追加されるHTMLからの + div +`の参照を保存します。 `+ Add custom inline JavaScript`コメントの下の + <script> + `タグにハイライトJavaScriptコードを追加します。

search-bar.html

...
<!-- Add custom inline JavaScript -->
<script>


</script>
...

次に、APIからのJSON応答をページに表示するHTML要素に変換するコードを追加します。 このコードは、最初に検索バーのコンテンツをクリアしてから、検索結果アニメーションの遅延を設定します。

`+ <script> +`タグの間に強調表示された関数を追加します。

search-bar.html

...
<!-- Add custom inline JavaScript -->
<script>
   const output = document.getElementById("response-list");












<^>                <h5 class="mb-1">${(item['title'] && item['title'][0]) || "&lt;Title not available&gt;"}</h5>
















</script>
...

`+ if `で始まるコードブロックは、検索結果をチェックする条件付きループであり、結果が見つからなかった場合はメッセージを表示します。 結果が見つかった場合、 ` forEach +`ループはユーザーにアニメーションで結果を提供します。

このステップでは、結果を受け入れてページ上に返すことができる関数を作成して、RxJSのベースをレイアウトしました。 次のステップでは、検索バーを機能的にします。

手順3-リスナーの設定

RxJSはデータストリームに関係しています。データストリームは、このプロジェクトでは、ユーザーが入力要素または検索バーに入力する一連の文字です。 このステップでは、更新をリッスンするリスナーをinput要素に追加します。

最初に、チュートリアルの前半で追加した `+ search-input +`識別子に注意してください:

search-bar.html

...
<input  type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus>
...

次に、 + search-input`要素への参照を保持する変数を作成します。 これは、コードが入力イベントをリッスンするために使用する_ + Observable + `_になります。 `+ Observables `は、 ` Observer +`がリッスンする将来の値またはイベントのコレクションであり、_callback functions_とも呼ばれます。

前のステップのJavaScriptの下の `+ <script> +`タグに強調表示された行を追加します。

search-bar.html

...
     output.insertAdjacentHTML("beforeend", resultItem);
     animationDelay += 0.1;

   });
 }
}



...

参照入力に変数を追加したら、 `+ fromEvent `演算子を使用してイベントをリッスンします。 これにより、特定の種類のイベントの_DOM_または** Document ** Object ** M ** odel要素にリスナーが追加されます。 DOM要素は、ページ上の「 html」、「+ body」、「+ div」、または「+ img」要素です。 この場合、DOM要素は検索バーです。

+ searchInput +`変数の下に強調表示された次の行を追加して、パラメーターを `+ fromEvent +`に渡します。 `+ searchInput + DOM要素が最初のパラメーターです。 この後に、2番目のパラメーターとして「+ input +」イベントが続きます。これは、コードがリッスンするイベントタイプです。

search-bar.html

...
     let searchInput = document.getElementById("search-input");

...

リスナが設定されたので、入力要素で更新が行われるたびにコードが通知を受け取ります。 次のステップでは、演算子を使用してこのようなイベントに対してアクションを実行します。

ステップ4-オペレーターの追加

`+ Operators `は、データに対して操作を実行する1つのタスクを持つ純粋な関数です。 このステップでは、演算子を使用して、 ` input +`パラメータのバッファリング、HTTPリクエストの作成、結果のフィルタリングなどのさまざまなタスクを実行します。

まず、ユーザーがクエリを入力すると、結果がリアルタイムで更新されることを確認します。 これを実現するには、前のステップのDOM入力イベントを使用します。 DOM入力イベントにはさまざまな詳細が含まれていますが、このチュートリアルでは、ターゲット要素に入力される値に関心があります。 次のコードを追加して、 `+ pluck +`演算子を使用してオブジェクトを取得し、指定されたキーで値を返します。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')

...

イベントが必要な形式になったので、検索語の最小値を3文字に設定します。 多くの場合、3文字未満では関連する結果が得られないか、ユーザーがまだ入力中です。

最小値を設定するには、 `+ filter `演算子を使用します。 指定された条件を満たす場合、データをストリームのさらに下に渡します。 長さ条件を「+2」より大きく設定して、少なくとも3文字を必要とします。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')

...

また、APIサーバーの負荷を軽減するために、リクエストが500ミリ秒間隔でのみ送信されるようにします。 これを行うには、 `+ debounceTime `演算子を使用して、ストリームを通過する各イベント間の最小指定間隔を維持します。 ハイライトされたコードを ` filter +`演算子の下に追加します:

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)

...

また、最後のAPI呼び出し以降に変更がなかった場合、アプリケーションは検索語を無視する必要があります。 これにより、送信されるAPI呼び出しの数がさらに減り、アプリケーションが最適化されます。

例として、ユーザーは「スーパーカー」と入力し、最後の文字を削除して(「スーパーカー」という用語を作成)、削除した文字を追加して「スーパーカー」に戻します。 その結果、用語は変更されなかったため、検索結果は変更されません。 このような場合、操作を実行しないことは理にかなっています。

これを設定するには、 `+ distinctUntilChanged +`演算子を使用します。 この演算子は、ストリームを介して渡された以前のデータを記憶し、異なる場合にのみ別のデータを渡します。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)
       .debounceTime(500)

...

ユーザーからの入力を規制したので、検索語でAPIを照会するコードを追加します。 これを行うには、_AJAX_のRxJS実装を使用します。 AJAXは、ロードされたページのバックグラウンドで非同期的にAPI呼び出しを行います。 AJAXを使用すると、新しい検索語の結果でページをリロードすることを回避でき、サーバーからデータを取得してページの結果を更新することもできます。

次に、 `+ switchMap `を使用してAJAXをアプリケーションにチェーンするコードを追加します。 また、「 map 」を使用して、入力を出力にマップします。 このコードは、渡された関数を ` Observable +`によって発行されたすべてのアイテムに適用します。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)
       .debounceTime(500)
       .distinctUntilChanged()








...

このコードは、API応答を3つの部分に分割します。

  • + status +:APIサーバーによって返されるHTTPステータスコード。 このコードは、「+ 200+」または成功した応答のみを受け入れます。

  • + details +:受信した実際の応答データ。 これには、クエリされた検索語の結果が含まれます。

  • + result_hash +:APIサーバーから返される応答のハッシュ値。このチュートリアルでは、UNIXタイムスタンプを使用します。 これは、結果が変わると変わる結果のハッシュです。 一意のハッシュ値により、アプリケーションは、結果が変更されており、更新する必要があるかどうかを判断できます。

システムに障害が発生し、エラーを処理するためのコードを準備する必要があります。 API呼び出しで発生する可能性のあるエラーを処理するには、成功した応答のみを受け入れるために `+ filter +`演算子を使用します。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)
       .debounceTime(500)
       .distinctUntilChanged()
       .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
         .map(resp => ({
             "status" : resp["status"] == 200,
             "details" : resp["status"] == 200 ? resp["response"] : [],
             "result_hash": Date.now()
           })
         )
       )

...

次に、応答で変更が検出された場合にのみDOMを更新するコードを追加します。 DOM更新はリソースを大量に消費する操作になる可能性があるため、更新の数を減らすとアプリケーションにプラスの影響があります。 `+ result_hash +`は応答が変更されたときにのみ変更されるため、この機能を実装するために使用します。

これを行うには、前と同じように `+ distinctUntilChanged +`演算子を使用します。 コードはこれを使用して、キーが変更されたときにのみユーザー入力を受け入れます。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)
       .debounceTime(500)
       .distinctUntilChanged()
       .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
         .map(resp => ({
             "status" : resp["status"] == 200,
             "details" : resp["status"] == 200 ? resp["response"] : [],
             "result_hash": Date.now()
           })
         )
       )
       .filter(resp => resp.status !== false)

...

以前に `+ distinctUntilChanged +`演算子を使用してデータ全体が変更されたかどうかを確認しましたが、この例では、応答で更新されたキーを確認します。 単一のキーの変更を識別することと比較すると、応答全体を比較することはリソースのコストがかかります。 キーハッシュは応答全体を表しているため、応答の変更を識別するために自信を持って使用できます。

この関数は、前に見た値と新しい値の2つのオブジェクトを受け入れます。 これらの2つのオブジェクトからハッシュをチェックし、これらの2つの値が一致する場合は「+ True +」を返します。その場合、データはフィルターで除外され、パイプラインでそれ以上渡されません。

このステップでは、ユーザーが入力した検索語を受け取り、それに対してさまざまなチェックを実行するパイプラインを作成しました。 チェックが完了すると、API呼び出しを行い、結果をユーザーに表示する形式で応答を返します。 必要に応じてAPI呼び出しを制限することにより、クライアント側とサーバー側の両方でリソース使用量を最適化しました。 次の手順では、入力要素でリッスンを開始するようにアプリケーションを構成し、結果をページにレンダリングする関数に渡します。

ステップ5-サブスクリプションですべてをアクティブ化する

`+ subscribe `は、リンクの最後の演算子であり、オブザーバーが ` Observable +`によって発行されたデータイベントを確認できるようにします。 次の3つのメソッドを実装します。

  • + onNext +:これは、イベントが受信されたときの処理を指定します。

  • + onError +:これはエラーの処理を担当します。 このメソッドが呼び出されると、 `+ onNext `と ` onCompleted +`の呼び出しは行われません。

  • + onCompleted +:このメソッドは、 `+ onNext +`が最後に呼び出されたときに呼び出されます。 パイプラインで渡されるデータはこれ以上ありません。

サブスクライバーのこのシグネチャは、_lazy execution_を実現するためのものです。これは、「+ Observable 」パイプラインを定義し、サブスクライブしたときにのみ動作するようにする機能です。 コードではこの例を使用しませんが、次の例では、「 Observable +」をサブスクライブする方法を示しています。

次に、 `+ Observable +`をサブスクライブし、UIでのレンダリングを担当するメソッドにデータをルーティングします。

search-bar.html

...
     let searchInput = document.getElementById("search-input");
     Rx.Observable.fromEvent(searchInput, 'input')
       .pluck('target', 'value')
       .filter(searchTerm => searchTerm.length > 2)
       .debounceTime(500)
       .distinctUntilChanged()
       .switchMap(searchKey => Rx.Observable.ajax(`https://api.crossref.org/works?rows=50&query.author=${searchKey}`)
         .map(resp => ({
             "status" : resp["status"] == 200,
             "details" : resp["status"] == 200 ? resp["response"] : [],
             "result_hash": Date.now()
           })
         )
       )
       .filter(resp => resp.status !== false)
       .distinctUntilChanged((a, b) => a.result_hash === b.result_hash)

...

これらの変更を行った後、ファイルを保存して閉じます。

コードの記述が完了したので、検索バーを表示してテストする準備ができました。 `+ search-bar.html`ファイルをダブルクリックして、Webブラウザーで開きます。 コードが正しく入力されていれば、検索バーが表示されます。

image:https://assets.digitalocean.com/articles/CART-62307/completed_searchbar.png [完成した検索バー]

検索バーにコンテンツを入力してテストします。

image:https://assets.digitalocean.com/articles/CART-62307/test_searchbar.gif [検索バーに入力されるコンテンツのgif。2文字では結果が返されないことを示しています。]

このステップでは、 `+ Observable +`にサブスクライブして、コードをアクティブにします。 これで、定型化され機能する検索バーアプリケーションができました。

結論

このチュートリアルでは、ユーザーにリアルタイムの結果を提供する、RxJS、CSS、およびHTMLを備えた機能豊富な検索バーを作成しました。 検索バーには最低3文字が必要で、自動的に更新され、クライアントとAPIサーバーの両方に最適化されています。

複雑な一連の要件と考えられるものは、18行のRxJSコードで作成されました。 このコードは読みやすいだけでなく、スタンドアロンのJavaScript実装よりもはるかにクリーンです。 これは、将来、コードの理解、更新、および保守が容易になることを意味します。

RxJSの使用の詳細については、https://rxjs-dev.firebaseapp.com/api [公式APIドキュメント]をご覧ください。