非同期通信 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