2022年5月14日

XMLHttpRequest

XMLHttpRequest は、JavaScriptでHTTPリクエストを行うことができる組み込みのブラウザオブジェクトです。

名前の中に「XML」という単語が含まれていますが、XML形式だけでなく、あらゆるデータを操作できます。ファイルのアップロード/ダウンロード、進捗状況の追跡などが可能です。

現在、XMLHttpRequest をある程度非推奨にする、よりモダンなメソッド fetch があります。

現代のWeb開発では、XMLHttpRequest は次の3つの理由で使用されます。

  1. 歴史的な理由:XMLHttpRequest を使用した既存のスクリプトをサポートする必要があります。
  2. 古いブラウザをサポートする必要があり、ポリフィル (例えば、スクリプトを小さく保つため) は使用したくありません。
  3. fetch ではまだできないこと、例えば、アップロードの進捗状況を追跡する必要があります。

それは馴染みがありますか?もしそうなら、それで結構です。XMLHttpRequest を進めてください。そうでなければ、Fetchに進んでください。

基本

XMLHttpRequestには、同期と非同期の2つの動作モードがあります。

ほとんどの場合で使用される非同期を最初に見てみましょう。

リクエストを行うには、3つのステップが必要です。

  1. XMLHttpRequest を作成する

    let xhr = new XMLHttpRequest();

    コンストラクタには引数はありません。

  2. 初期化を行います。通常、new XMLHttpRequest の直後に行います。

    xhr.open(method, URL, [async, user, password])

    このメソッドは、リクエストの主なパラメータを指定します。

    • method – HTTP メソッド。通常は "GET" または "POST" です。
    • URL – リクエスト先の URL。文字列。 URL オブジェクトを使用できます。
    • async – 明示的に false に設定すると、リクエストは同期になります。これについては後で少し説明します。
    • user, password – 基本 HTTP 認証に必要なログインとパスワード (必要な場合)。

    open の呼び出しは、その名前とは異なり、接続を開きません。リクエストを設定するだけですが、ネットワークアクティビティは send の呼び出しでのみ開始されます。

  3. 送信します。

    xhr.send([body])

    このメソッドは接続を開き、リクエストをサーバーに送信します。オプションの body パラメータにはリクエストボディが含まれます。

    GET のような一部のリクエストメソッドにはボディがありません。POST のような一部のメソッドは、サーバーにデータを送信するために body を使用します。これについては後で例を見てみましょう。

  4. レスポンスの xhr イベントをリッスンします。

    これら3つのイベントが最も広く使用されています。

    • load – リクエストが完了したとき (HTTPステータスが 400 や 500 の場合でも)、レスポンスが完全にダウンロードされたとき。
    • error – リクエストができなかったとき。例えば、ネットワークがダウンした場合や無効な URL の場合。
    • progress – レスポンスがダウンロードされている間、定期的にトリガーされ、ダウンロードされた量を報告します。
    xhr.onload = function() {
      alert(`Loaded: ${xhr.status} ${xhr.response}`);
    };
    
    xhr.onerror = function() { // only triggers if the request couldn't be made at all
      alert(`Network Error`);
    };
    
    xhr.onprogress = function(event) { // triggers periodically
      // event.loaded - how many bytes downloaded
      // event.lengthComputable = true if the server sent Content-Length header
      // event.total - total number of bytes (if lengthComputable)
      alert(`Received ${event.loaded} of ${event.total}`);
    };

完全な例を次に示します。以下のコードは、サーバーから /article/xmlhttprequest/example/load のURLをロードし、進捗状況を出力します。

// 1. Create a new XMLHttpRequest object
let xhr = new XMLHttpRequest();

// 2. Configure it: GET-request for the URL /article/.../load
xhr.open('GET', '/article/xmlhttprequest/example/load');

// 3. Send the request over the network
xhr.send();

// 4. This will be called after the response is received
xhr.onload = function() {
  if (xhr.status != 200) { // analyze HTTP status of the response
    alert(`Error ${xhr.status}: ${xhr.statusText}`); // e.g. 404: Not Found
  } else { // show the result
    alert(`Done, got ${xhr.response.length} bytes`); // response is the server response
  }
};

xhr.onprogress = function(event) {
  if (event.lengthComputable) {
    alert(`Received ${event.loaded} of ${event.total} bytes`);
  } else {
    alert(`Received ${event.loaded} bytes`); // no Content-Length
  }

};

xhr.onerror = function() {
  alert("Request failed");
};

サーバーが応答したら、次の xhr プロパティで結果を受け取ることができます。

