サーバからのデータ取得
モダンな Web サイトやアプリケーションでしょっちゅう必要になる仕事は、サーバから個々のデータを取ってきて、新しいページ全体を読んでくることなしに、ページの一部を書き換える事です。この一見ちょっとした事が、サイトのパフォーマンスや振舞いに巨大なインパクトを与えました。この記事ではそのコンセプトを解説し、これを可能にした技術 XMLHttpRequest や Fetch API について見ていきます。
前提条件: | JavaScript の基本 (最初のステップ、ビルディングブロック、JavaScript オブジェクトを参照)、クライアントサイド API の基本 |
---|---|
目標: | サーバからデータを取得し、それを使用して Web ページのコンテンツを更新する方法を習得する。 |
これの問題は何か?
もともと Web のページ読み込みは単純でした — Web サイトのデータをサーバにリクエストすると、何も問題がなければ、ページを構成するいろいろなものがダウンロードされてあなたのコンピュータに表示されていました。
このモデルの問題は、どこかページの一部を書き換えたい場合、例えば新しい商品の一群を表示したり新しいページを読み込ませたりをする毎に、ページ全体を読み直さなければならない事です。これはとても無駄が多くてユーザ体験が悪化します、とりわけページが大きくて複雑になってくるにつれて。
Ajax の登場
上述の問題を解決すべく、Web ページから細かいデータ (HTML、XML、JSON やプレーンテキストのような) をリクエストし、それを必要な時だけ表示するという技術の誕生へと繋がりました。
これは XMLHttpRequest
や、最近では Fetch API の利用によって実現されます。これらの技術は、Web ページがサーバにある特定のリソースを直接 HTTP リクエストし、必要があれば結果のデータを表示する前に整形する事を可能にしました。
注記: これらのテクニック一般はかつて Ajax (Asynchronous JavaScript and XML)と呼ばれていましたが、これは XMLHttpRequest
を使って XML データを要求するものが多かったためです。今日ではそういうものばかりではありませんが (XMLHttpRequest
や Fetch を使って JSON を要求する場合の方が多いでしょう)、結果としては同じであり、"Ajax" という用語はしばしば今でもこのテクニックを説明するのに使われます。
Ajax モデルには、ブラウザにページ全体をリロードされるのではなく、もっと賢くデータをリクエストするために Web API をプロキシとして使うという事も含まれます。これの重要性を考えてみて下さい:
- お気に入りの情報に富んだサイト、アマゾンとか YouTube とか CNN とかに行って読み込みます。
- さて新しい商品だか何だかを検索します。メインのコンテンツは変わるでしょうが、周りに表示されている情報、ヘッダーやフッター、ナビゲーションメニューなど、大半はそのままでしょう。
これはとても良いことで、それは:
- ページの更新がずっと素早く、切り替わるのを待つ必要もないので、サイトがずっと早くて反応の良いものに感じられます。
- 更新毎にダウンロードされるデータが少ないので、帯域の無駄が少なくなります。ブロードバンドに接続されたデスクトップではさして問題ではないかもしれませんが、モバイルデバイスからや、どこでも高速インターネット接続が使えるわけではない開発途上国ではとても重要な問題です。
さらなる高速化のために、サイトの中には必要なものやデータを最初にリクエストされた時にユーザのコンピュータに保存してしまい、以降の訪問では保存ずみのものを、サーバから最新版のダウンロードさせる事なく使用するものもあります。コンテンツはそれが更新された時だけサーバから再読み込みされます。
基本的な Ajax リクエスト
XMLHttpRequest
と Fetch それぞれを使って、そのようなリクエストをどうやるのか見ていきましょう。それらの例では、いくつかの異なるテキストファイルから取り出したデータをリクエストし、コンテンツ領域に埋め込みます。
この一連のファイルは疑似データベースとして働きます。実際のアプリケーションでは、PHP や Python、Node のようなサーバサイド言語を使ってデータベースから取り出したデータをリクエストする場合が多いでしょう。ですがここでは簡単にしておき、クライアント側のパートに集中します。
XMLHttpRequest
XMLHttpRequest
(よく XHR と略記されます) は今となってはかなり古い技術です — Microsoft によって1990年代に発明され、非常に長い間ブラウザを超えて標準化されてきました。
-
この例題を始めるにあたり、ajax-start.html と4つのテキストファイル — verse1.txt、verse2.txt、verse3.txt と verse4.txt — のローカルコピーを、あなたのコンピュータの新しいディレクトリに作って下さい。この例題では、ドロップダウンメニューから選択されたら、詩 (ご存知の詩かも) のこれら異なる節を XHR を使って読み込みます。
-
<script>
要素のすぐ内側に、下のコードを書き足して下さい。これは<select>
と<pre>
要素への参照を定数に保存し、onchange
イベントハンドラ関数を定義していて、これは select の値が変わったら、その値が呼び出される関数updateDisplay()
の引数となるようにします。const verseChoose = document.querySelector('select'); const poemDisplay = document.querySelector('pre'); verseChoose.onchange = function() { const verse = verseChoose.value; updateDisplay(verse); };
-
updateDisplay()
関数を定義しましょう。まずはさっきのコードブロックの下に以下を書き足します — これは関数のからっぽのガワです。 注: ステップ 4 から 9 はすべて、この関数内で実施します。function updateDisplay(verse) { }
-
関数を、後から必要になる読み込みたいテキストファイルを指す相対 URL を作るところからはじめます。
<select>
要素の値は常に、選択されている<option>
の内側テキスト、例えば"Verse 1"とか、に一致します (value 属性で異なる値を設定していなければ)。これに相当するテキストファイルは "verse1.txt" で HTML と同じディレクトリにあるので、ファイル名だけで十分です。ただ、Web サーバはたいてい大文字小文字を区別しますし、今回のファイル名にスペースは含まれていません。"Verse 1" を "verse1.txt" に変換するためには、V を小文字にして、スペースを取り除き、.txt を末尾に追加しなければなりません。これは
replace()
にtoLowerCase()
、あと単なる 文字列の結合 で実現できます。以下のコードをあなたのupdateDisplay()
関数の内側に追加して下さい:verse = verse.replace(" ", ""); verse = verse.toLowerCase(); let url = verse + '.txt';
-
XHR リクエストを作り始めるため、リクエストオブジェクトを
XMLHttpRequest()
コンストラクタを使って作成しなければなりません。このオブジェクトには好きな名前を付けられますが、単純にするためrequest
を使います。updateDisplay()
関数の内側で、先の行の下に以下を追加します:let request = new XMLHttpRequest();
-
次に
open()
メソッドを使ってどの HTTP リクエストメソッド を使ってリソースをネットワークから取得するか、URL はどこかを指定しなければなりません。ここでは単にGET
メソッドを使い、URL にはurl
変数の値をセットします。先の行の下に以下を追加します:request.open('GET', url);
-
次はレスポンスにどのような形式にしたいか指定 — これはリクエストの
responseType
プロパティで指定します —text
にします。厳密に言えばこの場合は必須の指定ではありません — XHR はデフォルトで text を返します — が、いつの日か他のデータ形式を指定したくなる場合にそなえて、この設定をする習慣をつけておくと良いと思います。次を追加して下さい:request.responseType = 'text';
-
ネットワークからリソースを取得する処理は非同期asynchronous 処理なので、戻りを使って何かをする前に、あなたは処理が完了(リソースがネットワークから返ってくる)するのを待たなければならず、さもないとエラーが投げられます。XHR では
onload
イベントハンドラを使ってこの問題をさばけます — これはload
イベントが発火(レスポンスが返ってきた)した時に実行されます。このイベントが起きた後は、レスポンスデータは XHR リクエストオブジェクトのresponse
プロパティとして取得できます。さっき追加した行の後に以下を追加して下さい。
onload
イベントハンドラの中で、poemDisplay
(<pre>
要素) のtextContent
プロパティにrequest.response
プロパティの値を設定しているのがお判りでしょう。request.onload = function() { poemDisplay.textContent = request.response; };
-
以上は全部、XHR リクエストの設定です — 実は私たちがやれと指示するまで動作はしません。やれと指示するには、
send()
メソッドを使います。さっき追加した行の後に以下を追加して、関数を完成させます。この行は、updateDisplay()
関数の閉じ中括弧のすぐ上に置く必要があります。request.send();
-
今の時点でのこの例題にある問題の一つは、最初に読み込まれた時点ではなにも詩が表示されないことです。これを直すには、あなたのコードの一番下 (
</script>
閉じタグのすぐ上) に以下の二行を追加し、デフォルトで1番の詩を読み込みませ、<select>
要素に適切な値を指させます:updateDisplay('Verse 1'); verseChoose.value = 'Verse 1';
サーバからあなたの例題を送らせる
今時のブラウザ (Chrome も含まれます) は、ローカルファイルとして例題を実行しても XHR リクエストを行ないません。これはセキュリティの制限によるものです (Web のセキュリティにより詳しくは Webサイトのセキュリティを読んで下さい)。
これをどうにかするため、例題をローカルの Web サーバを使って実行しなければなりません。どうやるのかは、 テスト用のローカルサーバを設定するにはどうすればいい? を読んで下さい。
Fetch
Fetch API は、基本的には XHR の今風の代替品です — 最近になってブラウザに組込まれたもので、非同期 HTTP リクエストを JavaScript で、開発者や他の Fetch の上に組まれた API から簡単に行なえるようにするためのものです。
先の例を Fetch を使うように書き換えてみましょう!
-
さっき完成させた例題のディレクトリのコピーを作ります(前の例題を完成させていないなら、新しいディレクトリを作成して、そこに xhr-basic.html と4つのテキストファイル — (verse1.txt、verse2.txt、verse3.txt と verse4.txt) のコピーを作って下さい。
-
updateDisplay()
関数の中から、XHR のコードを探し出します:let request = new XMLHttpRequest(); request.open('GET', url); request.responseType = 'text'; request.onload = function() { poemDisplay.textContent = request.response; }; request.send();
-
XHR のコードを次のように置き換えます:
fetch(url).then(function(response) { response.text().then(function(text) { poemDisplay.textContent = text; }); });
-
例題をブラウザに読み込むと(Web サーバから読んで下さい)、XHR 版と同様に動作するするはずです。今時のブラウザを使っていれば。
Fetch のコードでは何が起きている?
まず最初に、fetch()
メソッドが呼ばれ、取得したいリソースの URL が渡されています。これは XHR の request.open()
の今時な同等品で、さらに言えば .send()
に相当するものは必要ありません。
その後に、.then()
メソッドが fetch()
の後に連鎖されているのがわかるでしょう — このメソッドは Promises
の一部で、非同期処理を行なうための今風な JavaScript に備わる機能です。fetch()
はプロミスを返し、これはサーバから送られたレスポンスによって解決されます — .then()
を使ってプロミスが解決された後にある種後始末のコードを走らせるようにし、そのコードとは内側で定義した関数にあたります。これは XHR 版の onload
イベントハンドラに相当します。
この関数には、fetch()
のプロミスが解決された際に、自動的にサーバからのレスポンスが引数として渡されます。関数の中で、レスポンスをつかまえてその text()
メソッド、これは基本的にレスポンスを生のテキストで返すもの、を走らせます。これは XHR 版の request.responseType = 'text'
部分と等価です。
text()
もプロミスを返しているのがおわかりでしょう、ですのでそれに別の .then()
を連鎖させ、その中で text()
のプロミスが解決する生テキストを受けとるよう、関数を定義します。
内側のプロミスの関数の中で、XHR 版でやったのとほとんど同じ事をやっています — <pre>
要素のテキストコンテントにテキスト値を設定しています。
Aside on promises
プロミスは初めて見るとちょっと混乱させられますが、今はひとまずそんなに心配しなくて大丈夫です。ちょっとすれば慣れます、とくに今風の JavaScript APIを学んでいけば — 新しい部分の大半がこのプロミスに強く依存しています。
上の例のプロミスの構造を見直してみましょう、もうちょっと意味が通じてくるかもしれません:
fetch(url).then(function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
});
最初の行で言っているのは、「urlにあるリソースを取ってこい(fetch)」(fetch(url)
)で、「それから(then)プロミスが解決したら指定した関数を実行しろ」(.then(function() { ... })
)です。「解決」とは、「この先どこかの時点で、指定された処理の実行を終える」事を意味します。この場合だと指定された処理とは、指定のURLからリソースを取ってきて(HTTPリクエストを使って)、そのレスポンスを私たちがどうにかできるように返せ、です。
実際のところ、then()
に渡される関数は、すぐには実行されないコードの塊です — すぐにではなく、未来のどこかの時点でレスポンスが返って来た時に実行されます。頭に入れておいて下さい、プロミスは変数に保存する事もできて、変数に .then()
を連鎖する事ができます。次のコードがやっているのも同じ事です:
let myFetch = fetch(url);
myFetch.then(function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
});
fetch()
メソッドは HTTP レスポンスによって解決されるプロミスを返し、その後ろに連鎖された .then()
の中にどのような関数を定義しても、それには引数としてレスポンスが自動で渡されます。引数にどんな名前を付けるのもご自由です — 下の例もちゃんと動きます:
fetch(url).then(function(dogBiscuits) {
dogBiscuits.text().then(function(text) {
poemDisplay.textContent = text;
});
});
ですがパラメータにはその中身がわかる名前を付けた方がいいですよね!
今度は関数だけに着目しましょう:
function(response) {
response.text().then(function(text) {
poemDisplay.textContent = text;
});
}
レスポンスオブジェクトには text()
メソッドがあって、これはレスポンスボディにある生データを受けて、プレインテキスト(これが私たちの必要とする形式です)、に変換します。このメソッドもプロミス(これは結果となるテキスト文字列で解決します)を返すので、ここでまた別の .then()
を使い、この内部で、テキスト文字列を使って私たちがやりたい事を行うための別の関数を定義します。私たちがやるのは、ただ詩用の <pre>
要素の textContent
プロパティをテキスト文字列と同じに設定だけなので、これはとても単純です。
これも覚えておく価値があります、それぞれのブロックの結果を次のブロックに渡していくように、直接複数のプロミスブロック(.then()
ブロック以外の種類もあります)を次から次へと連鎖する事ができます、あたかも鎖を下にたどっていくように。このおかげで、プロミスはとても強力なのです。
次のブロックはもとの例題と同じ事をしますが、違うやり方で書かれています:
fetch(url).then(function(response) {
return response.text()
}).then(function(text) {
poemDisplay.textContent = text;
});
多くの開発者はこの書き方の方が好きです、なぜなら平らで、間違いなく長大なプロミス連鎖も読みやすいからです — それぞれのプロミスが、前のやつの内側に来る(これは扱いづらくなる場合があります)のではなく、前のやつから順々に続いています。違うのは return
文を response.text() の前に書いて、それが出した結果を次の鎖に渡すようにしなければならないところだけです。
どっちの機構を使うべき?
これは本当に、あなたがどんなプロジェクトを進めているかによります。XHR は長いこと存在しているので、様々なブラウザで非常によくサポートされています。一方 Fetch とプロミスは Web プラットフォームに最近追加されたものなので、ブラウザ界では結構サポートされているんですが、IE はサポートしていません。
古いブラウザをサポートする必要があるのならば、XHR の方が良いでしょう。ですがあなたがもっと先進的なプロジェクトで働いて、古いブラウザの事でさして悩まないなら、Fetch が良い選択になるでしょう。
本当はどっちも学ぶべきです — Fetch は IE が消えていくにつれ(IE は、Microsoft の新しい Edge ブラウザのおかげで開発が終了しています)どんどん一般的になっていくでしょうが、もうしばらくは XHR が必要でしょう。
もっとややこしい例題
この記事のまとめとして、Fetch のより興味深い使い方を示す、ちょっとばかり難しい例題を見ていきましょう。例題用に缶詰屋というサイトを作成しました — これは缶詰だけを売る仮想のお店です。これの GitHubでのライブ実行 と ソースコード が見られます。
デフォルトではサイトには全ての商品が表示されますが、左側のカラムにあるフォームコントロールからカテゴリから、検索語から、あるいはその両方によってフィルタリングをかけられます。
商品をカテゴリや検索語によってフィルタリングする処理をし、UIでデータが正しく表示されるように文字列を操作するためなどに、けっこうな量の複雑なコードがあります。この記事のなかでそれら全てについて解説しませんが、ソースコードのコメントに詳しいことがたくさん書いてあります(can-script.jsを見て下さい)。
ですが、Fetch のコードについては説明していきます。
Fetch を使うブロックの最初は、JavaScript の初めの方にあります:
fetch('products.json').then(function(response) {
return response.json();
}).then(function(json) {
let products = json;
initialize(products);
}).catch(function(err) {
console.log('Fetch problem: ' + err.message);
});
fetch()
関数はプロミスを返します。これが成功裏に完了すると、一つ目の .then()
ブロックの中にある関数は、ネットワークから返された response
を受け取ります。
この関数の中で、text()
ではなくて json()
を実行しています。プレインテキストではなく、構造化された JSON データとしてレスポンスを返してほしいからです。
次に、別の .then()
を最初の .then()
の後に連鎖させています。これに、response.json()
プロミスから返された json
を含む成功時の関数を渡しています。この json
を products
変数の値として代入してから、initialize(products)
を実行します。すべての商品をユーザーインターフェイスに表示する処理が開始されます。
エラーを処理するために、連鎖の最後に .catch()
ブロックを連鎖させています。これは、何らかの理由でプロミスが失敗した場合に実行されます。その中には、引数として渡される関数、error
オブジェクトが含まれています。この error
オブジェクトを使用して、発生したエラーがどういうものかを伝えられます。ここでは単純な console.log()
を使用して伝えています。
ただし、完全な Web サイトでは、ユーザの画面にメッセージを表示し、状況を改善する選択肢を提供することで、このエラーをより適切に処理するでしょう。とは言え、ここでは単純な console.log()
意外は必要ありません。
あなたは自分でも失敗した場合のテストができます:
- 例題のファイルのローカルコピーを作成して下さい(缶詰屋の ZIPファイルをダウンロードして展開して下さい)。
- コードを Web サーバから読んで走らせるようにします(方法は前に Serving your example from a serverで解説しました)。
- fetch するファイルのパスを、'produc.json' のようなものに変更します(誤ったファイル名にして下さい)。
- ここでインデックスファイルをブラウザに読み込んで(
localhost:8000
から)、あなたのブラウザの開発者コンソールを見ます。次の行のようなメッセージが表示されるはずです「Network request for produc.json failed with response 404: File not found」。
二つ目の Fetch ブロックは fetchBlob()
関数の中にあります:
fetch(url).then(function(response) {
return response.blob();
}).then(function(blob) {
// Convert the blob to an object URL — this is basically a temporary internal URL
// that points to an object stored inside the browser
let objectURL = URL.createObjectURL(blob);
// invoke showProduct
showProduct(objectURL, product);
});
これも前のとおおよそ同じように動作しますが、json()
ではなくて blob()
を使っているところが違います — 今回の場合は画像ファイルを返したいので、これ用に使うデータ形式は Blob — これは "Binary Large Object" の略で、たいていは巨大なファイルのようなオブジェクト、画像や動画のようなものを示すのに使われます。
blob を成功裏に受信したら、createObjectURL()
を使ってそこからオブジェクトURLを取り出します。これはそのブラウザの中でのみ有効なオブジェクトを示す一時的な URL を返します。あまり読み易いものではありませんが、缶詰屋アプリを開いて画像を Ctrlクリックもしくは右クリックして、メニューから「画像を表示」を選択する(これはあなたが使っているブラウザによって異なる場合があります)と見ることができます。オブジェクトURLはブラウザのアドレスバーに表示され、こんな感じになるでしょう:
blob:http://localhost:7800/9b75250e-5279-e249-884f-d03eb1fd84f4
課題: XHR 版の缶詰屋
ちょっとした練習として、アプリの Fetch 版を XHR を使うように書き換えて下さい。ZIPファイル のコピーを作って、上手く JavaScript を書き換えてみて下さい。
ちょっとしたヒントです:
XMLHttpRequest
のリファレンス記事が役に立つでしょう。- 基本的には、初めの方の XHR-basic.html の例で見たのと同じようなパターンを使う必要があります。
- ただし、Fetch 版の缶詰屋でお見せしたのと同様なエラー処理を追加する必要があります:
load
イベントが発火した後は、プロミスのthen()
の中ではなく、request.response
の中にレスポンスはあります。- XHR において、Fetch の
response.ok
に相当する一番良いやり方は、request.status
が 200 であるか、request.readyState
が 4 である事をチェックする事です。 - ステータスとステータスメッセージを取得するためのプロパティは一緒ですが、これは
response
オブジェクトの中ではなくrequest
(XHR)オブジェクトの中にあります。
注記: 上手くいかないときは、我々のGitHubにある完成版のコード (ソースコードはこちらから、ライブ実行版もどうぞ) と比べてみて下さい。
まとめ
私たちのサーバからのデータ取得に関する記事は以上です。ここまでくれば、どう XHR と Fetch を使って進めていけばいいのか理解できたことでしょう。
あわせて参照
この記事には様々なほんのさわりしか説明していない事項がたくさんあります。これらの事項についてもっと詳しくは、以下の記事を見て下さい: