【STEP.37】
fetch・XMLHttpRequest・JavaScript間の通信をマスターしよう!

fetch

fetchとは

従来のXMLHttpRequestオブジェクトに代わって、モダンな非同期通信の標準メソッドとして定着しつつあるのがfetchメソッドです。

fetchメソッドは、「Promiseを前提としているため、ES20XXとも相性が良い」「$.ajaxとも似ているため、従来のjQueryに慣れた人にとっても馴染みやすい」などの特徴があります。

fetchメソッドを使って非同期的にデータを取得する

fetchメソッドの構文は次の通りです。

fetchメソッド

fetch(input [, opts])
input  取得したいリソース
opts   リクエストオプション(表1-1

引数optsに指定できるリクエストオプションの主なものは次に掲げる表の通りです。

【主なリクエストオプション(引数opts)】(表1-1)
オプション概要
methodリクエストメソッド(GET/POSTなど。規定はGET)
headersリクエストヘッダー(Headerオブジェクト。または「キー名:値」のハッシュ形式)
bodyリクエスト本体(FormData、URLSearchParamsなど)
modeリクエストモード
credentials機密情報の送信ルール
cacheキャッシュモード(設定値はno-store、reload、no-cache、force-cache、only-if-cachedなど)
redirectリダイレクトの処理方法(設定値はfollow、manualなど)
referrerPolicyRefererヘッダーを送信するか(設定値はno-referrer、same-origin、strict-origin、unsafe-urlなど)
integrity取得するリソースのハッシュ値

たとえば以下は、指定されたページを非同期に読み込み<div id=”result”>要素に反映する例です。

<input id="btn" type="button" value="現在日時" />
<div id="result"></div>
document.getElementById('btn').addEventListener('click', function (e) {
  fetch('fetch.php')
    .then(function (response) {
      return response.text();
    })
    .then(function (data) {
      document.getElementById('result').textContent = data;
    });
}, false);
<?php
print('現在日時:'.date('Y年m月d日 H:i:s'));
?>

fetchメソッドは、非同期通信の結果をPromise<Response>Responseオブジェクトを含んだPromiseオブジェクト)として返します。

これを処理するのがthenメソッドです。

コールバック関数の引数responseは、Promise経由で渡されたResponseオブジェクトです。

ここでは、そのtextメソッドにアクセスして応答データを文字列(Promise<string>)として取得しているわけです。

あとは、上のJavaScriptのコード6~8行目でさらに文字列を取り出し、<div id=”result”>要素に反映しています。

文章として表すと複雑に見えるかもしれませんが、(1)非同期通信の開始、(2)レスポンスの形式に応じてデータを取り出す、(3)データをページに反映させる、という流れはおおよそ定石なので、まず決まり文句として覚えてしまうことを推奨します。

なお、引数inputには、Requestオブジェクトを渡すこともできます。

Requestコンストラクター

