PHPの進行状況の課題

どういうわけか、重いPHPスクリプトを処理しました。 かなり長いサイクルでPHP側で計算が実行されたときに、ブラウザーで何らかの方法でタスクの進行状況を表示する必要がありました。 このような場合、通常、次のような文字列の定期的な出力に頼ります。



<script>document.getElementById('progress').style.width = '1%';</script>
      
      





このオプションはいくつかの理由で私には向いていませんでした。さらに、私は基本的にこのアプローチが好きではありません。



約3000〜5000回繰り返しました。 そのような単純な仕事にはトラフィックが大きすぎると思いました。 さらに、このオプションは技術的な観点から非常に見苦しく、ページの外観はまったく見苦しくなりました。フッターはすぐに届かない-タスクの100%完了に関する最後の通知の後。



pageいページの問題を回避することは大したことではありませんでしたが、残りのマイナス面は私を幸せにし、よりエレガントなソリューションを探し始めました。



いくつかの主要な質問。 非同期HTTPリクエストは可能ですか? -はい。 大きなタスクの一部が完了したことをシングルバイトの助けを借りて伝えることは可能ですか? -はい。 XMLHttpRequest.onreadystatechange



を使用して、徐々に(連続して)データを受信および処理できますか? -はい。 HTTPヘッダーを使用して、タスクの合計期間の事前通知を送信することもできます(これが原則的に可能な場合)。



解決策は簡単です。 設立されたページはコントロールパネルです。 リモートからタスクを開始および停止できます。 このページはXMLHttpRequestを開始します-メインタスクが開始します。 (メインループ内で)このタスクを実行するプロセスで、スクリプトはクライアントに1バイト-スペース文字を送信します。 リモートコントロールのonreadystatechange



ハンドラーで、バイト単位で受信することにより、タスクの進行状況について結論を出すことができます。



スキームは次のとおりです。 操作スクリプト:



 <?php set_time_limit(0); for ($i = 0; $i < 50; $i++) // ,    50 { sleep(1); //   echo ' '; }
      
      





XMLHttpRequest.onreadystatechange



ハンドラー:



 xhr.onreadystatechange = function() { if (this.readyState == 3) { var progress = this.responseText.length; document.getElementById('progress').style.width = progress + '%'; } };
      
      





ただし、反復は50回しかありませんが、スクリプトファイルでその数を決定したため、これについてはわかっています。 そして、あなたが知らない場合、または量を変えることができますか? readyState == 2



を使用すると、ヘッダーから情報を取得できます。 これを使用して、反復回数を決定しましょう。



 header('X-Progress-Max: 50');
      
      





そして、リモートコントロールでこの値を受け取り、記憶します。



 var progressMax = 100; xhr.onreadystatechange = function() { if (this.readyState == 2) { progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; } else if (this.readyState == 3) { var progress = 100 * this.responseText.length / progressMax; document.getElementById('progress').style.width = progress + '%'; } };
      
      





一般的な概要は明確でなければなりません。 それでは、落とし穴について話しましょう。



まず、 output_buffering



がPHPで有効になっている場合、これを考慮する必要があります。 ここではすべてが簡単です。有効になっている場合、スクリプトを実行すると、 ob_get_level()



は0より大きくなります。バッファリングをバイパスする必要があります。 また、Nginx FastCGI PHPバンドルを使用する場合、FastCGIとNginx自体の両方が出力をバッファリングすることを考慮する必要があります。 後者は、送信するデータを圧縮する場合にこれを行います。 問題は簡単に修正されます。



 header('Content-Encoding: none', true);
      
      





gzipの問題がPHPスクリプト自体の内部で解決できる場合、FastCGIにサーバー構成を修正することによってのみデータを直ちに転送させることができます。



 fastcgi_keep_conn on;
      
      





さらに、Nginx、FastCGI、またはChrome自体は、1バイトのみを含む応答本文の送受信を開始するのは無駄だと考えています。 したがって、操作全体の前に追加のバイトを置く必要があります。 同意する必要があります。最初の20個のスペースは何の意味もありません。 PHP側では、出力に「吐き出す」だけで、 onreadystatechange



ハンドラーでは無視する必要があります。 私の意見では、構成コンポーネント全体がヘッダーで送信されるため、この数の無視されたスペースもヘッダーで伝達する方が適切です。 paddingと呼びます



 <?php header('X-Progress-Padding: 20', true); echo str_repeat(' ', 20); flush(); // ...
      
      





クライアント側では、これも考慮する必要があります。



 var progressMax = 100, progressPadding = 0; xhr.onreadystatechange = function() { if (this.readyState == 2) { progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; } else if (this.readyState == 3) { var progress = 100 * (this.responseText.length - progressPadding) / progressMax; document.getElementById('progress').style.width = progress + '%'; } };
      
      