status
HTTPステータスコード (数値): 200, 404, 403 など。HTTP以外のエラーの場合、0 になる可能性があります。
statusText
HTTPステータスメッセージ (文字列): 通常、200 の場合は OK404 の場合は Not Found403 の場合は Forbidden など。
response (古いスクリプトでは responseText を使用する場合があります)
サーバーのレスポンスボディ。

対応するプロパティを使用してタイムアウトを指定することもできます。

xhr.timeout = 10000; // timeout in ms, 10 seconds

指定された時間内にリクエストが成功しない場合、キャンセルされ、timeout イベントが発生します。

URL検索パラメータ

?name=value のように URL にパラメータを追加し、適切なエンコーディングを確保するために、URL オブジェクトを使用できます。

let url = new URL('https://google.com/search');
url.searchParams.set('q', 'test me!');

// the parameter 'q' is encoded
xhr.open('GET', url); // https://google.com/search?q=test+me%21

レスポンスタイプ

xhr.responseType プロパティを使用して、レスポンスの形式を設定できます。

  • "" (デフォルト) – 文字列として取得、
  • "text" – 文字列として取得、
  • "arraybuffer"ArrayBuffer として取得 (バイナリデータの場合、ArrayBuffer、バイナリ配列の章を参照)、
  • "blob"Blob として取得 (バイナリデータの場合、Blobの章を参照)、
  • "document" – XMLドキュメント (XPath やその他のXMLメソッドを使用できます) または HTMLドキュメント (受信データのMIMEタイプに基づく) として取得、
  • "json" – JSON として取得 (自動的に解析されます)。

例えば、レスポンスをJSONとして取得しましょう。

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/example/json');

xhr.responseType = 'json';

xhr.send();

// the response is {"message": "Hello, world!"}
xhr.onload = function() {
  let responseObj = xhr.response;
  alert(responseObj.message); // Hello, world!
};
ご注意

古いスクリプトでは、xhr.responseTextxhr.responseXML プロパティも見られるかもしれません。

これらは、文字列またはXMLドキュメントを取得するための歴史的な理由から存在します。現在では、xhr.responseType で形式を設定し、上記のように xhr.response を取得する必要があります。

準備状態

XMLHttpRequest は、処理が進むにつれて状態を変化させます。現在の状態は xhr.readyState としてアクセスできます。

仕様書に記載されているすべての状態

UNSENT = 0; // initial state
OPENED = 1; // open called
HEADERS_RECEIVED = 2; // response headers received
LOADING = 3; // response is loading (a data packet is received)
DONE = 4; // request complete

XMLHttpRequest オブジェクトは、0123 → … → 34 の順に状態を移行します。状態 3 は、ネットワーク経由でデータパケットが受信されるたびに繰り返されます。

readystatechange イベントを使用して追跡できます。

xhr.onreadystatechange = function() {
  if (xhr.readyState == 3) {
    // loading
  }
  if (xhr.readyState == 4) {
    // request finished
  }
};

readystatechange リスナーは非常に古いコードで見つけることができます。これは歴史的な理由によるもので、load やその他のイベントがなかった時代があったからです。現在では、load/error/progress ハンドラがそれを非推奨にしています。

リクエストの中止

リクエストはいつでも終了できます。xhr.abort() の呼び出しがそれを行います。

xhr.abort(); // terminate the request

これにより、abort イベントが発生し、xhr.status0 になります。

同期リクエスト

open メソッドで3番目のパラメータ asyncfalse に設定されている場合、リクエストは同期的に行われます。

つまり、JavaScriptの実行は send() で一時停止し、レスポンスを受信したときに再開します。 alert または prompt コマンドのようなものです。

ここに書き直された例を示します。open の3番目のパラメータは false です。

let xhr = new XMLHttpRequest();

xhr.open('GET', '/article/xmlhttprequest/hello.txt', false);

try {
  xhr.send();
  if (xhr.status != 200) {
    alert(`Error ${xhr.status}: ${xhr.statusText}`);
  } else {
    alert(xhr.response);
  }
} catch(err) { // instead of onerror
  alert("Request failed");
}

良さそうに見えるかもしれませんが、同期呼び出しは、読み込みが完了するまでページ内の JavaScript をブロックするため、めったに使用されません。一部のブラウザではスクロールが不可能になります。同期呼び出しに時間がかかりすぎると、ブラウザは「ハング」しているWebページを閉じるように提案する可能性があります。