new Request(input [,opts])
input  取得したいリソース
opts   リクエストオプション(表1-1

そのため、サンプルの「fetch('fetch.php')」(上のJavaScriptのコード2行目)は以下のように表しても同じ意味です。

fetch(new Request('fetch.php'))

Internet Explorer(IE)でfetchメソッドを使う方法

fetchメソッドは、Internet Explorer(IE)には対応していません。

IE環境までサポートする場合には、旧来のXMLHttpRequestオブジェクトを利用するか、fetch-polyfillを利用する必要があります。

ポリフィル(polyfill)とは、ブラウザーに不足している機能を補うためのライブラリです。

利用にあたっては、.htmlファイルから以下のライブラリをインポートします。

<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.4/fetch.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/promise-polyfill@8/dist/polyfill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/url-search-params-polyfill@4.0.1/index.min.js"></script>

promise-polyfillは、promiseの機能を補うためのポリフィルで、fetch-polyfillを利用する際に必要となります。

url-search-params-polyfillは、URLSearchParamsのためのポリフィルなので、URLSearchParamsを利用しない場合には不要です。

fetchメソッドを使った非同期通信で通信エラーが発生した場合

以下は、ボタンクリックで指定されたファイルにアクセスし、その内容をページに反映する例です。

ただし、ここではエラーの挙動を確認するために、アクセス先には存在しないファイル(nothing.php)を指定するものとします。

<input id="btn" type="button" value="ファイル表示" />
<div id="result"></div>
document.getElementById('btn').addEventListener('click', function (e) {
  fetch('nothing.php')
    .then(function (response) {
      if (response.ok) {
        return response.text();
      }
      throw new Error('指定されたファイルは存在しません。');
    })
    .then(function (data) {
      document.getElementById('result').textContent = data;
    }) 
    .catch(function (error) {
      window.alert('Error:' + error.message);
    });	
}, false);

fetchメソッドはネットワークエラーに対してTypeErrorを返します。

404(Not Found)はいわゆるネットワークエラーとは見なされません。

そのため、上のJavaScriptのコード4~7行目でもokプロパティで成功レスポンス(200~299)が返されたかをチェックし、成功であれば応答データを取得し、さもなくば明示的にエラーをスローしています。

エラーを処理するのがcatchメソッドです。(上のJavaScriptのコード12~14行目)

コールバック関数の引数はスローされたエラーを受け取るので、ここでは、そのmessageプロパティ(エラーメッセージ)をログ出力しています。

Responseオブジェクトは、他にも次に掲げる表のようなメンバーを提供しています。

【Responseオブジェクトのメンバー】
分類メンバー概要
ステタスok成功したか
redirectedレスポンスがリダイレクトの結果であるか
statusHTTPステータスコード
statusTextステータスメッセージ
ヘッダheadersヘッダー情報
typeレスポンスタイプ
urlレスポンスのURL
本文bodyレスポンス本体を取得(ReadableStreamオブジェクト)
arrayBuffer()ArrayBufferとして取得
blob()Blobとして取得
formData()FormDataとして取得
json()JSON形式で取得
text()テキストとして取得

URLSearchParamsを使ってクエリ情報を動的に組み立てる

以下のサンプルコードは、[氏名]欄から入力した名前に基づいて、サーバー側で「こんにちは、○○さん!」というメッセージを組み立て、ページに反映する例です。

<form>
  <label for="name">氏名:</label>
  <input id="name" name="name" type="text" size="20" />
  <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
let result = document.getElementById('result');
document.getElementById('btn').addEventListener('click', function (e) {
  // クエリ情報を生成
  let params = new URLSearchParams();
  params.set('name', document.getElementById('name').value);
  // クエリ情報を付与してリクエストを開始
  fetch('fetch_query.php?' + params.toString())
    .then(function (response) {
      return response.text();
    })
    .then(function (text) {
      result.textContent = text;
    });	
}, false);
<?php
  $name = htmlspecialchars($_GET['name'],ENT_QUOTES | ENT_HTML5, 'UTF-8');
  if ($name !== '') {
    print('こんにちは、'.$name.'さん!');
  }
?>

URLSearchParamsオブジェクトは、クエリ情報をキー/値の組み合わせで管理します。

次に掲げる表は、URLSearchParamsオブジェクトの主なメソッドです。

【URLSearchParamsオブジェクトの主なメソッド】
メソッド概要
append(name,value)キーname/値valueを新しい検索パラメーターとして追加
delete(name)キーnameの値を削除
get(name)指定されたキーnameにマッチする最初の値を取得
getAll(name)指定されたキーnameにマッチするすべての値を取得
has(name)指定されたキーnameが存在するか
set(name,value)キーnameに値valueを設定

params.set('name', document.getElementById('name').value);」(上のJavaScriptのコード5行目)では、setメソッドを利用して[氏名]欄の値を1つ追加していますが、もちろん、同じ要領で複数の値を追加することも可能です。

準備ができたら、あとはtoStringメソッドを呼び出すことで、「キー=値&...」形式のクエリ情報を取得できます。

マルチバイト文字列など、クエリ情報として利用できない文字は自動的にエスケープ処理されるので、encodeURIComponent関数などの呼び出しは不要です。

以下は、生成されたクエリ情報付きURLの例です。

http://localhost/fetch/fetch_query.php?name=%E6%A8%AA%E6%B5%9C%E6%B5%81%E6

HTTP POSTでデータを送信する場合

fetchメソッドは、既定でHTTP GETを利用して通信します。

HTTP GETでもクエリ情報を介することでデータの送信は可能ですが、送信サイズに制限があります。

具体的な制限は環境に依りますが、一般的には数百バイトを超えるデータを送信する場合には、HTTP POSTの利用を推奨します。

以下は、HTTP POSTを利用してデータを送信する例です。

<form id="myform">
  <label for="name">氏名:</label>
  <input id="name" name="name" type="text" size="20" />
  <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
document.getElementById('btn').addEventListener('click', function (e) {
  // フォームからの入力を取得
  let data = new FormData(document.getElementById('myform'));
  // HTTP POST経由でデータを送信
  fetch('fetch_post.php?', {
    method: 'POST',
    body: data,
  })
  .then(function (response) {
    return response.text();
  })
  .then(function (text) {
    document.getElementById('result').textContent = text;
  });	
}, false)
<?php
  $name = htmlspecialchars($_POST['name'],ENT_QUOTES | ENT_HTML5, 'UTF-8');
  if ($name !== '') {
    print('こんにちは、'.$name.'さん!');
  }
?>

FormDataは、multipart/form-data形式のフォームデータを表現するためのオブジェクトで、まさにフォームの内容をfetch経由で送信するのに適しています。

FormDataにデータを渡すには、コンストラクターに<form>要素を渡すだけです。

これによって、フォームの内容をまとめてFormData化できます。

もちろんappendメソッドを利用することで、手動でデータを組み立ててもかまいません。

あとは、methodパラメーターとしてPOST、bodyパラメーターに生成したフォームデータを渡すことで、データをポストできます。

bodyパラメーターにURLSearchParamsオブジェクトを渡す場合

bodyパラメーターには、URLSearchParamsオブジェクトを渡すこともできます。

これには、「let data = new FormData(document.getElementById('myform'));」(上のJavaScriptのコード3行目)を以下のように書き換えます。

let data = new URLSearchParams();
data.append('name', document.getElementById('name').value);

appendメソッドには「名前」「値」の組み合わせで、送信すべきパラメーターを引き渡します。

クロスオリジン通信

クロスオリジン制約とは

セキュリティ上の理由から、ブラウザーではJavaScriptによる別オリジンへのアクセスを制限しています。

これをクロスオリジン制約といいます。

しかし、サードベンダーのサービスからデータを取得するなどの目的で、オリジンをまたがって、非同期通信を実施したいという状況はよくあります。

そのような場合によく利用されるのが、プロキシCORSJSONPなどの技術です。

プロキシ

まずは最もクラシカルなプロキシについて解説します。

プロキシとは、JavaScriptの代わり(Proxy)として、外部サービスにアクセスするためのサーバースクリプトのことです。

JavaScriptでは、あとは同一のオリジンにあるサーバースクリプトに対してアクセスすることで、疑似的に外部サービスにアクセスできます。

以下では、プロキシの例として、郵便番号検索API(http://zip.cgis.biz/)から郵便番号に対応する住所を取得し、その結果をページに反映する例です。

 <form>
   <label for="zip">郵便番号:</label>
   <input id="zip" type="text" size="20" />
   <input id="btn" type="button" value="検索" />
 </form>
 <div id="result"></div>
// [検索]ボタンクリック時に住所を検索
document.getElementById('btn').addEventListener('click', function (e) {
  let zip = document.getElementById('zip');
  // サーバー側にクエリ情報[?zip=~]で郵便番号を引き渡す
  fetch('fetch_get.php?zip=' + zip.value)
    .then(function (response) {
      return response.text();
    })
    .then(function (data) {
    // 成功時に、その結果を解析&ページに反映
    let parser = new DOMParser();
    let xml = parser.parseFromString(data, 'text/xml');
    let state = xml.querySelector('value[state]').getAttribute('state');
    let city = xml.querySelector('value[city]').getAttribute('city');
    let address = xml.querySelector('value[address]').getAttribute('address');
    document.getElementById('result').textContent = state + city + address;
  });
}, false);
<?php
  // 文字コードを宣言
  mb_http_output('UTF-8');
  mb_internal_encoding('UTF-8');
  header('Content-Type: text/xml;charset=UTF-8');
  // 郵便番号検索APIにアクセスして、住所情報を取得
  print(file_get_contents('http://zip.cgis.biz/xml/zip.php?zn='.$_GET['zip']));
?>

fetch_get.phpはプロキシ(代理)なので、郵便番号検索APIから得た結果をそのまま出力するだけです。

ただし、textメソッド(return response.text();)で取得しただけではプレーンなテキストなので、XMLとして処理するには、DOMParserオブジェクトのparseFromStringメソッドを利用します。

parseFromStringメソッド

parser.parseFromString(xml, type)
xml   XML文字列
type  コンテンツタイプ(text/xml、text/htmlなど)

parseFromStringメソッドの戻り値は、Documentオブジェクトです。

あとは、querySelectorメソッドで、それぞれstatecityaddress属性を持つ<value>要素を検索し、その値を連結したものを住所として組み立てています。

cors

CORS(Cross-Origin-Resource-Sharing)は、オリジンをまたがってデータを受け渡しするための仕組みです。

クライアント/サーバー双方の対応が必要となりますが、W3Cで標準化されている仕様なので、環境が許すならば積極的に活用していくことをお勧めします。

以下は、クロスオリジン対応する例です。

動作させるにあたっては、サーバーサイドのfetch_mode.phpは、クライアントサイド(fetch_mode.htmlfetch_mode.js)と異なるサーバーに配置してください。

クライアントサイドの準備

fetchメソッドのmodeオプションに「'cors'」を指定します。

<input id="btn" type="button" value="現在日時" />
<div id="result"></div>
document.getElementById('btn').addEventListener('click', function (e) {
  // 別サーバーへのリクエストを送信
  fetch('https://yutamanshop.com/fetch/fetch_mode.php', {
    mode:'cors'
  })
  .then(function (response) {
    return response.text();
  })
  .then(function (data) {
    document.getElementById('result').textContent = data;	
  });
},false);

設定値「cors」は別オリジンへの通信を許可することを意味します。(既定値なので、省略しても同じ意味です。)

ちなみに、同じオリジンしか通信できないように宣言するには、「same-origin」とします。

サーバーサイドの準備

サーバーサイドでもクライアントサイド側のオリジンを明示的に許可しなければなりません。

これをを行うのが、Access-Control-Allow-Originヘッダーの役割です。

<?php
  $parsed = parse_url($_SERVER['HTTP_REFERER']);
  header('Access-Control-Allow-Origin:'.$parsed['scheme'].'://'.$parsed['host']);
  print('現在日時:'.date('Y年m月d日H:i:s'));
?>

上のphpのソースコードは、

リファラー(リンク元アドレス)をもとに、「Access-Control-Allow-Origin:http://localhost」のような応答ヘッダーを生成する(localhostは許可するオリジン)

という意味です。

ここでは、リファラーをもとに許可するオリジンを設定していますが、特定のオリジンのみを許可したい場合には、許可するオリジンを明示的に指定します。

また、「http://localhost」の部分を「*」とした場合には、無条件にすべてのオリジンを許可します。

「fetch_mode.js」のmodeオプションを「same-origin」にすると

Fetch API cannot load
https://yutamanshop.com/fetch/fetch_mode.php. Request mode is "same-progin" but the URL's origin is not same as the request origin http://localhost.

のようなエラーとなることと、modeオプションを「cors」にした状態でAccess-Control-Allow-Originヘッダーを削除した場合には、

Failed to load https://yutamanshop.com/fetch/fetch_mode.php: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

のようなエラーになることが確認できます。

cors(通信時にクッキーを送信する場合)

credentialsパラメーターを設定します。

指定可能な値は、次に掲げる表の通りです。

既定はomitで、fetchメソッドはクッキーを送信しません。

【credentialsパラメーターの設定値】
設定値概要
omitクッキーを送信しない
same-origin同一オリジンの場合にだけ送信
includes常に送信

以下は、クロスオリジンでクッキーを送信する例です。

<input id="btn" type="button" value="現在日時" />
<div id="result"></div>
document.getElementById('btn').addEventListener('click', function (e) {
  // ボタンクリック時にリクエストを送信
  fetch('https://yutamanshop.com/fetch/fetch_cred.php', {
    mode:'cors',
    // クッキーの送信を有効化
    credentials:'includes',
  })
  .then(function (response) {
    return response.text();
  })
  .then(function (data) {
    document.getElementById('result').innerHTML = data;	
  });
},false);
<?php
  // クロスオリジン通信とクッキーの授受を有効化
  $parsed = parse_url($_SERVER['HTTP_REFERER']);
  header('Access-Control-Allow-  Origin:'.$parsed['scheme'].'://'.$parsed['host']);
  header('Access-Control-Allow-Credentials: true');
  setcookie('name','R.Yokohama', time() + 60*60*24*180);
  print('現在日時:'.date('Y年m月d日H:i:s').'<br />');
  // 取得したクッキーの値を取得
  print('クッキー値:'.htmlspecialchars($_COOKIE['name']));
?>

クッキーを授受するには、サーバー側でもAccecc-Control-Allow-Originに加えて、「Accecc-Control-Allow-Credentials: true」ヘッダーを付与しなければなりません。

また、Accecc-Control-Allow-Credentialsを利用する際には、「Accecc-Control-Allow-Origin:*」(ワイルドカード)は許されない点にも注意が必要です。

実際に、fetch_cred.phpにアクセス、[現在時刻]ボタンを複数回押して、2度目以降のアクセスでクッキーが取得できることを確認してください。(credentialsオプションをコメントアウトした場合、クッキーは送信されなくなります。)

また、credentialsオプションは「includes」のままに、Accecc-Control-Allow-Credentialsヘッダーを削除した場合には、以下のようなエラーになることも確認しましょう。

Failed to load https://yutamanshop.com/fetch/fetch.cred.php: The value of the 'Access-Control-Allow-Credentials' header in the response is which must be 'true' when the request's credentials mode is 'include'. Origin 'http://localhost' is therefore not allowed access.

JSONP

オリジンをまたがって通信するためのもう一つの手段が、JSONP(JSON with Padding)です。

JSONPとは、JavaScriptのオブジェクト形式(JSON)でデータを交換する仕組みのことです。

本来、非同期通信機能を担当しているfetchメソッドを利用しないため、クロスオリジンの制約を受けないのが特徴です。

JavaScript標準では、残念ながら、JSONPのための機能は提供していないませんが、fetch-jsonp(https://github.com/camsong/fetch-jsonp)というライブラリを利用することで、fetchライクな構文でJSONPを利用できます。

本家サイトから[Clone or download][Download ZIP]でライブラリ一式をダウンロードできるので、配下の/build/fetch-jsonp.jsをアプリフォルダ配下に配置します。

具体的な利用例も見てみます。

以下は、はてなブックマークエントリー情報取得API(http://developer.hatena.ne.jp/ja/documents/bookmark/apis/getinfo)を利用して、指定されたURLに付けられたはてなブックマークの件数とコメントを表示する例です。

<form>
  <label for="url">URL:</label>
  <input id="url" type="text" size="100" value="http://" />
  <input id="btn" type="button" value="検索" />
</form>
<div id="count">-</div>
<ul id="comment"></ul>
<!--fetch-jsonpをインクルード-->
<script src="./fetch-jsonp.js"></script>
// [検索]ボタンクリックで検索を開始
document.getElementById('btn').addEventListener('click', function (e) {
  let url = document.getElementById('url');
		
  fetchJsonp('http://b.hatena.ne.jp/entry/jsonlite/?url=' + url.value, {
    timeout: 7000,
  })
  .then(function (response) {
    return response.json();
  })
  .then(function (data) {
  // 取得したデータをページに反映
    document.getElementById('count').textContent = `${data.count}件`;
    let frag = document.createDocumentFragment();
    for (let bk of data.bookmarks) {
      let c = bk.comment;
      if (c !== '') {
        let li = document.createElement('li');
        li.textContent = c;
        frag.appendChild(li);
      }
    }
    document.getElementById('comment').appendChild(frag);
  })
  // 失敗時の処理
  .catch( function (ex) {
    console.log('例外発生' + ex);
  });
}, false);

fetchJsonpメソッドの用法は、fetchメソッドとよく似ています。

ただし、第2引数で指定できるオプションは、次に掲げる表の範囲に限定されます。

【fetchJsonpメソッドの動作オプション】
オプション名概要既定値
jsonpCallbackリクエストURLに付与するクエリ情報のキー名callback
jsonpCallbackFunction内部的に生成される関数名jsonp_<乱数>
timeoutタイムアウト値5000

通信の結果をthenメソッドで受けて、Responseオブジェクトのjsonメソッドでデータ本体を取得&処理するという流れは、fetchメソッドの場合と同じです。

得られる情報はもちろん利用しているサービスによって異なりますが、「はてなブックマークエントリー情報取得API」であれば、次のような内容が含まれます。

上のJavaScriptのコード15~21行目では、data.bookmarks配列を順に処理して、配下のユーザーコメントを列記しています。

XMLHttpRequest

XMLHttpRequestとは

モダンなブラウザーで非同期通信機能を実装するには、ほとんどの場合、fetchメソッドを利用するのが良い選択肢です。

ただし、現時点では、Internet Explorer 11(IE)がfetchメソッドに未対応である点に注意が必要です。(各ブラウザのfetchメソッドの対応状況については【Can I use?】をご覧ください。)

IE(もしくは、その他のfetchメソッドを未サポートの古いブラウザ)をサポートするならば、fetch-polyfillを利用するか、以前から利用されているXMLHttpRequestオブジェクトを利用します。

以下は、XMLHttpRequestオブジェクトを使用して、[氏名]欄から入力した名前に基づいて、サーバー側で「こんにちは、○○さん!」というメッセージを組み立て、ページに反映する例です。

<form>
  <label for="name">氏名:</label>
  <input id="name" name="name" type="text" size="20" />
  <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
let result = document.getElementById('result');
let xhr = new XMLHttpRequest();
	
// 通信成功時の処理
xhr.addEventListener('load', function () {
  result.textContent = xhr.responseText;
}, false);
	
// 通信エラー時の処理
xhr.addEventListener('error', function () {
  result.textContent = '通信時にエラーが発生しました。';
} ,false);
	
// [送信]ボタンで非同期通信を開始
document.getElementById('btn').addEventListener('click', function (e) {
  xhr.open('GET', 'fetch_query.php?name=' + encodeURIComponent(document.getElementById('name').value), true);
  xhr.send(null);
}, false);
<?php
  $name = htmlspecialchars($_GET['name'],ENT_QUOTES | ENT_HTML5, 'UTF-8');
  if ($name !== '') {
    print('こんにちは、'.$name.'さん!');
  }
?>

XMLHttpRequest(XHR)では、一般的に以下の流れで通信を実行します。

  1. 通信成功(load)、失敗(error)時のリスナーを登録する
  2. openメソッドで通信を初期化
  3. sendメソッドでリクエストを送信

loadイベントリスナー(JavaScriptのコード5~7行目)では、responseTextプロパティを介して結果データを取得できます。

ここでは、取得したテキストをそのままページに反映しているだけですが、もしもJSONデータで処理するならば、JSON.encodeメソッドでオブジェクト化してから、目的のプロパティにアクセスすることになります。

XMLデータであれば、responseXMLプロパティを利用することで、解析した結果(Documentオブジェクト)を取得できます。

openメソッド(JavaScriptのコード16行目)の構文は、以下の通りです。

openメソッド

xhr.open(method, url [,async [,user [,passwd]]])
method  HTTPメソッド(GET/POST/PUT/DELETEなど)
url     通信先のURL
async   非同期通信か(既定はtrue)
user    認証時のユーザー名
passwd  認証時のパスワード

次に、クエリ情報を組み立てる必要がありますが、IEでは クエリ情報を動的に組み立てるURLSearchParamsが未対応なので、文字列としてクエリ情報を組み立てます。

その場合、クエリ情報として利用できない文字(マルチバイト文字など)も自分でencodeURIComponent関数を呼び出してエンコードしなければならない点に注意が必要です。

また、openメソッドはあくまでもリクエストを初期化するだけで、まだ送信はしていません。

sendメソッドを呼び出すのを忘れないように注意が必要です。

sendメソッドの引数は、リクエスト本体を表すもので、HTTP POSTによる通信時にだけ指定できます。

HTTP GET通信ではnullを渡しておきます。

XHRオブジェクトの主なメンバー

XHRで利用できる主なメンバーを、次に掲げる表にまとめます。

【XMLHttpRequestオブジェクトの主なメンバー】
分類メンバー概要
プロパティresponse応答本体
responseType応答の型
responseText応答本体(プレーンテキスト)
responseXML応答本体(Documentオブジェクト)
status応答ステータスコード
statusText応答ステータスの詳細メッセージ
timeoutリクエストのタイムアウト時間
readyState非同期通信の状態を取得
withCredentialsクロスオリジン通信に際して機密情報を送信するか
メソッドopen(…)HTTPリクエストを初期化
send(body)HTTPリクエストを送信(引数bodyは要求本体)
setRequestHeader(name,value)リクエスト時に送信するヘッダーを追加
abort()非同期通信を中断
getAllRequestHeaders()すべての応答ヘッダーを取得
getResponseHeader(name)指定した応答ヘッダーを取得
イベントloadstartリクエストを送信したとき
progressデータを送受信している途中
abortリクエストがキャンセルされたとき
loadリクエストが成功したとき
errorリクエストが失敗した時
loadend成功/失敗に関わらず、リクエストが完了したとき
timeoutリクエストがタイムアウトしたとき

fetchメソッドと異なり、CORSのためのオプションはありません。(サーバー側のヘッダ設定のみでCORSを有効化できます。)

ただし、クロスオリジンでクッキーを送信する場合には、明示的にwithCredentialsプロパティをtrue(有効化)としておく必要があります。

非同期通信でデータをポストする

multipart/form-data形式、もしくはJSON形式で送信する必要があります。

まずは、 multipart/form-data形式の例から見ていきます。

multipart/form-data形式で送信する

application/x-www-form-urlencodedは標準的な<form>要素が利用できるデータ形式です。

「キー名=値&…」の形式でポストデータを表します。

<form id="myform">
  <label for="name">氏名:</label>
  <input id="name" name="name" type="text" size="20" />
  <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
let result = document.getElementById('result');
let xhr = new XMLHttpRequest();
	
// 通信成功時の処理
xhr.addEventListener('load', function () {
  result.textContent = xhr.responseText;
}, false);
	
// 通信エラー時の処理
xhr.addEventListener('error', function () {
  result.textContent = '通信時にエラーが発生しました。';
}, false);
	
// [送信]ボタンクリックで通信を開始
document.getElementById('btn').addEventListener('click', function (e) {
  xhr.open('POST', 'fetch_post.php', true);
  xhr.setRequestHeader('content-type',
'application/x-www-form-urlencoded;carset=UTF-8');
  xhr.send('name=' + encodeURIComponent(document.getElementById('name').value));
}, false);
<?php
  $name = htmlspecialchars($_POST['name'],ENT_QUOTES | ENT_HTML5, 'UTF-8');
  if ($name !== '') {
    print('こんにちは、'.$name.'さん!');
  }
?>

リクエストのデータ形式はsetRequestHeaderメソッド(JavaScriptのコード17~18行目)で、データ本体はsendメソッド(JavaScriptのコード19行目)で、それぞれ設定します。

JSON形式で送信する

sendメソッドに対して、JSON.stringifyメソッドで交換したJSON文字列を渡します。

この場合、content-typeヘッダーも’application/json’とします。

<form id="myform">
  <label for="name">氏名:</label>
  <input id="name" name="name" type="text" size="20" />
  <input id="btn" type="button" value="送信" />
</form>
<div id="result"></div>
let data = { mid: 'y001', name: '横浜流星', age: 23 };
let xhr = new XMLHttpRequest();
	
// 通信成功時の処理
xhr.addEventListener('load', function () {
  console.log(xhr.responseText);
}, false);

// 通信エラー時の処理
xhr.addEventListener('error', function () {
  console.log('通信時にエラーが発生しました。');
}, false);
	
xhr.open('POST','fetch_json.php', true);
xhr.setRequestHeader('content-type','application/json');
xhr.send(JSON.stringify(data));
<?php
  $name = htmlspecialchars($_POST['name'],ENT_QUOTES | ENT_HTML5, 'UTF-8');
  if ($name !== '') {
    print('こんにちは、'.$name.'さん!');
  }
?>

XML形式のデータを取得する

responseXmlプロパティを利用します。

以下は、XMLHttpRequest(XHR)オブジェクトを用いて、郵便番号検索API(http://zip.cgis.biz/)から郵便番号に対応する住所を取得し、その結果をページに反映する例です。

<form>
  <label for="zip">郵便番号:</label>
  <input id="zip" type="text" size="20" />
  <input id="btn" type="button" value="検索" />
</form>
<div id="result"></div>
let zip = document.getElementById('zip');
let xhr = new XMLHttpRequest();
let result = document.getElementById('result');
	
// 通信成功時には、取得したデータをページに反映
xhr.addEventListener('load', function () {
   let xml = xhr.responseXML;
  let state = xml.querySelector('value[state]').getAttribute('state');
  let city = xml.querySelector('value[city]').getAttribute('city');
  let address = xml.querySelector('value[address]').getAttribute('address');
  document.getElementById('result').textContent = state + city + address;
}, false);

// エラー時にはエラーメッセージを表示
xhr.addEventListener('error', function () {
  result.textContent = '通信時にエラーが発生しました。';
}, false);
	
// [検索]ボタンクリック時に住所を検索
document.getElementById('btn').addEventListener('click', function (e) {
  // サーバー側にクエリ情報[?zip=~]で郵便番号を引き渡す
  xhr.open('GET', 'fetch_get.php?zip=' + encodeURIComponent(document.getElementById('zip').value), true);
  xhr.send(null);
}, false);
<?php
  mb_http_output('UTF-8');
  mb_internal_encoding('UTF-8');
  header('Content-Type: text/xml;charset=UTF-8');
  // 郵便番号検索APIにアクセスして、住所情報を取得
  print(file_get_contents('http://zip.cgis.biz/xml/zip.php?zn='.$_GET['zip']));
?>

fetchメソッドと異なり、XHRオブジェクトではresponseXmlプロパティがXMLデータをオブジェクト化してくれるので、アプリ側では解析の手間は不要です。

そのままquerySelectorなどのメソッドを利用してXMLデータから個々の要素にアクセスできます。

fetch-jsonpとは

fetch-jsonpは、ES2015で導入されたPromiseオブジェクトのサポートを前提としています。

Promise未対応のブラウザーでJSONPを実装するには、以下のようなコードを書きます。

以下のコードは、 はてなブックマークエントリー情報取得API(http://developer.hatena.ne.jp/ja/documents/bookmark/apis/getinfo)を利用して、指定されたURLに付けられたはてなブックマークの件数とコメントを表示する例です。

fetch-jsonpを利用するので、まず初めに本家サイト(https://github.com/camsong/fetch-jsonp)から[Clone or download][Download ZIP]でライブラリ一式をダウンロードし、配下の/build/fetch-jsonp.jsをアプリフォルダ配下に配置する必要があります。

<form>
  <label for="url">URL:</label>
  <input id="url" type="text" size="100" value="http://" />
  <input id="btn" type="button" value="検索" />
</form>
<div id="count"></div>
<ul id="comment"></ul>
<!--fetch-jsonpをインクルード-->
<script src="./fetch-jsonp.js"></script>
document.getElementById('btn').addEventListener('click', function (e) {
  let url = document.getElementById('url');
  let script = document.createElement('script');
  script.src = 'http://b.hatena.ne.jp/entry/jsonlite/?callback=mycallback&url=' + url.value;
  // サーバーからの応答によって呼び出されるコールバック関数
  window.mycallback = function (data) {
    document.getElementById('count').textContent = data.count + '件';
    let frag = document.createDocumentFragment();
    // ブックマーク情報を<li>要素に整形
    for (let i = 0; i < data.bookmarks.length; i++) {
      let c = data.bookmarks[i].comment;
      if (c !== '') {
        let li = document.createElement('li');
        li.textContent = c;
        frag.appendChild(li);
      }
    }
    document.getElementById('comment').appendChild(frag);
    // 要素になった<script>要素を破棄
    document.body.removeChild(script);
  };
   document.body.appendChild(script);
});

mycallback関数(JavaScriptのコード6~21行目)は、取得したデータをもとに<li>リストを組み立てるだけで、thenメソッドと中身は同じです。

ここでは、サービスへの通信を担うコード(JavaScriptのコード3~4行目)に注目です。

JavaScriptのコード3~4行目では、以下のような<script>要素を組み立てています。

http://b.hatena.ne.jp/entry/jsonlite/?callback=mycallback&url=http://amazon.com/

「はてなブックマークエントリー情報取得API」は、クエリ情報callback関数で指定された関数名と検索結果で、

mycallback({...})

のようなコードを返します。

MEMO
fetch-jsonp
fetch-jsonpでは、内部的にクエリ情報callbackを付与しています。

よって、アプリ開発者が意識することはありませんでした。

つまり、「ボタンクリックによって<script>要素を埋め込む」ということは、「ボタンクリックでmycallback関数を呼び出しなさい」という意味になります。

mycallback関数の引数dataには検索結果(オブジェクト)が渡されるので、あとはこれを処理していくだけです。

また、繰り返しアクセスした場合に、<script>要素が増殖するのを防ぐために、処理後は<script>要素を破棄します。(document.body.removeChild(script);

JavaScript間の通信

バックグラウンドでJavaScriptのコードを実行する【ワーカー編】

従来、JavaScriptはすべてのコードが単一のUIスレッド上で動作していたため、「重い」処理が発生すると、ページの動作が寸断されてしまうということがよくありました。

しかし、Web Workersという機能を利用することで、指定されたJavaScriptのコードをバックグラウンドで実行できるようになります。

これによって、重い処理が実行されている間も、ユーザーはブラウザー上での操作を継続できます。

以下は、1~指定された値(target)の間にいくつnumの倍数があるかを求めるためのコードです。

倍数の個数を算出するためのコードをワーカーとして切り出します。

ワーカーとは、バックグラウンドで動作するJavaScriptのコードのことです。

ワーカーは、メインのJavaScriptとは独立したファイルとして用意します。

// messageイベントによる処理
self.addEventListener('message', function (e) {
  let count = 0; // 個数カウント
  // 1~targetの間でnumで割り切れる数があるかをチェック
  for (let i = 1; i < e.data.target; i++) {
    if (i % e.data.num === 0) { count++; }
  }
  // カウントした結果をメイン処理に送信
  postMessage(count);
});

messageイベントは、メインの処理からメッセージを受け取った(=ワーカーが起動された)タイミングで呼び出されます。

ワーカーの処理は、messageイベントリスナーとして表すのが基本です。

つまり、太字部分(コードの2行目)は、ワーカーの定型的な枠組みであるということです。(selfは、現在のウィンドウ自身を表します。)

リスナーは、イベントリスナーオブジェクトedataプロパティでメイン処理からのメッセージ(ここではtargetnumプロパティ)を受け取り(コードの5~7行目)、処理を実行した結果をpostMessageメソッドでメイン処理に返します。

別の.jsファイルをインポートする

ここでは利用していませんが、ワーカー内ではimportScriptメソッドを利用することで、外部のJavaScriptファイルをインポートすることもできます。

importScript('external.js');

バックグラウンドでJavaScriptのコードを実行する【起動編】

ワーカー(process.js)を作成&起動するコードは次の通りです。

[起動]ボタンをクリックすることで、ワーカーに対してテキストボックスの値を渡します。

<form>
  <input id="target" type="text" size="7" />の中に
  <input id="num" type="text" size="3" />の倍数は
  <span id="result">-</span>個あります。
  <input id="btn" type="button" value="起動" />
</form>
let result = document.getElementById('result');
// ワーカーを準備
let worker = new Worker('process.js');
	
// [起動]ボタンをクリックしたときにワーカーを起動
document.getElementById('btn').addEventListener('click', function (e) {
  worker.postMessage({
    target: document.getElementById('target').value,
    num: document.getElementById('num').value,
  });
  result.textContent = '(計算中...)';
});
	
// メッセージを受け取ったら、その結果を反映
worker.addEventListener('message', function (e) {
  result.textContent = e.data;
}, false);
	
// エラー時にはメッセージをダイアログに表示
worker.addEventListener('error', function (e) {
  window.alert(e.message);
}, false);

ワーカーは、Workerオブジェクトで表します。(let worker = new Worker('process.js');上のJavaScriptのコード3行目)

コンストラクターの引数には、ワーカーを表す.jsファイルのパスを渡します。

ワーカーを起動するのは、postMessageメソッドの役割です。

引数にはワーカーに引き渡すデータを、「名前:値,...」のハッシュ形式で指定します。(上のJavaScriptのコード7~10行目)

ワーカーからの結果を処理しているのは、messageイベントリスナーです。(上のJavaScriptのコード15~17行目)

ワーカーからの戻り値は、イベントオブジェクトのdataプロパティに格納されるているので、ここではページにそのまま反映します。

上のJavaScriptのコード20~22行目は、エラー処理です。

エラー情報は、イベントオブジェクトのmessage(エラーメッセージ)、filename(ファイル名)、lineno(行数)プロパティなどで取得できます。

ワーカー側でwindow.alertconsole.logメソッドなどは呼び出せないので、errorイベントリスナーを介して、メインスクリプトでエラー情報を受け取るようにします。

サンプルを実行し、テキストボックスtarget、numにそれぞれ1000000、3のような値を入力して、スクリプトを実行します。

計算処理の間もページに対する操作はロックされないことが確認できます。

MEMO
ワーカーを中断する
実行中のワーカーをメインスクリプトから中断するには、「worker.terminate();」のようにします。

ワーカー自身で処理を中断するならば、「self.close();」とします。

ワーカーは中断された場合、その時点で破棄されます。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です