サーバー側でループなどで時間がかかる処理を実行する場合、タイムアウトになるのを防ぐために処理中に応答を返してあげる手段をとります。要求側ではその応答から進捗状態を表示させることが可能です。これは他にもストリーミング配信などにも応用できます。
実装には JS から AJAX 通信を使うのが便利です。非同期通信を行い onreadystate / onprogress イベントで処理します。onreadystate では状態取得(1:応答待ち、2:処理開始、3:受信中、4:終了)、onprogress は受信中のデータを取得します。
// 非同期通信定義
xhr.open('GET', 'url', true);
// 進捗状況取得
xhr.onprogress = function (_response) {
elm.textContent = (100 * _response.loaded / _response.total) + '%';
};
// 通信状態取得
xhr.onreadystatechange = function () {
elm.className = 'state' + xhr.readyState;
};
// 通信開始
xhr.send();
上記があれば最低限の表示はできます。
サーバー側の実装ですが、通常は高速化のためにバッファリングを行っています。一定量のデータをバケツに貯めておき、満杯になると一気に出力します。
PHP では、そのバケツが3つほどあります。
- PHP の最初のバッファ、flush() で出力
- もうひとつある PHP バッファリング、ob_flush() で出力
- Web サーバーで用意されているバッファリング、サーバーウェアによって設定が異なる
- Apache の場合は .htaccess で php_flag output_buffering Off 設定
- IIS の場合は applicationHost.config の handlers 内に定義されている PHP ハンドラで responseBufferLimit="0" 設定
- nginx の場合は fastcgi_buffering off;
あとはバッファサイズを Content-Type ヘッダに、実際のデータを処理ごとに echo などで応答すれば実現できます。以下ではピリオド文字1バイトを進捗に使っています。またカウントが進むたびに時間がかかる処理にしてみました。
// 直接応答、ダウンロードしない
header('Content-Disposition: inline');
// テキストで応答する
header('Content-Type: plain/text');
// 最大カウント
$max = 25;
// コンテンツの長さ
$lng = $max - 1;
header("Content-Length: {$lng}");
for ($idx = 0; $idx < $max; $idx++) {
// 進捗はドットで応答
echo '.';
// バッファリングしないで出力
flush();
ob_flush();
// 検証のため時間がかかる処理を設定
set_time_limit($idx * 100 / 1000 + 1000);
sleep($idx * 100 / 1000);
}