20という数字はどこから来たのですか? あなたが私に言ったら、私は非常に感謝します。 実験的にインストールしました。



ところで、PHP output_buffering



。 複雑なバッファリングがあり、それを壊したくない場合は、この関数を使用できます。



 function ob_ignore($data, $flush = false) { $ob = array(); while (ob_get_level()) { array_unshift($ob, ob_get_contents()); ob_end_clean(); } echo $data; if ($flush) flush(); foreach ($ob as $ob_data) { ob_start(); echo $ob_data; } return count($ob); }
      
      





その助けを借りて、すべてのレベルのバッファリングをバイパスし、データを直接出力し、その後すべてのバッファを復元できます。



ところで、タスクの完了部分を通知するためにスペースが使用されているのはなぜですか? ウェブ上でデータを表示するためのほとんどすべての形式が、そのようなスペースによって台無しにできないという理由だけで。 このメソッドを使用して、操作の進行状況に関する通知を送信し、この後、結果に関するレポートをJSONで表示できます。



すべてを整理し、少し最適化し、便利になる可能性のあるすべての機能でコードを補完すると、次のようになります。



progress-loader.js
 function ProgressLoader(url, callbacks) { var _this = this; for (var k in callbacks) if (typeof callbacks[k] != 'function') callbacks[k] = false; delete k; function getXHR() { var xhr; try { xhr = new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) { try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } catch (E) { xhr = false; } } if (!xhr && typeof XMLHttpRequest != 'undefined') xhr = new XMLHttpRequest(); return xhr; } this.xhr = getXHR(); this.xhr.open('GET', url, true); var contentLoading = false, progressPadding = 0, progressMax = -1, progress = 0, progressPerc = 0; this.xhr.onreadystatechange = function() { if (this.readyState == 2) { contentLoading = false; progressPadding = +this.getResponseHeader('X-Progress-Padding') || progressPadding; progressMax = +this.getResponseHeader('X-Progress-Max') || progressMax; if (callbacks.start) callbacks.start.call(_this, this.status); } else if (this.readyState == 3) { if (!contentLoading) contentLoading = !!this.responseText .replace(/^\s+/, ''); // .trimLeft() —  _ if (!contentLoading) { progress = this.responseText.length - progressPadding; progressPerc = progressMax > 0 ? progress / progressMax : -1; if (callbacks.progress) { callbacks.progress.call(_this, this.status, progress, progressPerc, progressMax ); } } else if (callbacks.loading) callbacks.loading.call(_this, this.status, this.responseText); } else if (this.readyState == 4) { if (callbacks.end) callbacks.end.call(_this, this.status, this.responseText); } }; if (callbacks.abort) this.xhr.onabort = callbacks.abort; this.xhr.send(null); this.abort = function() { return this.xhr.abort(); }; this.getProgress = function() { return progress; }; this.getProgressMax = function() { return progressMax; }; this.getProgressPerc = function() { return progressPerc; }; return this; }
      
      





process.php
 <?php function ob_ignore($data, $flush = false) { $ob = array(); while (ob_get_level()) { array_unshift($ob, ob_get_contents()); ob_end_clean(); } echo $data; if ($flush) flush(); foreach ($ob as $ob_data) { ob_start(); echo $ob_data; } return count($ob); } if (($work = @$_GET['work']) > 0) { header("X-Progress-Max: $work", true, 200); header("X-Progress-Padding: 20"); ob_ignore(str_repeat(' ', 20), true); for ($i = 0; $i < $work; $i++) { usleep(rand(100000, 500000)); ob_ignore(' ', true); } echo $work.' done!'; die(); }
      
      





launcher.html
 <!DOCTYPE html> <html> <head> <title>ProgressLoader</title> <script type="text/javascript" src="progress-loader.js"></script> <style> progress, button { display: inline-block; vertical-align: middle; padding: 0.4em 2em; margin-right: 2em; } </style> </head> <body> <progress id="progressbar" value="0" max="0" style="display: none;"></progress> <button id="start">Start/Stop</button> <script> var progressbar = document.getElementById('progressbar'), btnStart = document.getElementById('start'), worker = false; btnStart.onclick = function() { if (!worker) { var url = 'process.php?work=42'; worker = new ProgressLoader(url, { start: function(status) { progressbar.style.display = 'inline-block'; }, progress: function(status, progress, progressPerc, progressMax) { progressbar.value = +progressbar.max * progressPerc; }, end: function(status, s) { progressbar.style.display = 'none'; worker = false; }, }); } else { worker.abort(); progressbar.style.display = 'none'; worker = false; } }; </script> </body> </html>
      
      





カットの前に入る代わりに: 蹴らないでください。 スキームを実行する前にグーグルで正しい結果を得られなかったので、発明する必要があったので、この最初の出版物でそれを設定することにしました。



All Articles