この記事は翻訳作業中です。

モダンなウェブサイトやアプリケーションでしょっちゅう必要になる仕事は、サーバから個々のデータを取ってきて、新しいページ全体を読んでくることなしに、ページの一部を書き換える事です。この一見ちょっとした事が、サイトのパフォーマンスや振舞いに巨大なインパクトを与えました。この記事ではそのコンセプトを解説し、これを可能にした技術 XMLHttpRequest や Fetch API について見ていきます。

Prerequisites: JavaScript basics (see first steps, building blocks, JavaScript objects), the basics of Client-side APIs
Objective: To learn how to fetch data from the server and use it to update the contents of a web page.

これの問題は何か?

もともとはウェブのページ読み込みは単純でした — ウェブサイトのデータをサーバにリクエストすると、何も問題がなければ、ページを構成するいろいろがダウンロードされてあなたのコンピュータに表示されていました。

A basic representation of a web site architecture

このモデルの問題は、どこでもページの一部を書き換えたい場合、例えば新しい商品の一群を表示したり新しいページを読み込ませたりをする毎に、ページ全体を読み直さなければならない事です。これはとても無駄が多くてユーザ体験が悪化します、とりわけページが大きくて複雑になってくるにつれて。

Ajaxの登場

これは、上述の問題を解決すべく、ウェブページから細かいデータ( HTMLXMLJSON やプレーンテキストのような)をリクエストし、それを必要な時にだけ表示するという技術の誕生へと繋がりました。

これは XMLHttpRequest や、最近では Fetch API の利用によって実現されます。これらの技術は、ウェブページがサーバにある特定のリソースを直接 HTTP リクエストし、必要があれば結果のデータを表示する前に整形する事を可能にしました。

注記: これらのテクニック一般はかつて Ajax(Asynchronous JavaScript and XML)と呼ばれていましたが、これは XMLHttpRequest を使って XML データを要求するものが多かったためです。今日ではそういうものばかりではありませんが(XMLHttpRequest や Fetch を使って JSON を要求する場合の方が多いでしょう)、結果としては同じであり、"Ajax"という用語はしばしば今でもこのテクニックを説明するのに使われます。

A simple modern architecture for web sites

Ajax モデルには、ブラウザーにページ全体をリロードされるのではなく、もっと賢くデータをリクエストするためにウェブAPIをプロキシーとして使うという事も含まれます。これの重要性を考えてみて下さい:

  1. あなたのお気に入りの情報に富んだサイト、アマゾンとかYouTubeとかCNNとかに行って読み込みます。
  2. さて新しい商品だか何だかを検索します。メインのコンテンツは変わるでしょうが、周りに表示されている情報、ヘッダーやフッター、ナビゲーションメニューなど、大半はそのままでしょう。

これはとても良いことで、それは:

  • ページの更新がずっと素早く、ページが切り替わるのを待つ必要もないので、サイトがずっと早くて反応の良いものに感じられます。
  • 更新毎にダウンロードされるデータが少ないので、帯域の無駄が少なくなります。ブロードバンドに接続されたデスクトップではさして問題ではないかもしれませんが、モバイルデバイスからや、どこでも高速インターネット接続が使えるわけではない開発途上国ではとても重要な問題です。

さらなる高速化のために、サイトの中には必要なものやデータを最初にリクエストされた時にユーザのコンピュータに保存してしまい、以降の訪問では保存ずみのものを、サーバから最新版のダウンロードさせる事なく使用するものもあります。コンテンツはそれが更新された時だけサーバから再読み込みみされます。

A basic web app data flow architecture

基本的な Ajax リクエスト

XMLHttpRequestFetch それぞれを使って、そのようなリクエストをどうやるのか見ていきましょう。 それらの例では、いくつかの異なるテキストファイルから取り出したデータをリクエストし、コンテンツ領域に埋め込みます。

この一連のファイルは疑似データベースとして働きます。実際のアプリケーションでは、PHP や Python、Node のようなサーバ側言語を使ってデータベースから取り出したデータをリクエストする場合が多いでしょう。ですがここでは簡単にしておき、クライアント側のパートに集中します。

XMLHttpRequest