別のドメインからのリクエストやタイムアウトの指定など、XMLHttpRequest の多くの高度な機能は、同期リクエストでは利用できません。また、ご覧のとおり、進捗状況の表示はありません。

以上の理由から、同期リクエストは非常に控えめに、ほとんど使用されません。これ以上は説明しません。

HTTPヘッダー

XMLHttpRequest では、カスタムヘッダーを送信することも、レスポンスからヘッダーを読み取ることもできます。

HTTPヘッダーには3つのメソッドがあります。

setRequestHeader(name, value)

指定された namevalue でリクエストヘッダーを設定します。

例えば

xhr.setRequestHeader('Content-Type', 'application/json');
ヘッダーの制限

RefererHost など、いくつかのヘッダーはブラウザによって排他的に管理されています。完全なリストは、仕様書に記載されています。

XMLHttpRequest は、ユーザーの安全性とリクエストの正確性のために、それらを変更することは許可されていません。

ヘッダーを削除できない

XMLHttpRequest のもう1つの特徴は、setRequestHeader を取り消すことができないことです。

ヘッダーが設定されると、設定されたままになります。追加の呼び出しは、ヘッダーに情報を追加しますが、上書きしません。

例えば

xhr.setRequestHeader('X-Auth', '123');
xhr.setRequestHeader('X-Auth', '456');

// the header will be:
// X-Auth: 123, 456
getResponseHeader(name)

指定された name でレスポンスヘッダーを取得します (Set-Cookie および Set-Cookie2 を除く)。

例えば

xhr.getResponseHeader('Content-Type')
getAllResponseHeaders()

Set-Cookie および Set-Cookie2 を除く、すべてのレスポンスヘッダーを返します。

ヘッダーは1行で返されます。例えば

Cache-Control: max-age=31536000
Content-Length: 4260
Content-Type: image/png
Date: Sat, 08 Sep 2012 16:53:16 GMT

ヘッダー間の改行は常に "\r\n" (OSに依存しません) なので、個々のヘッダーに簡単に分割できます。名前と値の間の区切り文字は、常にコロンとそれに続くスペース ": " です。これは仕様書で固定されています。

したがって、名前と値のペアを持つオブジェクトを取得する場合は、少しJSを投入する必要があります。

このように (2つのヘッダーが同じ名前を持っている場合、後者が前者を上書きすると仮定)

let headers = xhr
  .getAllResponseHeaders()
  .split('\r\n')
  .reduce((result, current) => {
    let [name, value] = current.split(': ');
    result[name] = value;
    return result;
  }, {});

// headers['Content-Type'] = 'image/png'

POST, FormData

POSTリクエストを行うには、組み込みの FormData オブジェクトを使用できます。

構文

let formData = new FormData([form]); // creates an object, optionally fill from <form>
formData.append(name, value); // appends a field

フォームを作成し、オプションでフォームから入力し、必要に応じてさらにフィールドを append して、その後

  1. xhr.open('POST', ...)POST メソッドを使用します。
  2. xhr.send(formData) でフォームをサーバーに送信します。

例えば

<form name="person">
  <input name="name" value="John">
  <input name="surname" value="Smith">
</form>

<script>
  // pre-fill FormData from the form
  let formData = new FormData(document.forms.person);

  // add one more field
  formData.append("middle", "Lee");

  // send it out
  let xhr = new XMLHttpRequest();
  xhr.open("POST", "/article/xmlhttprequest/post/user");
  xhr.send(formData);

  xhr.onload = () => alert(xhr.response);
</script>

フォームは multipart/form-data エンコーディングで送信されます。

または、JSONの方が好きなら、JSON.stringify を使用して文字列として送信します。

ヘッダー Content-Type: application/json を設定することを忘れないでください。多くのサーバーサイドフレームワークは、それでJSONを自動的にデコードします。

let xhr = new XMLHttpRequest();

let json = JSON.stringify({
  name: "John",
  surname: "Smith"
});

xhr.open("POST", '/submit')
xhr.setRequestHeader('Content-type', 'application/json; charset=utf-8');

xhr.send(json);

.send(body) メソッドは非常に汎用性があります。Blob オブジェクトや BufferSource オブジェクトを含め、ほとんどすべての body を送信できます。

アップロードの進捗状況

progress イベントは、ダウンロード段階でのみトリガーされます。

つまり、何かを POST する場合、XMLHttpRequest は最初にデータ(リクエストボディ)をアップロードし、次にレスポンスをダウンロードします。

大きなものをアップロードしている場合、私たちはアップロードの進捗状況を追跡することに、より関心があるはずです。しかし、xhr.onprogress はここでは役に立ちません。

