RxJSで検索バーを作成する方法
著者は、 Write for DOnations プログラムの一環として、 MozillaFoundationを選択して寄付を受け取りました。
序章
リアクティブプログラミングは、非同期データストリームに関連するパラダイムであり、プログラミングモデルはすべてを時間の経過とともに広がるデータストリームと見なします。 これには、キーストローク、HTTPリクエスト、印刷されるファイル、さらには配列の要素も含まれます。これらは、非常に短い間隔でタイミングが取られていると見なすことができます。 非同期データは言語で一般的であるため、これはJavaScriptに完全に適合します。
RxJS は、JavaScriptのリアクティブプログラミングで人気のあるライブラリです。 RxJSの傘であるReactiveXは、 Java 、 Python 、 C ++ 、などの他の多くの言語で拡張されています。 Swift 、およびDart。 RxJSは、AngularやReactなどのライブラリでも広く使用されています。
RxJSの実装は、一定期間のデータを認識して処理できる連鎖関数に基づいています。 これは、引数とコールバックのリストを受け取る関数だけでRxJSのほぼすべての側面を実装し、そうするように指示されたときにそれらを実行できることを意味します。 RxJSを取り巻くコミュニティはこの手間のかかる作業を行っており、その結果、あらゆるアプリケーションで直接使用して、クリーンで保守可能なコードを記述できるAPIが作成されています。
このチュートリアルでは、RxJSを使用して、リアルタイムの結果をユーザーに返す機能豊富な検索バーを作成します。 また、HTMLとCSSを使用して検索バーをフォーマットします。 最終結果は次のようになります。
検索バーのように一般的で一見単純に見えるものには、さまざまなチェックを行う必要があります。 このチュートリアルでは、RxJSが、かなり複雑な一連の要件を、管理しやすく理解しやすいコードに変換する方法を示します。
前提条件
このチュートリアルを開始する前に、次のものが必要です。
- Atom 、 Visual Studio Code 、 SublimeTextなどのJavaScript構文の強調表示をサポートするテキストエディター。 これらのエディターは、Windows、macOS、およびLinuxで使用できます。
- HTMLとJavaScriptを一緒に使用することに精通していること。 詳細については、JavaScriptをHTMLに追加する方法をご覧ください。
- JavaScriptでJSONを操作する方法で詳細を学ぶことができる、JSONデータ形式に精通していること。
チュートリアルの完全なコードは、Githubで入手できます。
ステップ1—検索バーの作成とスタイリング
このステップでは、HTMLとCSSを使用して検索バーを作成し、スタイルを設定します。 このコードは、 Bootstrap のいくつかの一般的な要素を使用して、ページの構造化とスタイル設定のプロセスを高速化し、カスタム要素の追加に集中できるようにします。 Bootstrap は、タイポグラフィ、フォーム、ボタン、ナビゲーション、グリッド、その他のインターフェイスコンポーネントなどの一般的な要素のテンプレートを含む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 --> <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" /> ...
このチュートリアルでは、 GoogleFontsライブラリのRubikというカスタムフォントを使用して、検索バーのスタイルを設定します。 Load Rubik font
コメントの下に強調表示されたコードを追加して、フォントをロードします。
search-bar.html
... <!-- Load Rubik font --> <link href="https://fonts.googleapis.com/css?family=Rubik" rel="stylesheet"> ...
次に、Add Custom inline CSS
コメントの下のページにカスタムCSSを追加します。 これにより、ページの見出し、検索バー、および結果が読みやすく、使いやすくなります。
search-bar.html
... <!-- 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> ...
すべてのスタイルが配置されたので、Page Header and Search Bar
コメントの下にヘッダーと入力バーを定義するHTMLを追加します。
search-bar.html
... <!-- 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> ...
これは、Bootstrapのグリッドシステムを使用して、ページヘッダーと検索バーを構成します。 search-input
識別子を検索バーに割り当てました。これは、チュートリアルの後半でリスナーにバインドするために使用します。
次に、検索結果を表示する場所を作成します。 Results
コメントの下に、response-list
識別子を使用してdiv
を作成し、チュートリアルの後半で結果を追加します。
search-bar.html
... <!-- 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> ...
この時点で、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 --> <script src="https://unpkg.com/@reactivex/[email protected]/dist/global/Rx.js"></script> ...
次に、結果が追加されるHTMLからのdiv
の参照を保存します。 Add custom inline JavaScript
コメントの下の<script>
タグに強調表示されたJavaScriptコードを追加します。
search-bar.html
... <!-- Add custom inline JavaScript --> <script> const output = document.getElementById("response-list"); </script> ...
次に、APIからのJSON応答をページに表示するHTML要素に変換するコードを追加します。 このコードは、最初に検索バーの内容をクリアしてから、検索結果のアニメーションの遅延を設定します。
<script>
タグの間に強調表示された関数を追加します。
search-bar.html
... <!-- Add custom inline JavaScript --> <script> const output = document.getElementById("response-list"); function showResults(resp) { var items = resp['message']['items'] output.innerHTML = ""; animationDelay = 0; if (items.length == 0) { output.innerHTML = "Could not find any :("; } else { items.forEach(item => { resultItem = ` <div class="list-group-item animated fadeInUp" style="animation-delay: ${animationDelay}s;"> <div class="d-flex w-100 justify-content-between"> <^> <h5 class="mb-1">${(item['title'] && item['title'][0]) || "<Title not available>"}</h5> </div> <p class="mb-1">${(item['container-title'] && item['container-title'][0]) || ""}</p> <small class="text-muted"><a href="${item['URL']}" target="_blank">${item['URL']}</a></small> <div> <p class="badge badge-primary badge-pill">${item['publisher'] || ''}</p> <p class="badge badge-primary badge-pill">${item['type'] || ''}</p> </div> </div> `; output.insertAdjacentHTML("beforeend", resultItem); animationDelay += 0.1; }); } } </script> ...
if
で始まるコードブロックは、検索結果をチェックし、結果が見つからなかった場合にメッセージを表示する条件付きループです。 結果が見つかった場合、forEach
ループは結果にアニメーションをユーザーに提供します。
このステップでは、結果を受け入れてページに返すことができる関数を記述して、RxJSのベースをレイアウトしました。 次のステップでは、検索バーを機能させます。
ステップ3—リスナーを設定する
RxJSは、データストリームに関係しています。このプロジェクトでは、ユーザーが入力要素または検索バーに入力する一連の文字です。 このステップでは、入力要素にリスナーを追加して、更新をリッスンします。
まず、チュートリアルの前半で追加したsearch-input
識別子に注意してください。
search-bar.html
... <input id="search-input" type="text" class="form-control" placeholder="eg. Richard" aria-label="eg. Richard" autofocus> ...
次に、search-input
要素の参照を保持する変数を作成します。 これは、コードが入力イベントをリッスンするために使用するObservableになります。 Observables
は、Observer
がリッスンする将来の値またはイベントのコレクションであり、コールバック関数とも呼ばれます。
前の手順のJavaScriptの下にある<script>
タグに強調表示された行を追加します。
search-bar.html
... output.insertAdjacentHTML("beforeend", resultItem); animationDelay += 0.1; }); } } let searchInput = document.getElementById("search-input"); ...
入力を参照する変数を追加したので、fromEvent
演算子を使用してイベントをリッスンします。 これにより、特定の種類のイベントの要素であるDOMまたはD ocument O bject Modelにリスナーが追加されます。 DOM要素は、ページ上のhtml
、body
、div
、またはimg
要素である可能性があります。 この場合、DOM要素は検索バーです。
searchInput
変数の下に次の強調表示された行を追加して、パラメーターをfromEvent
に渡します。 searchInput
DOM要素が最初のパラメーターです。 この後に、2番目のパラメーターとしてinput
イベントが続きます。これは、コードがリッスンするイベントタイプです。
search-bar.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') ...
リスナーが設定されたので、入力要素で更新が行われるたびにコードは通知を受け取ります。 次のステップでは、演算子を使用してそのようなイベントに対してアクションを実行します。
ステップ4—演算子を追加する
Operators
は、データに対して操作を実行するという1つのタスクを持つ純粋関数です。 このステップでは、オペレーターを使用して、input
パラメーターのバッファー、HTTP要求の作成、結果のフィルター処理などのさまざまなタスクを実行します。
まず、ユーザーがクエリを入力すると、結果がリアルタイムで更新されることを確認します。 これを実現するには、前の手順のDOM入力イベントを使用します。 DOM入力イベントにはさまざまな詳細が含まれていますが、このチュートリアルでは、ターゲット要素に入力された値に関心があります。 次のコードを追加して、pluck
演算子を使用してオブジェクトを取得し、指定されたキーの値を返します。
search-bar.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') ...
イベントが必要な形式になっているので、検索語の最小値を3文字に設定します。 多くの場合、3文字未満では関連する結果が得られないか、ユーザーがまだ入力中である可能性があります。
filter
演算子を使用して最小値を設定します。 指定された条件を満たす場合、データはさらに下流に渡されます。 長さ条件を2
より大きく設定して、少なくとも3文字を必要とします。
search-bar.html
... let searchInput = document.getElementById("search-input"); Rx.Observable.fromEvent(searchInput, 'input') .pluck('target', 'value') .filter(searchTerm => searchTerm.length > 2) ...
また、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) .debounceTime(500) ...
最後のAPI呼び出し以降に変更がない場合、アプリケーションは検索語も無視する必要があります。 これにより、送信されるAPI呼び出しの数がさらに減り、アプリケーションが最適化されます。
たとえば、ユーザーはsuper cars
と入力し、最後の文字を削除して(super car
という用語を作成)、削除した文字を追加して、用語をsuper cars
に戻すことができます。 。 その結果、用語は変更されなかったため、検索結果は変更されません。 このような場合、操作を実行しないことは理にかなっています。
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() ...
ユーザーからの入力を調整したので、検索語を使用して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() .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() }) ) ) ...
このコードは、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() }) ) ) .filter(resp => resp.status !== false) ...
次に、応答で変更が検出された場合にのみ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((a, b) => a.result_hash === b.result_hash) ...
以前はdistinctUntilChanged
演算子を使用してデータ全体が変更されたかどうかを確認しましたが、この場合は、応答で更新されたキーを確認します。 応答全体を比較することは、単一のキーの変更を識別することと比較すると、リソースコストがかかります。 キーハッシュは応答全体を表すため、応答の変更を識別するために自信を持って使用できます。
この関数は、以前に見た値と新しい値の2つのオブジェクトを受け入れます。 これらの2つのオブジェクトからハッシュをチェックし、これら2つの値が一致すると、True
を返します。この場合、データはフィルターで除外され、パイプラインでそれ以上渡されません。
このステップでは、ユーザーが入力した検索語を受け取り、それに対してさまざまなチェックを実行するパイプラインを作成しました。 チェックが完了すると、API呼び出しが行われ、結果をユーザーに表示する形式で応答が返されます。 必要に応じてAPI呼び出しを制限することにより、クライアント側とサーバー側の両方でリソースの使用を最適化しました。 次のステップでは、入力要素のリッスンを開始するようにアプリケーションを構成し、その結果をページにレンダリングする関数に渡します。
ステップ5—サブスクリプションですべてをアクティブ化する
subscribe
は、Observable
によって発行されたデータイベントをオブザーバーが確認できるようにするリンクの最後の演算子です。 次の3つのメソッドを実装します。
onNext
:イベントを受信したときに何をするかを指定します。onError
:これはエラーの処理を担当します。onNext
およびonCompleted
の呼び出しは、このメソッドが呼び出されると行われません。onCompleted
:このメソッドは、onNext
が最後に呼び出されたときに呼び出されます。 パイプラインで渡されるデータはこれ以上ありません。
サブスクライバーのこの署名により、レイジー実行を実現できます。これは、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) .subscribe(resp => showResults(resp.details)); ...
これらの変更を行った後、ファイルを保存して閉じます。
コードの記述が完了したので、検索バーを表示してテストする準備が整いました。 search-bar.html
ファイルをダブルクリックして、Webブラウザで開きます。 コードが正しく入力されている場合は、検索バーが表示されます。
検索バーにコンテンツを入力してテストします。
このステップでは、Observable
にサブスクライブしてコードをアクティブ化しました。 これで、定型化され機能する検索バーアプリケーションができました。
結論
このチュートリアルでは、ユーザーにリアルタイムの結果を提供するRxJS、CSS、およびHTMLを使用して機能豊富な検索バーを作成しました。 検索バーには最低3文字が必要で、自動的に更新され、クライアントとAPIサーバーの両方に最適化されています。
複雑な要件のセットと見なすことができるものは、18行のRxJSコードで作成されました。 このコードは読みやすいだけでなく、スタンドアロンのJavaScript実装よりもはるかにクリーンです。 これは、コードが将来的に理解、更新、および保守しやすくなることを意味します。
RxJSの使用の詳細については、公式APIドキュメントをご覧ください。