XMLHttpRequest (よく XHR と略記されます) は今となってはかなり古い技術です — Microsoft によって1990年代に発明され、非常に長い間ブラウザーを超えて標準化されてきました。

  1. この例題を始めるにあたり、ajax-start.html と4つのテキストファイル — verse1.txtverse2.txtverse3.txtverse4.txt — のローカルコピーを、あなたのコンピュータの新しいディレクトリに作って下さい。この例題では、ドロップダウンメニューから選択されたら、詩(ご存知の詩かも)のこれら異なる節を XHR を使って読み込みます。

  2. <script>要素のすぐ内側に、下のコードを書き足して下さい。これは <select><pre> 要素への参照を変数に保存し、onchange イベントハンドラー関数を定義していて、これは select の値が変わったら、その値が呼び出される関数 updateDisplay() の引数となるようにします。

    var verseChoose = document.querySelector('select');
    var poemDisplay = document.querySelector('pre');
    
    verseChoose.onchange = function() {
      var verse = verseChoose.value;
      updateDisplay(verse);
    };
  3. updateDisplay() 関数を定義しましょう。まずはさっきのコードブロックの下に以下を書き足します — これは関数のからっぽのガワです:

    function updateDisplay(verse) {
    
    };
  4. 関数を、後から必要になる読み込みたいテキストファイルを指す相対URLを作るところからはじめます。<select> 要素の値は常に、選択されている<option>の内側テキスト、例えば"Verse 1"とか、に一致します(value 属性で異なる値を設定していなければ)。これに相当するテキストファイルは"verse1.txt"でHTMLと同じディレクトリにあるので、ファイル名だけで十分です。

    ただ、ウェブサーバーはたいてい大文字小文字を区別しますし、今回のファイル名にスペースは含まれていません。"Verse 1" を "verse1.txt" に変換するためには、 V を小文字にして、スペースを取り除き、.txt を末尾に追加しなければなりません。これはreplace()toLowerCase()、あと単なる 文字列の結合 で実現できます。 以下のコードをあなたの updateDisplay() 関数の内側に追加して下さい:

    verse = verse.replace(" ", "");
    verse = verse.toLowerCase();
    var url = verse + '.txt';
  5. XHR リクエストを作り始めるため、リクエストオブジェクトを XMLHttpRequest() コンストラクタを使って作成しなければなりません。このオブジェクトをやりたいように呼び出せますが、単純にするため request を使います。先の行の下に以下を追加します:

    var request = new XMLHttpRequest();
  6. 次に open() メソッドを使ってどの HTTP リクエストメソッド を使ってリソースをネットワークから取得するか、URLはどこかを指定しなければなりません。ここでは単に GET メソッドを使い、URL には url 変数の値をセットします。先の行の下に以下を追加します:

    request.open('GET', url);
  7. 次はレスポンスにどのような形式にしたいか指定 — これはリクエストの responseType プロパティで指定します — text にします。厳密に言えばこの場合は必須の指定ではありません — XHR はデフォルトで text を返します — が、いつの日か他のデータ形式を指定したくなる場合にそなえて、この設定をする習慣をつけておくと良いと思います。次を追加して下さい:

    request.responseType = 'text';
  8. ネットワークからリソースを取得する処理は非同期asynchronous 処理なので、戻りを使って何かをする前に、あなたは処理が完了(リソースがネットワークから返ってくる)するのを待たなければならず、さもないとエラーが投げられます。XHR では onload イベントハンドラーを使ってこの問題をさばけます — これは load イベントが発火(レスポンスが返ってきた)した時に実行されます。このイベントが起きた後は、レスポンスデータは XHR リクエストオブジェクトの response プロパティとして取得できます。

    さっき追加したのの後に以下を追加して下さい。onload イベントハンドラーの中で、poemDisplay(<pre>要素)の textContentプロパティに request.response プロパティの値を設定しているのがお判りでしょう。

    request.onload = function() {
      poemDisplay.textContent = request.response;
    };
  9. 以上は全部、XHR リクエストの設定です — 実は私たちがやれと指示するまで動作はしません。やれと指示するには、send() メソッドを使います。さっき追加したのの後に以下を追加して、関数を完成させます:

    request.send();
  10. 今の時点でのこの例題にある問題の一つは、最初に読み込まれた時点ではなにも詩が表示されないことです。これを直すには、あなたのコードの一番下(</script> 閉じタグのすぐ上)に以下の二行を追加し、デフォルトで1番の詩を読み込みませ、<select>要素に適切な値を指させます:

    updateDisplay('Verse 1');
    verseChoose.value = 'Verse 1';

サーバからあなたの例題を送らせる

ブラウザーによって(Chromeも含まれます)は、ローカルファイルとして例題を実行しても XHR リクエストを行ないません。これはセキュリティの制限によるものです(ウェブのセキュリティにより詳しくは ウェブサイトのセキュリティを読んで下さい)。

これをどうにかするため、例題をローカルのウェブサーバを使って実行しなければなりません。どうやるのかは、 テスト用のローカルサーバを設定するにはどうすればいい? を読んで下さい。

Fetch

Fetch API は、基本的には XHR の今風の代替品です — 最近になってブラウザーに組込まれたもので、非同期 HTTP リクエストを JavaScript で、開発者や他の Fetch の上に組まれた API から簡単に行なえるようにするためのものです。