アップロードイベントを追跡するためだけの、メソッドを持たない別のオブジェクト、xhr.upload があります。

これは xhr と同様のイベントを生成しますが、xhr.upload はアップロード時にのみそれらをトリガーします。

  • loadstart – アップロードが開始されました。
  • progress – アップロード中に定期的にトリガーされます。
  • abort – アップロードが中止されました。
  • error – HTTPエラーではないエラーです。
  • load – アップロードが正常に完了しました。
  • timeout – アップロードがタイムアウトしました(timeout プロパティが設定されている場合)。
  • loadend – アップロードが成功またはエラーのいずれかで完了しました。

ハンドラの例

xhr.upload.onprogress = function(event) {
  alert(`Uploaded ${event.loaded} of ${event.total} bytes`);
};

xhr.upload.onload = function() {
  alert(`Upload finished successfully.`);
};

xhr.upload.onerror = function() {
  alert(`Error during the upload: ${xhr.status}`);
};

以下は実際の例です。進捗状況を示すファイルアップロードです。

<input type="file" onchange="upload(this.files[0])">

<script>
function upload(file) {
  let xhr = new XMLHttpRequest();

  // track upload progress
  xhr.upload.onprogress = function(event) {
    console.log(`Uploaded ${event.loaded} of ${event.total}`);
  };

  // track completion: both successful or not
  xhr.onloadend = function() {
    if (xhr.status == 200) {
      console.log("success");
    } else {
      console.log("error " + this.status);
    }
  };

  xhr.open("POST", "/article/xmlhttprequest/post/upload");
  xhr.send(file);
}
</script>

クロスオリジンリクエスト

XMLHttpRequest は、fetch と同じ CORS ポリシーを使用して、クロスオリジンリクエストを作成できます。

fetch と同様に、デフォルトではクッキーや HTTP 認証を別のオリジンに送信しません。それらを有効にするには、xhr.withCredentialstrue に設定します。

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;

xhr.open('POST', 'http://anywhere.com/request');
...

クロスオリジンヘッダーの詳細については、Fetch:クロスオリジンリクエスト の章を参照してください。

まとめ

XMLHttpRequest を使用した GET リクエストの典型的なコード

let xhr = new XMLHttpRequest();

xhr.open('GET', '/my/url');

xhr.send();

xhr.onload = function() {
  if (xhr.status != 200) { // HTTP error?
    // handle error
    alert( 'Error: ' + xhr.status);
    return;
  }

  // get the response from xhr.response
};

xhr.onprogress = function(event) {
  // report progress
  alert(`Loaded ${event.loaded} of ${event.total}`);
};

xhr.onerror = function() {
  // handle non-HTTP error (e.g. network down)
};

実際には、さらに多くのイベントがあり、最新の仕様 でそれら(ライフサイクルの順に)リストされています。

  • loadstart – リクエストが開始されました。
  • progress – レスポンスのデータパケットが到着しました。現時点でのレスポンスボディ全体が response にあります。
  • abort – リクエストは xhr.abort() の呼び出しによってキャンセルされました。
  • error – 接続エラーが発生しました。たとえば、間違ったドメイン名などです。404 などの HTTP エラーでは発生しません。
  • load – リクエストが正常に完了しました。
  • timeout – リクエストがタイムアウトのためキャンセルされました(設定されている場合のみ発生)。
  • loadendloaderrortimeout または abort の後にトリガーされます。

erroraborttimeout および load イベントは相互に排他的です。それらのうちの1つだけが発生します。

最もよく使用されるイベントは、ロード完了(load)、ロード失敗(error)です。または、単一の loadend ハンドラを使用して、リクエストオブジェクト xhr のプロパティを調べて何が起こったかを確認できます。

別のイベント:readystatechange はすでに見てきました。歴史的に、これは仕様が決定されるずっと前に登場しました。今日では、それを使用する必要はありません。新しいイベントで置き換えることができますが、古いスクリプトではよく見られます。

特にアップロードを追跡する必要がある場合は、xhr.upload オブジェクトで同じイベントをリッスンする必要があります。

チュートリアルマップ

コメント

コメントする前にこれを読んでください…
  • 改善するための提案がある場合は、コメントする代わりに GitHub の issue を送信するか、プルリクエストを送信してください。
  • 記事の内容が理解できない場合は、詳しく説明してください。
  • コードを数語挿入するには、<code> タグを使用し、複数行の場合は <pre> タグでラップし、10行以上場合はサンドボックス (plnkr, jsbin, codepen…) を使用してください。