ブラウザのJavaScriptでAJAX
以前にLaravelの開発環境設定やVue.jsの初期設定をしました。その際にも触れましたが、最近のWebアプリにはAJAX機能が欠かせないものとなっています。
そこで今回はAJAX通信に関する知識の整理をしながら、実際にそれらに触れてみたいと思います。
JSONとJavaScriptのオブジェクト
AJAXとは直接関係ありませんが、以降の説明でJSONという言葉が出てくるので先に説明しておきます。
JSONはデータのフォーマットのことです。{}カッコの中に{ "key" : 値 }として、データを表現します。値が文字列の場合は""で囲みます。JavaScriptのオブジェクトも似た記法であらわされますが、キーにダブルクォートをつけません。また、JavaScriptのオブジェクトには関数をセットすることができますが、JSONには関数という概念はありません。
JSONとJavaScriptのオブジェクトとは似ているというだけではなく、値を相互変換する関数も標準で用意されています。キーにダブルクォーテーションを含んだJSON文字列をJSON.parse関数で、オブジェクトに変換することができます。その逆はJSON.stringifyを使うことでJavaScriptのオブジェクトをJSON文字列形式に変換することが可能です。
さらに、JavaScriptでAJAXするWebサーバーでよく用いられるPHPにも、同様にJSON文字列をオブジェクトや連想配列にするメソッドと、オブジェクトや配列からJSON文字列を作成するするメソッドがあります。そのためJSONをうまく使うとコードの省力化が望めます。
サーバー側(PHP)
//json文字列をオブジェクトまたは連想配列に変換します
//第二引数がfalseならオブジェクト(クラス)、trueなら連想配列で返ります
$data = json_decode($_POST['jsonstr'],true);
//配列やオブジェクトをjson文字列化します
echo json_encode($response);
//ちなみに$_POSTで値を受け取るのではなく直接JSONを受け取る
//(Content-Typeがapplication/jsonの時)
$data=file_get_contents("php://input");
AJAX
AJAX(Asynchronous JavaScript + XML)とは、ブラウザでJavaScriptを使って非同期通信をする手法です。
ブラウザでのフォーム入力を思い出してみてください。フォーム入力後「送信」等のボタンを押すと、画面全体が一度再読み込みされるページと、そうでないページとがあります。
この時ページの再読み込みなしで処理するのがAJAXです。もはや当たり前になりすぎて実感がわきづらいかもしれませんが、バックグラウンドで通信をし結果が戻ってきたらJavaScriptから画面を操作して結果を表示したりする機能です。
全体の再読み込みをしない分処理も軽くなります。
XMLHttpRequest
AJAX用のライブラリはいくつも存在しますが、まず基本であるXMLHttpRequestを紹介します。これはJavaScriptに含まれるAPIで、外部のライブラリをロードすることなく使うことができます。
全体のコードのサイズが小さかったり、AJAXを使う箇所が限られている場合は、ライブラリの読み込みがない分クライアントやネットワークの負荷を減らせます。
HTMLのインラインでJavaScriptで書く場合の例を紹介します。最近の多くのサーバーでは、セキュリティの観点から、表示されているページとAJAXで通信しようとするページが違うとCORSエラーになることがが多いので、コードを試してみる場合は、自分の管理するページ宛に通信をするとトラブル少ないと思います。
sample.html
...
<script>
//インスタンスの作成
let xhr = new XMLHttpRequest();
//処理が終わった時の関数を事前にセットしておきます。
xhr.onload = ()=> {
if(xhr.status==200) {
//レスポンスが200=成功時の処理
console.log(xhr.response);
} else {
//エラーの時(404等)
console.log('error:'+xhr.status);
}
}
//通信の初期化 GETメソッドでsample.htmlを指定
xhr.open("GET", "./sample.html");
//通信開始
xhr.send();
</script>
...
HTTPヘッダーや、タイムアウト、非同期処理、等一通りのことができるようになっています。実際の運用で使いそうなオプションを参考として挙げておきます。
- リクエストヘッダーの送信
たとえば、xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')とすることで、ヘッダーの「Content-Type」に「'application/x-www-form-urlencoded'」を設定して、ページのフォームからPOSTした際の通信を設定しています。この時、xhr.openの第一引数もPOSTにします。
この時値はkey=valueの形式にしてsendメソッドに渡します。この時URIエンコードが必要です。
また、「application/json」とすれば、jsonも渡せます。
- タイムアウトの設定
xhr.timeout=60 * 1000
タイムアウト属性値として、ミリ秒で指定します。
- クロスサイトのCookieや認証情報
デフォルトはfalseで「xhr.withCredentials = true」というように設定します。通常は表示中のサイトと違うAJAX通信の場合、Cookieは書き込まれませんが、この設定値をtureにすることで、Cookieを受け入れることができます。AJAX通信が表示中のサイトと同じ場合はこの設定は効果がありません。
- レスポンスタイプ
テキストとして受け取るなら「xhr.responseType='text'」というように設定します。jsonを設定すると、パースした状態でうけとれますが、レスポンスのフォーマットがjson形式でないとエラーとなります。
- 非同期通信
xhr.openの第三引数にtrueを設定すると、処理の終了待たない非同期通信になります。false値(処理を待つ)は非推奨のようです。
axios
AJAX用のライブラリのひとつにaxiosがあります。
axiosの説明には「Promise based HTTP client for the browser and node.js」とあります。このPromiseとはJavaScriptのオブジェクトのことです。非同期処理のためのもので、中に含んだ処理が終わった際に実行するメソッドを渡すことができます。
ちなみに先のXMLHttpRequestでは、通信終了後にコールバック関数を実行するようにイベントリスナーを設定しました。
コールバック関数が少ないうちはいいのですが、増えてくると記述するのが大変になります。Promiseはそのような際の記述をシンプルにできます。
Promiseの使い方やその挙動の詳細はリンク先にて紹介しています。
axiosの初期設定
ここではライブラリをCDN(Contents Delivery Network)で取得します。CDNを利用すると自分のサイトにライブラリ(.jsファイル)を配置する必要がありません。
主要なライブラリはだれもが利用できる形で公開されていることが多く、axiosもそのひとつです。ここではunpkg.comというサイトからaxiosのライブラリを読み込んでみます。HTMLのheadかbodyに次のコードを挿入します。
axiosの基本文法
axios({オブジェクト})かaxios('URL',{オブジェクト})でAJAX通信を開始させることができます。{オブジェクト}には、通信手段(例: method: get等)の設定情報と、送信するデータが入ります。
また、axios.get('URL',{オブジェクト})やaxios.post('URL',{オブジェクト})でも通信処理ができます。さらにaxios.create({オブジェクト})で任意の設定済みのインスタンスを作成できますますので、インスタンスからgetやpost処理もできます。
インスタンスを作成しないで「get」する方法は次のようにします。
axios({
method : 'get',
url: 'https://URL/page.html',
params: {
param1: 'data1',
param2: 'data2'
}
});
または
axios.get(
'https://URL/page.html',
{params: {
param1: 'data1',
param2: 'data2'
}}
);
インスタンスを作成する場合は次のようになります。
let ax = axios.create({
timeout: 1500
});
ax.get('https://URL/page.html',{params:{param1: "value1", param2: "value2"}});
axiosで「post」でデータを送信する場合は環境によっては注意が必要です。前述の{params: ...といった書き方はJSON構造なので、そのままだとJSONデータで送信してしまいます。ブラウザのフォームからPOSTした際とは異なったデータの送り方になるため、たとえばサーバ側PHPだった場合$_POSTから値を取得ができません。もしそのような使い方をしたかったら、公式ページにあるようにURLSearchParamsを使って値をセットします。ちなみに、JSON形式でPOSTされた値をPHPサーバーで取得する場合は「file_get_contents("php://input");」とすれば可能です。
let p = new URLSearchParams();
p.append('param1', 'value1');
p.append('param2', 'value2');
axios({url: 'https://URL/page.html', method: 'post', data: p});
//または
axios.post('https://URL/page.html',p);
//さらに、インスタンスからも同様に処理できます
let ax = axios.create({
timeout: 1500,
});
ax.post('https://URL/page.html', p );
get時にparamsの値にURLSearchParamsで作成した値をセットしても稼働するようです。
thenとcatch
通信開始の処理に続けてthenとcatchを設定することで、通信後の処理を設定できます。thenは成功時、catchはエラー時の処理です。
各パートで定義する無名関数(function)の引数(ここではres)にはサーバーからのレスポンスやリクエストが入っています。サーバーからのHTML出力は.data(res.data)、200等のステータスコードは.status(res.status)にに入ります。
たいていのコードはわかりやすいように、.thenや.catchの前に改行をいれているようです。
axios.get('URL',{…}).then(…).catch(…);
axios.post('URL',{…})
.then(function(res){
//成功時のスクリプト
console.log(res.data);
})
.catch(function(err){
//失敗時のスクリプト
console.log(err);
});
JavaScriptのES2017に対応した近年のブラウザなら、「await」を指定して通信の終了を待つこともできます。ただし、awaitは非同期処理関数内(async function(){...}内部)でしか利用できないので注意してください。
let res = await axios('URL', { オブジェクト });
if (res.status == 200) {
//成功時の処理
...
} else {
//失敗時の処理
...
}
awaitをつけない場合は、resにはPromiseオブジェクトが返るので、思った挙動にはなりません。
jQuery
もうひとつ有名なライブラリのjQueryでの処理の方法も紹介します。
jQueryはAJAX用のライブラリではなく、ブラウザ間の挙動の違いを吸収したりコードを省力化するための、JavaScript全体のライブラリとなります。
jQueryも多くのCDNでホストされています。今度はjsDeliverを使ってみます。
設定値は今まで登場したものとほぼ同じなのでわかると思います。jQueryに触れたことがない人向けに書いておくと、jQueryを読み込んだ後は、$はjQueryが代入されます。
$.ajax({
type:"post",
url:'https://URL/page.html',
contentType: 'application/json',
timeout: 60000,
data:JSON.stringify(params),
//データタイプ デフォルトはapplication/x-www-form-urlencoded
dataType: "json",
//成功時の処理
success: function(data,dataType){ ... },
//失敗時の処理
error: function(xhr,status,error) { ... },
//成功でもエラーでもそれぞれの処理の後に実行されます
complete: function() { ... }
});
CORS
XMLHttpRequestでもaxiosでもjQueryでも、ブラウザが表示したサイトと、AJAXで通信しようとするサイトのドメインが違うと次のようなエラーになります。
このCORSはCross-Origin Resource Sharingの略でオリジン間リソース共有を指します。
「ドメイン」と書きましたが、正確にはそれより粒度の細かい「オリジン」単位で判別されます。「オリジン」は「ドメイン」に、「スキーム(http or https)」と「ポート」を含めた区別を指します。
このエラーが出る場合はMDN web docs:「Access-Control-Allow-Origin」にあるように、AJAXを受け付ける側でHTTPヘッダを出力します。
筆者はPHPでサーバーを作成しているので次のようにしました。AJAXを受け付ける側のサーバーに、それを要求する側のオリジンを記載します。
サーバー側(PHPで例示)
//bodyの出力をする前に記述
//すべて許可する場合(非推奨)
header('Access-Control-Allow-Origin: *');
//特定のオリジンから許可する場合
header('Access-Control-Allow-Origin: https://yyy.xxx.jp');
fetch
fetchは割と最近になって登場したJavaScriptの機能だけで動くAjaxの方法です。前述のXMLHttpRequest(XHR)をPromiseでラップしたような機能です。
たとえば、別記事で紹介しているWebでPDFを作成できるLIB-PDF用に、Web上にホストしているフォントファイルを読み込んでArrayBufferに変換するスクリプトを考えてみます。
これを、XHRとPromiseで実装すると次のようになります。
let fontBytes=await new Promise((resolve,reject)=> {
let xhr = new XMLHttpRequest();
let blob = null;
xhr.open('GET','./ume-hgo4.ttf',true);
xhr.responseType="blob";
xhr.onload= async function(e){
blob = e.target.response;
let ab = await blob.arrayBuffer();
resolve(ab);
};
xhr.send();
});
これだけの記述をfetchを使うことにより、次のように省略できます。
fetchにアドレスを渡すだけで、レスポンスが返るPromiseを取得できます。
let fontBytes=await fetch('./ume-hgo4.ttf').then((res)=> res.arrayBuffer(););
ここでの「レスポンス」はfetch APIのオブジェクトです。例示では本文をArrayBufferに変換するarrayBuffer()メソッドで変換しています。同様の変換メソッドにはblob()やjson()、text()などがあり、これらはすべてPromiseを返します。またレスポンスオブジェクトにはステータスコードが入るstatusをはじめ、headersやbodyといったプロパティも存在します。
筆者がfetchで躓いたことは、POSTをする際のContent-Typeの指定です。
MDN:fetchの使用にならって次のようなコード書いてしまい、意図した挙動にならず解決に時間を費やしました。
...
let form = new FormData();
form.append('user',"marune");
fetch('https://...',{
method: 'POST',
headers: {
//間違い
'Content-Type': 'application/x-www-form-urlencoded'
},
body: form,
}).then((v)=>{...
Formの値を送るのだから、application/x-www-form-urlencoded だろうと考えてのことでしたが、FormDataオブジェクトをfetchに渡す際には、 リクエストの Content-Type ヘッダーを明示的に設定してはいけないそうです。
筆者の環境で、Content-Typeを設定しないで(ブラウザに任せて)fetchをしたログを追ったところ、Content-Type は multipart/form-data となっていました。
別のページではここで紹介したfetchを使ってバイナリファイルをダウンロードする方法も紹介していますので、よろしければご覧になってください。