JavaScript/XMLHttpRequest
XMLHttpRequest (XHR) とは、ウェブサーバと通信を行うためのJavaScriptのオブジェクトです。
fetch API編集
Ajax/XMLHttpRequestは、古くから使われている技術ですが、すでにAjax/XMLHttpRequestの欠点や問題点を解消した fetch API がモダンブラウザに実装されているので、新しく覚えるあるいは新規にコードを起こす場合は、Ajax/XMLHttpRequestではなく、 fetch APIを使うべきです。
fetch APIを使うと、下記のようなawait async promiseを使って非同期な並行・並列処理をメソッドチェインで簡素に表現できます。
- fetch.html
<!DOCTYPE html> <html> <head> <title>fetch API</title> <script> fetch('./hello.txt') .then(response => response.text()) .then(data => document.body.innerHTML += `<div>${data}<\/div>`) </script> </head> <body> <h1>XMLHttpRequest</h1> </body> </html>
Ajax編集
CGIなどの仕組みを使ってサーバからデータを取得するにはウェブページを遷移する必要がありますが、XMLHttpRequestを用いるとページの遷移なしに、動的かつ非同期にデータを取得することができます。このような技術はAjax (asynchronous JavaScript and XML) と呼ばれます。Ajaxを駆使するとJavaScriptを用いてGoogle マップのようなインタラクティブなウェブアプリケーションを構築することができます。
XMLHttpRequestはその名に反してXMLドキュメントでないデータもやり取りすることができます。また、HTTP以外にFTP通信も行うことができます[1](ローカルホストではローカルファイルも取得可能)。ただし、XMLHttpRequestを用いて通信できるのは同じドメインのサーバに限られるため、外部のサーバと通信をすることはできません。これを同一生成元ポリシー(どういつせいせいもとポリシー、same origin policy)といいます。
XMLHttpRequest編集
サンプルコードおよび前提の解説編集
まず、これらの機能は、webサーバを仲介して行う必要がある。よってコードをテストするには、まずwebサーバ(ApacheやXAMPPなど)を立ち上げて、そこのドキュメントルートで行う必要がある。ドキュメントルートの説明については、『PHP/確実に動作させるまで』に説明しておいたので、webサーバに詳しくなければ参照してもらいたい。(もしwebサーバの使い方が全く分からない場合、JavaScriptの学習はいったん中断して、上記リンク先のPHPの単元を先に学習したほうが良いだろう。)
とりあえず、サーバの設定や構成の準備がうまく行ってるかどうかを、次のコードで試してみよう。下記プログラムは、XMLHttpRequestの動作確認用のプログラムである。(本来なら動作確認のほかに、サーバとのデータ送受信などを行いたいのだが、コードが長くなるので、今回はまだ動作確認だけにしている。)
ただし、アクセス先のテキストファイルとして hello.txt を test.html と同じディレクトリに用意してもらいたい。 今後の作業のため、hello.txt には「Hello world!」と書いておく(単にテキストファイルや画像を読み込むだけなら HTML の通常の機能でも可能だが、今回は動作確認なので、そのような単純な事例にしている)。 なお、実用の場合には サーバのPHPプログラム(rubyなど別言語でも構わない)と、もっと複雑な通信をしたりするだろう。
- test.html
<!DOCTYPE html> <html> <head> <title>XMLHttpRequest</title> <script> const req = new XMLHttpRequest(); req.responseType = 'text'; const ReadyStateStrings = [] for (const property in XMLHttpRequest) { ReadyStateStrings[XMLHttpRequest[property]] = property } console.log(ReadyStateStrings) var start = Date.now() for (const event in ["loadstart", "load", "loadend", "error", "abort", "timeout"]) { req.addEventListener(event, ev => document.body.innerHTML += `<div>${Date.now() - start}(ms): event: ${event}<\/div>`) } req.addEventListener("readystatechange", (ev) => { document.body.innerHTML += `<div>${Date.now() - start}(ms): status = ${req.status}, readyState = ${ReadyStateStrings[req.readyState]}<\/div>` if (req.readyState === 4 && req.status === 200) { document.body.innerHTML += `<div>${Date.now() - start}(ms): hello.txt => ${req.responseText}<\/div>` } }); req.open('GET', './hello.txt'); req.send(null); </script> </head> <body> <h1>XMLHttpRequest</h1> </body> </html>
- 実行結果(ブラウザ)
XMLHttpRequest 41(ms): status = 200, readyState = HEADERS_RECEIVED 41(ms): status = 200, readyState = LOADING 43(ms): status = 200, readyState = DONE 43(ms): hello.txt => Hello world!
ミスの原因編集
通信に成功せずに表示結果が上記の成功例と異なる場合、なんらかの失敗をしている。
- よくあるミス
・ダブルクリック起動してしまっているミス
失敗する場合、原因は色々とありうるが、特にあるミスとして、 http://localhost/test.html のアクセスではなく、htmlファイルを直接ダブルクリックして起動してしまっていないかを注意してみよう。
単にドキュメントルートの中にhtmlを入れて、そのhtmlファイルをダブルクリック起動しても、失敗する。
必ず、ドキュメントルートにhtmlファイルを入れた上で、さらに必ず http://localhost/test.html にアクセスしているかを確認しよう。
・サーバ立ち上げていないミス
また、そもそもwebサーバが立ち上がっていない場合も当然ながら上記コードの動作確認には失敗するので、ローカルホスト自体http://localhostにもアクセスしてみて、確認してみよう。サーバが立ち上がっているなら、webサーバ(XAMPPまたはApacheなど)の用意している画面(ロゴマークなどがある画面)が表示されるハズであるので、その画面が表示されるか確認してみよう。
技術的な解説編集
HTTPコマンド編集
「GET」とは、HTTPコマンドの一種です。HTTPコマンドには、主にデータ取得を目的とするために通信する「GET」と、データ送信のために通信する「POST」という、2種類があります。
なお、GETもPOSTもどちらとも、相手先サーバとの通信のために、送信をしています。このため、原理的には「GET」で送信をすることも可能です[2]。もっとも、なるべく本来の目的にあったHTTPコマンドを選んで使うほうが良いプログラムではあるでしょう。
また、GETコマンドは送信URLの末尾にデータを付加して
?キー名=値&
の形式でデータ送信できるのですが、しかし日本語などの2バイト文字や「&」や「?」を正しく扱えません。
それらの文字は encodeURIComponent() 関数などでエンコードができます。
readyState プロパティ編集
readyState プロパティについては、次表のように仕様が決まっています。
readyState プロパティの意味 戻り値 意味 XMLHttpRequest.UNSENT(数値 0) オブジェクトが構築された。 XMLHttpRequest.OPENED(数値 1) open()メソッドが正常に呼び出されました。この状態では、setRequestHeader()を使ってリクエスト・ヘッダーを設定し、send()メソッドを使ってフェッチを開始することができます。 XMLHttpRequest.HEADERS_RECEIVED(数値 2) すべてのリダイレクト(もしあれば)が行われ、応答のすべてのヘッダーが受信されていること。 XMLHttpRequest.LOADING(数値 3) レスポンスボディの受信中です。 XMLHttpRequest.DONE(数値 4) データの転送が完了した、または転送中に何か問題が発生した(無限リダイレクトなど)。
HTTPステータスコードは、いくつもありますが、今回のコードに関係ありそうな主要なものは下記の通りです。下記の表以外にもいくつもありますが、要するに「200」であれば「成功」ですし、「200」以外なら通常は失敗です。
また、「XMLHttpRequest.DONE」でデータの転送が完了、あるいは転送中に何か問題が発生し(無限リダイレクトなど)異常終了です。
なお、普通の通信なら readyState プロパティは XMLHttpRequest.UNSENT → XMLHttpRequest.OPENED → XMLHttpRequest.HEADERS_RECEIVED → XMLHttpRequest.LOADING → XMLHttpRequest.DONE の順番で変化していきます(サンプルコードの表示結果もこのような順番になっています)。
status プロパティ編集
status プロパティの意味 戻り値 意味 200 成功 404 リクエストされたリソースが見つからない (Not Found)
URLなどが間違っているのが原因500 サーバーのエラー。サーバーダウンの場合もこれに該当。
上述の readyState プロパティおよび status プロパティの説明を合わせて考えれば、readyState 「4」および status 「200」ならば、とりあえず通信の環境構築には成功しています。
send()メソッドなど編集
コード末尾のほうにある send() メソッドは、ここでは「通信を開始せよ」程度の意味である。「GET」コマンドでも「POST」コマンドでも、どちらの場合でも、send() メソッドによって通信を開始する。
send() メソッドによって
xhr.onreadystatechange = function(){ }
で指定した関数の内部が実行される。なお onreadystatechange プロパティは「オン・レディ・ステイト・チェンジ」である。
document.write("
にある responseText プロパティは、通信で帰って来たテキストを意味するプロパティである。
" + xhr.responseText + "
");
高度なコード編集
- イベントリスナーによるイベントハンドリング
const req = new XMLHttpRequest(); req.addEventListener("progress", (ev) => { if (ev.lengthComputable) { const percent = ev.loaded / ev.total * 100; // 進捗表示 console.log("${percent}%転送しました。") } else { // 長さ不明 console.log("転送中。") } }); req.addEventListener("loadstart", (ev) => console.log("転送を開始しました。")); req.addEventListener("load", (ev) => console.log("転送しました。")); req.addEventListener("loadend", (ev) => console.log("転送が終了しました。")); req.addEventListener("error", (ev) => console.log("ファイルの転送中にエラーが発生しました。")); req.addEventListener("abort", (ev) => console.log("ユーザーが転送をキャンセルしました。")); req.addEventListener("timeout", (ev) => console.log("転送がタイムアウトしました。")); req.addEventListener("readystatechange", (ev) => { switch (req.readyState) { case XMLHttpRequest.UNSENT: console.log("UNSENT"); break; case XMLHttpRequest.OPENED: console.log("OPENED"); break; case XMLHttpRequest.HEADERS_RECEIVED: console.log("HEADERS_RECEIVED"); break; case XMLHttpRequest.LOADIND: console.log("LOADIND"); break; case XMLHttpRequest.DONE: const status = req.status; if (status === 0 || (status >= 200 && status < 400)) { console.log(req.responseText); } else { console.log(req.responseXML); console.log(req.responseText); } break; default : console.log(`${req.readyState} は仕様にないXMLHttpRequest.readyState`); } }); req.open('GET', './example.xml', false); req.send(null);
- ※ イベントリスナーは open() を呼び出す前に追加する必要があります(もし、そうしないのであれば progress イベントは発火しません)。
資料編集
XMLHttpRequestの静的プロパティ編集
- XMLHttpRequest.UNSENT
- 0 : number
- XMLHttpRequest.OPENED
- 1 : number
- XMLHttpRequest.HEADERS_RECEIVED
- 2 : number
- XMLHttpRequest.LOADING
- 3 : number
- XMLHttpRequest.DONE
- 4 : number
- XMLHttpRequest.arguments
- null : object
- XMLHttpRequest.caller
- null : object
- XMLHttpRequest.length
- 0 : number
- XMLHttpRequest.name
- "XMLHttpRequest" : string
XMLHttpRequestのインスタンスプロパティ 編集
- XMLHttpRequest.prototype.DONE
- XMLHttpRequest.prototype.HEADERS_RECEIVED
- XMLHttpRequest.prototype.LOADING
- XMLHttpRequest.prototype.OPENED
- XMLHttpRequest.prototype.UNSENT
- XMLHttpRequest.prototype.abort
- XMLHttpRequest.prototype.constructor()
- XMLHttpRequest.prototype.getAllResponseHeaders
- XMLHttpRequest.prototype.getResponseHeader
- XMLHttpRequest.prototype.onreadystatechange
- XMLHttpRequest.prototype.open
- XMLHttpRequest.prototype.overrideMimeType
- XMLHttpRequest.prototype.readyState
- XMLHttpRequest.prototype.response
- XMLHttpRequest.prototype.responseText
- XMLHttpRequest.prototype.responseType
- XMLHttpRequest.prototype.responseURL
- XMLHttpRequest.prototype.responseXML
- XMLHttpRequest.prototype.send
- XMLHttpRequest.prototype.setRequestHeader
- XMLHttpRequest.prototype.status
- XMLHttpRequest.prototype.statusText
- XMLHttpRequest.prototype.timeout
- XMLHttpRequest.prototype.upload
- XMLHttpRequest.prototype.withCredentials
脚注編集
- ^ 2021年6月の時点でディフォルトでFTPスキームを(かつてのgopherのように)無効にするブラウザが増えつつある。https://blog.chromium.org/2020/09/chrome-86-improved-focus-highlighting.html
- ^ 山田、P398