非同期通信 PHP と JavaScript
前の記事では fetch を使って JSON ファイルからデータを取得する方法を見てきました。
実務では、クライアントからパラメータとリクエストを受け取り、サーバー側でデータベースなどから必要なデータを加工しクライアントへ返します。
今回は、サーバーサイドを PHP で簡単なデータを返すプログラムを作成して、非同期通信でクライアントとデータのやり取りをしてみます。
この記事では、PHP を使っていますので Apache 等のウェブサーバーを立てる必要があります。
プログラムの仕様としては、JavaScript から名前をリクエストすると、サーバー側の PHP がプロフィールのデータを返す。という非常に基本的なものです。本来であればデータベース等からデータを取得するのですが、本題から外れてしまいますので、データは名前をキーとした連想配列で持っています。
PHP (サーバー) 側コード
serverData.php
<?php
// serverData.php
// プロフィールデータ
$profileAry = array(
'NAKAZATO' => array(
'firstName' => 'Takeshi' ,
'lastName' => 'Nakazato' ,
'manufacture' => 'NISSAN' ,
'model' => 'BNR32 GT-R V-specII' ,
) ,
'TAKAHASHI' => array(
'firstName' => 'Keisuke' ,
'lastName' => 'Takahashi' ,
'manufacture' => 'MAZDA' ,
'model' => 'FD3S RX-7 Type R' ,
) ,
'FUJIWARA' => array(
'firstName' => 'Takumi' ,
'lastName' => 'Fujiwara' ,
'manufacture' => 'TOYOTA' ,
'model' => 'AE86 SPRINTER TRUENO GT-APEX 3door' ,
) ,
) ;
$retAry = array() ; // 返信用配列データ
$req = json_decode( file_get_contents( 'php://input' ), true ) ; // リクエストデータを取得 *注1
$reqName = strtoupper( $req[ 'name' ] ) ; // リクエストされた名前を大文字に変換
if ( array_key_exists( $reqName , $profileAry ) ) { // リクエストされた名前がプロフィールデータに存在するかのチェック
$retAry[ 'status' ] = true ; // プロフィール取得成功
$retAry[ 'data' ] = $profileAry[ $reqName ] ; // プロフィールの設定
} else {
$retAry[ 'status' ] = false ; // プロフィールの取得失敗
$retAry[ 'data' ][ 'reason' ] = $req['name'] . ' is not found!' ; // 失敗理由の設定
}
echo( json_encode( $retAry ) ) ; // JSON形式でデータを返します。
?>*注1 の行は php://input でリクエストの body 部を読み取って、JSON形式から配列へデコードしています。
データの返信は JSON エンコードしたものを echo で返信します。
JavaScript (クライアント側) スクリプト
// 汎用JSON取得関数
async function postData( argURL , argData ) {
try {
const response = await fetch(
argURL , {
method: 'POST' , // メソッドPOST
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( argData ), // JSONでパラメータを渡します。型は "Content-Type" ヘッダーと一致させる必要があります
}
) ;
if ( !response.ok ) { // fetchのエラーチェック
throw 'FETCH ERROR! HTTP Status:' + response.status ; // fetchでエラーがあればエラーを投げる
}
const resData = await response.json() ; // レスポンスデータのの取得
return { // データの取得状態(true)と取得したJSONデータを返します。
status: true ,
retData: resData ,
} ;
} catch( e ) { // エラー処理
return { // データの取得状態(false)とエラー内容を返します。
status: false ,
retData: e ,
} ;
}
}
// プロフィール表示関数
async function getProfile( argName ) {
const data = { name : argName } ;
try {
const res = await postData( 'http://localhost/testsrc/serverData.php' , data) ;
if ( !res.status ) { // データの取得状態のチェック
throw res.retData ; // 正常に取得できなかったらエラーを投げてcatch()で処理する。
}
// データが正常に取得できた処理
const retData = res.retData.data ;
if ( res.retData.status ) { // リクエストのエラーチェック
const profile = `${retData.lastName} ${retData.firstName}: ${retData.manufacture} ${retData.model}` ; // プロフィールの編集
console.log( profile ) ; // プロフィールの表示
} else {
console.error( retData.reason ) ; // リクエストエラーの表示
}
} catch( e ) { // エラーの処理
console.error(e) ;
}
}
// 表示順序を守って各人のプロフィールを表示する。
async function personalProfile() {
await getProfile( 'Fujiwara' ) ;
await getProfile( 'nakazato' ) ;
await getProfile( 'TAKAHASHI' ) ;
await getProfile( 'Mr.X' ) ;
}
personalProfile() ;
/*
Fujiwara Takumi: TOYOTA AE86 SPRINTER TRUENO GT-APEX 3door
Nakazato Takeshi: NISSAN BNR32 GT-R V-specII
Takahashi Keisuke: MAZDA FD3S RX-7 Type R
Mr.X is not found!
*/拡張してみます。
postData 関数は再利用可能なので、モジュール化します。
./modules/postData.js
// 汎用JSON取得関数 (./modules/postData.js)
export async function postData( argURL , argData ) {
try {
const response = await fetch(
argURL , {
method: 'POST' , // メソッドPOST
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( argData ), // JSONでパラメータを渡します。型は "Content-Type" ヘッダーと一致させる必要があります
}
) ;
if ( !response.ok ) { // fetchのエラーチェック
throw 'FETCH ERROR! HTTP Status:' + response.status ; // fetchでエラーがあればエラーを投げる
}
const resData = await response.json() ; // レスポンスデータのの取得
return { // データの取得状態(true)と取得したJSONデータを返します。
status: true ,
retData: resData ,
} ;
} catch( e ) { // エラー処理
return { // データの取得状態(false)とエラー内容を返します。
status: false ,
retData: e ,
} ;
}
}メインスクリプト
<script type='module'>
import { postData } from './modules/postData.js' ; // 汎用JSON取得関数
// プロフィール表示関数
async function getProfile( argName ) {
const data = { name : argName } ;
try {
const res = await postData( 'http://localhost/testsrc/serverData.php' , data) ;
if ( !res.status ) { // データの取得状態のチェック
throw res.retData ; // 正常に取得できなかったらエラーを投げてcatch()で処理する。
}
// データが正常に取得できた処理
const retData = res.retData.data ;
if ( res.retData.status ) { // リクエストのエラーチェック
const profile = `${retData.lastName} ${retData.firstName}: ${retData.manufacture} ${retData.model}` ; // プロフィールの編集
console.log( profile ) ; // プロフィールの表示
} else {
console.error( retData.reason ) ; // リクエストエラーの表示
}
} catch( e ) { // エラーの処理
console.error(e) ;
}
}
// 表示順序を守って各人のプロフィールを表示する。
async function personalProfile() {
await getProfile( 'Fujiwara' ) ;
await getProfile( 'nakazato' ) ;
await getProfile( 'TAKAHASHI' ) ;
await getProfile( 'Mr.X' ) ;
}
personalProfile() ;
/*
Fujiwara Takumi: TOYOTA AE86 SPRINTER TRUENO GT-APEX 3door
Nakazato Takeshi: NISSAN BNR32 GT-R V-specII
Takahashi Keisuke: MAZDA FD3S RX-7 Type R
Mr.X is not found!
*/
</script>FormData() を使ってのリクエストデータ渡し
FormData() を使ってリクエストデータをサーバーへ渡すことにより。form タグで input された入力データを使って、サーバーへリクエストを渡すこともできます。
汎用JSON取得関数モジュールに postDataReqForm() 関数を追加します。
postData() 関数との違いは fetch() のオプションが違うだけです。
./modules/postData.js
// 汎用JSON取得関数 (./modules/postData.js)
export async function postData( argURL , argData ) {
try {
const response = await fetch(
argURL , {
method: 'POST' , // メソッドPOST
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify( argData ), // JSONでパラメータを渡します。型は "Content-Type" ヘッダーと一致させる必要があります
}
) ;
if ( !response.ok ) { // fetchのエラーチェック
throw 'FETCH ERROR! HTTP Status:' + response.status ; // fetchでエラーがあればエラーを投げる
}
const resData = await response.json() ; // レスポンスデータのの取得
return { // データの取得状態(true)と取得したJSONデータを返します。
status: true ,
retData: resData ,
} ;
} catch( e ) { // エラー処理
return { // データの取得状態(false)とエラー内容を返します。
status: false ,
retData: e ,
} ;
}
}
// FormDataでリクエスト
export async function postDataReqForm( argURL , argData ) {
try {
const response = await fetch(
argURL , {
method: 'POST' , // メソッドPOST
body: argData,
}
) ;
if ( !response.ok ) { // fetchのエラーチェック
throw 'FETCH ERROR! HTTP Status:' + response.status ; // fetchでエラーがあればエラーを投げる
}
const resData = await response.json() ; // レスポンスデータのの取得
return { // データの取得状態(true)と取得したJSONデータを返します。
status: true ,
retData: resData ,
} ;
} catch( e ) { // エラー処理
return { // データの取得状態(false)とエラー内容を返します。
status: false ,
retData: e ,
} ;
}
}メインスクリプトは、import文の変更で postDataReqForm() 関数を呼びます。
postData() 関数に渡すデータは、オブジェクト型から FormData型に変更しています。formData.append() メソッドにより値を formData に設定します。
<script type='module'>
import { postDataReqForm as postData } from './modules/postData.js' ; // 汎用JSON取得関数 formDataでリクエスト
// プロフィール表示関数
async function getProfile( argName ) {
let data = new FormData() ; // リクエスト用formData
data.append( 'name' , argName ) ; // formData.append(name, value);
try {
const res = await postData( 'http://localhost/testsrc/serverData.php' , data) ;
if ( !res.status ) { // データの取得状態のチェック
throw res.retData ; // 正常に取得できなかったらエラーを投げてcatch()で処理する。
}
// データが正常に取得できた処理
const retData = res.retData.data ;
if ( res.retData.status ) { // リクエストのエラーチェック
const profile = `${retData.lastName} ${retData.firstName}: ${retData.manufacture} ${retData.model}` ; // プロフィールの編集
console.log( profile ) ; // プロフィールの表示
} else {
console.error( retData.reason ) ; // リクエストエラーの表示
}
} catch( e ) { // エラーの処理
console.error(e) ;
}
}
// 表示順序を守って各人のプロフィールを表示する。
async function personalProfile() {
await getProfile( 'Fujiwara' ) ;
await getProfile( 'nakazato' ) ;
await getProfile( 'TAKAHASHI' ) ;
await getProfile( 'Mr.X' ) ;
}
personalProfile() ;
/*
Fujiwara Takumi: TOYOTA AE86 SPRINTER TRUENO GT-APEX 3door
Nakazato Takeshi: NISSAN BNR32 GT-R V-specII
Takahashi Keisuke: MAZDA FD3S RX-7 Type R
Mr.X is not found!
*/
//}
</script>form タグでインプットされた値を利用
もし、form タグでインプットされた値を利用したい場合は次のようにします。
HTML の form タグが以下のようになっていたとします。
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8"/>
</head>
<body>
<div id="contents">
<form id="reqinputform">
<input id="firstname" type="text" name="firstname" />
<input id="sendbuttom" type="button" value="送信" />
</form>
</div>
</body>
</html>メインスクリプトの FormData はこのようになります。
document.getElementById('sendbutton').addEventListener('click', getProfile) ;
// プロフィール表示関数
async function getProfile() {
const inputName = document.getElementById('firstname') ;
let data = new FormData() ; // リクエスト用formData
data.append( 'name' , inputName.value ) ; // formData.append(name, value);PHP (サーバー) 側プログラムは、$_POST を使ってリクエスト値を取得します。
// serverData.php
// プロフィールデータ
$profileAry = array(
'NAKAZATO' => array(
'firstName' => 'Takeshi' ,
'lastName' => 'Nakazato' ,
'manufacture' => 'NISSAN' ,
'model' => 'BNR32 GT-R V-specII' ,
) ,
'TAKAHASHI' => array(
'firstName' => 'Keisuke' ,
'lastName' => 'Takahashi' ,
'manufacture' => 'MAZDA' ,
'model' => 'FD3S RX-7 Type R' ,
) ,
'FUJIWARA' => array(
'firstName' => 'Takumi' ,
'lastName' => 'Fujiwara' ,
'manufacture' => 'TOYOTA' ,
'model' => 'AE86 SPRINTER TRUENO GT-APEX 3door' ,
) ,
) ;
$retAry = array() ; // 返信用配列データ
$req = $_POST ; // $_POST でリクエストデータを取得
$reqName = strtoupper( $req[ 'name' ] ) ; // リクエストされた名前を大文字に変換
if ( array_key_exists( $reqName , $profileAry ) ) { // リクエストされた名前がプロフィールデータに存在するかのチェック
$retAry[ 'status' ] = true ; // プロフィール取得成功
$retAry[ 'data' ] = $profileAry[ $reqName ] ; // プロフィールの設定
} else {
$retAry[ 'status' ] = false ; // プロフィールの取得失敗
$retAry[ 'data' ][ 'reason' ] = $req['name'] . ' is not found!' ; // 失敗理由の設定
}
echo( json_encode( $retAry ) ) ; // JSON形式でデータを返します。
?>fetchの中断 (AbortController)
AbortController を使えば、処理中の fetch を簡単に中断することができます。
var controller = new AbortController();
var signal = controller.signal;
var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');
downloadBtn.addEventListener('click', fetchVideo);
abortBtn.addEventListener('click', function() {
controller.abort();
console.log('Download aborted');
});
function fetchVideo() {
...
fetch(url, {signal}).then(function(response) {
...
}).catch(function(e) {
reports.textContent = 'Download error: ' + e.message;
})
}AbortController オブジェクトインスタンスを生成します。
var controller = new AbortController();
AbortSignal オブジェクトのインスタンスを取得します。
var signal = controller.signal;
fetch にAbortSignal を渡します。
fetch(url, {signal}).then(function(response) {中断ボタンをクリックされたら、AbortController の abort() メソッド呼び出します。
これで、AbortSignal を使って fetch() に中断リクエストが送られます。
abortBtn.addEventListener('click', function() {
controller.abort();中断されると、fetch() promise は AbortError となるので、catch で中断されたかの判断をすることができます。
}).catch(err => {
if (err.name === 'AbortError') {
console.log('Fetch aborted');
}
}イベントリスナによって中断を取得することもできます。
signal.addEventListener('abort', () => {
console.log(signal.aborted);
});中断ボタンクリック以外にも、例えば5秒以上で中断などもできます。
setTimeout(() => controller.abort(), 5000);
参考リンク
MDN 開発者向けのウェブ技術 > FormData()
MDN 開発者向けのウェブ技術 > FormData.append()
MDN 開発者向けのウェブ技術 > JavaScript「再」入門
開発者向けのウェブ技術 > Web API > AbortController
