JavaScript 再入門(その25) 非同期通信 2 PHP と JavaScript

非同期通信 PHP と JavaScript

前の記事では fetch を使って JSON ファイルからデータを取得する方法を見てきました。
実務では、クライアントからパラメータとリクエストを受け取り、サーバー側でデータベースなどから必要なデータを加工しクライアントへ返します。
今回は、サーバーサイドを PHP で簡単なデータを返すプログラムを作成して、非同期通信でクライアントとデータのやり取りをしてみます。

JavaScript 再入門(その24) 非同期通信 1 fetch
非同期通信 fetch fetch オブジェクトは、XMLHttpRequest(XHR) オブジェクトに代わる、サーバーと対話するためのAPIです。fetch…
sakura-system.com

この記事では、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);
AbortController インターフェイスは 1 つ以上のウェブリクエストをいつでも中断することを可能にするコントローラーオブジェクトを表します。
developer.mozilla.org
Aborting fetches using a new web primitive – The abort controller.
developers.google.com

参考リンク

MDN 開発者向けのウェブ技術 > FormData()
MDN 開発者向けのウェブ技術 > FormData.append()
MDN 開発者向けのウェブ技術 > JavaScript「再」入門
開発者向けのウェブ技術 > Web API > AbortController