先の例を Fetch を使うように書き換えてみましょう!

  1. さっき完成させた例題のディレクトリのコピーを作ります(前の例題を完成させていないなら、新しいディレクトリを作成して、そこに xhr-basic.html と4つのテキストファイル — (verse1.txtverse2.txtverse3.txtverse4.txt)のコピーを作って下さい。

  2. updateDisplay() 関数の中から、XHR のコードを探し出します:

    var request = new XMLHttpRequest();
    request.open('GET', url);
    request.responseType = 'text';
    
    request.onload = function() {
      poemDisplay.textContent = request.response;
    };
    
    request.send();
  3. XHR のコードを次のように置き換えます:

    fetch(url).then(function(response) {
      response.text().then(function(text) {
        poemDisplay.textContent = text;
      });
    });
  4. 例題をブラウザーに読み込むと(ウェブサーバから読んで下さい)、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() を連鎖する事ができます。次のコードがやっているのも同じ事です:

var 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 とプロミスはウェブプラットフォームに最近追加されたものなので、ブラウザー界では結構サポートされているんですが、IE と Safari(これを書いている時点の最新のテクノロジープレビュー版で、Safari には Fetch のサポートが入りました)はサポートしていません。

古いブラウザーをサポートする必要があるのならば、XHR の方が良いでしょう。ですがあなたがもっと先進的なプロジェクトで働いて、古いブラウザーの事でさして悩まないなら、Fetch が良い選択になるでしょう。

本当はどっちも学ぶべきです — Fetch は IE が消えていくにつれ(IE は、Microsoft の新しい Edge ブラウザーのおかげで開発が終了しています)どんどん一般的になっていくでしょうが、もうしばらくは XHR が必要でしょう。

もっとややこしい例題

この記事のまとめとして、Fetch のより興味深い使い方を示す、ちょっとばかり難しい例題を見ていきましょう。例題用に缶詰屋というサイトを作成しました — これは缶詰だけを売る仮想のお店です。これの GitHubでのライブ実行ソースコード が見られます。

A fake ecommerce site showing search options in the left hand column, and product search results in the right hand column.

デフォルトではサイトには全ての商品が表示されますが、左側のカラムにあるフォームコントロールからカテゴリから、検索語から、あるいはその両方によってフィルタリングをかけられます。

商品をカテゴリや検索語によってフィルタリングする処理をし、UIでデータが正しく表示されるように文字列を操作するためなどに、けっこうな量の複雑なコードがあります。この記事のなかでそれら全てについて解説しませんが、ソースコードのコメントに詳しいことがたくさん書いてあります(can-script.jsを見て下さい)。

ですが、Fetch のコードについては説明していきます。

Fetch を使うブロックの最初は、JavaScript の初めの方にあります:

fetch('products.json').then(function(response) {
  if(response.ok) {
    response.json().then(function(json) {
      products = json;
      initialize();
    });
  } else {
    console.log('Network request for products.json failed with response ' + response.status + ': ' + response.statusText);
  }
});

以前に見たのと似ていますが、条件文の中にある二つ目のプロミスは異なります。この条件文の中で、レスポンスが成功裏に戻されたかチェックしています — response.ok プロパティにはブール値が入っており、レスポンスが OK (つまり 200 meaning "OK" の場合)ならば true が、失敗ならば false が入ります。

レスポンスが成功ならば、二つ目のプロミスを実行します — ですが今回は text() ではなくて json() を使っています。プレインテキストではなく、構造化された JSON を戻り値に要求しているからです。

レスポンスが成功でなければ、コンソールにネットワーク要求が失敗した事を示すエラーを表示し、ここではネットワーク状態とレスポンスの説明的なメッセージ(それぞれ response.statusresponse.statusText プロパティに含まれています)を表示します。もちろん完全なウェブサイトであればこのエラーをもっと上手く、ユーザの画面にメッセージを表示し状況に対する対策を示すなどしてさばくでしょう。

あなたは自分でも失敗した場合のテストができます:

  1. 例題のファイルのローカルコピーを作成して下さい(缶詰屋の ZIPファイルをダウンロードして展開して下さい)。
  2. コードをウェブサーバから読んで走らせるようにします(方法は前に Serving your example from a server解説しました)。
  3. fetch するファイルのパスを、'produc.json' のようなものに変更します(存在しないファイル名にして下さい)。
  4. ここでインデックスファイルをブラウザーに読み込んで( localhost:8000 から)、あなたのブラウザーの開発者コンソールを見ます。次の行のようなメッセージが表示されるはずです「Network request for products.json failed with response 404: File not found」。

二つ目の fetch ブロックは fetchBlob() 関数の中にあります:

fetch(url).then(function(response) {
  if(response.ok) {
    response.blob().then(function(blob) {
      objectURL = URL.createObjectURL(blob);
      showProduct(objectURL, product);
    });
  } else {
    console.log('Network request for "' + product.name + '" image failed with response ' + response.status + ': ' + response.statusText);
  }
});

これも前のとおおよそ同じように動作しますが、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 を使って進めていけばいいのか理解できたことでしょう。

参考文献

この記事には様々なほんのさわりしか説明していない事項がたくさんあります。これらの事項についてもっと詳しくは、以下の記事を見て下さい:

ドキュメントのタグと貢献者

このページの貢献者: i12o
最終更新者: i12o,