PHP5.2 跟踪文件上传进度[来自IBM]
Web 2.0 是 Internet 上最炙手可热的时髦字眼,投资者纷纷把资金投入到涉及这项技术的投资项目中。数以百万计的 Web 站点和应用程序覆盖的描述性术语有很多。使用 Web 2.0,我们将描述一类 Web 站点,这些站点都提供了了解 Internet 上数以百万计用户心声的途径。与众不同之处在于它们全都为用户提供了交流和分享与共同利益相关的观点和数据的场所,这些站点可以快速生成大量内容。
每个用户都将提供某种内容 —— 评论咖啡店、上班路线等。YouTube 在这点上是一个优秀示例,为人们提供一个空间可以上传视频并使其他用户可以观看这些视频并提供反馈。YouTube 是 Web 2.0 奉行者的新宠,值得关注的是到目前为止 YouTube 的流行度上升得比 Internet 中的任何一个站点都要快。这种流行度可以归因于大量各式各样的内容,以及能够让用户以留言的形式发表自己对内容的看法。并且不仅可以留言,用户甚至还可以上传与视频相对应的视频留言。
许多接收文件的 Web 站点都会在文本框旁边安置令人厌恶的 Browse 按钮,强制要求用户一次上传一个文件。这可能要花费很长时间,尤其是在以小型文件组的形式提供视频、甚至照片或其他项目的情况下。由于每个文件都必须单独上传,因此可能会十分繁琐。假定上传超大型文件所花的时间会使耐心不足的用户感到难以容忍,那么给这些用户提供积极反馈避免他们放弃并走开将十分重要。
幸运的是,PHP V5.2 引入到文件上传过程中的新 hook 使我们可以向用户实时显示上传的进展情况。在本文中,将使用 PHP V5.2 为用户创建一个进度条(要获得源代码,请参阅 下载)。
如果安装并配置了正确的库,则 PHP V5.2 中的新 “hook” 实际上是在文件传输过程中可获得的数据点。这些新 hook 将使用一种称为 Alternative PHP Cache 的功能。当 PHP 脚本收到一个上传文件时,解释程序将自动检查 $_POST array
以查找名为 APC_UPLOAD_PROGRESS 的隐藏字段,它将成为缓存变量,存储关于上传的信息以便脚本可以访问上传文件。当此信息已被缓存并且随时可以访问后,可以给用户提供可视化反馈,从而提高用户体验。
我们将介绍 HTML 表单中的 APC 代码的实现,以及如何在 PHP 中识别该实现及如何访问缓存的信息。表示此数据有很多方法:从 Ajax 到 FLEX,但是我们要关注的是准备这些前端技术所需的访问数据的方法。
默认情况下,PHP V5.2 中的 APC 不启用。由于新 hook 是 APC 的一部分,因此需要确保安装扩展并使其可用于 PHP 解释程序。这将通过下载 php_apc 扩展文件来完成。在我们的例子中,将使用 WAMP 安装,这是包括 Apache 和 MySQL 的免费获得打包的 PHP for Windows®。它提供了友好的用户界面并且由于拥有支持配置选项的菜单而十分易于管理。
要在 WAMP 上设置 APC,请执行以下步骤:
- 要下载库和 WAMP,请参阅 参考资料。
- 安装 WAMP。
- 把 php_apc.dll 文件放到 PHP 的扩展文件夹中。默认情况下,此文件夹为 <wamproot>/php/ext。
- 使用系统盘 WAMP 菜单来选择 PHP settings>PHP Extensions>Add Extension。
- 在弹出的命令行界面中,键入
php_apc.dll
并按 Enter。 - 使用文本编辑器,打开 <wamproot>/php/php.ini 并添加代码行
apc.rfc1867 = on
(添加到任何位置都可以)。如果要尝试在本地进行测试并计划上传大型文件以便可以实际看到进度,则还需要添加以下指令:apc.max_file_size = 200M
、upload_max_filesize = 200M
和post_max_size = 200M
。请不要在活动的生成服务器上执行此操作,不过,不这样做很可能用尽带宽和磁盘空间配额,更不必说会降低其他人的访问速度。 - 重新启动 PHP。
APC 现在应当已设置并被初始化。APC 的 RFC1867 特性 —— 使您可以跟踪文件上传的特性 —— 现在应当已被启用为选项,并且应当准备好探究文件上传以启用实时状态。
要接收文件,必须先设置接收文件的表单。很方便的是,HTML 附带了文件的标准字段类型。同所有 HTML 表单字段一样,它在逻辑上被命名为类型 file
。默认情况下,附带了显示在块右侧的便捷 Browse 按钮。
清单 1. upload.php 的 HTML 表单
<?php $id = $_GET['id']; ?> <form enctype="multipart/form-data" id="upload_form" action="target.php" method="POST"> <input type="hidden" name="APC_UPLOAD_PROGRESS" id="progress_key" value="<?php echo $id?>"/> <input type="file" id="test_file" name="test_file"/><br/> <input onclick="window.parent.startProgress(); return true;" type="submit" value="Upload!"/> </form> |
需要为此表单创建一个 PHP 页面,因为需要使用惟一密钥来跟踪上传。最后,它将是用于调用此页面作为 GET
值的 URL 的一部分。此数字将是稍后将检索的 APC 缓存条目密钥的值。要传递该值,表单字段需要有一个拥有特殊名称的隐藏字段,使 APC 知道它需要保存文件上传状态。此字段被称为 APC_UPLOAD_PROGRESS。这是前述的启动缓存过程的 hook。为确保 PHP 可以访问缓存中的正确条目,我们使用检索到的惟一 ID 作为隐藏字段的值,从而创建该值的密钥。用户提交表单后 —— 我们将简短地处理提交按钮 —— 浏览器将把文件和密钥作为发送给服务器的 POST
数据的一部分进行发送。
装入到浏览器中后,此页面应当提供一个非常简单的表单,如图 1 所示:
图 1. 上传表单
![上传表单](http://www.ibm.com/developerworks/cn/opensource/os-php-v525/fig01.jpg)
要在不重新装入整个页面的情况下使用户可以提交文件,需要把此表单嵌入到另一个文件的 iframe
中。如果尝试仅使用表单操作页面 (target.php) 来检索数据,则无法看到任何缓存信息,因为在上传完成之前页面不会返回任何信息。鉴于这个原因,使用此新 hook 的最常见示例都是用 Ajax 编写的。通过该方法,您可以提交表单并且还可以在同一个窗口中继续检查上传的状态而无需刷新。
要使脚本运行,需要继续转到一个包含页面,该页面将设置 iframe
并接收已上传文件的信息。还需要使用一组 JavaScript 函数来为进度指示器获得数据以及显示进度指示器。
提交表单中包括文件时,该文件将发送到服务器的临时位置中,直至它被保存到永久位置。当它在临时存储设备中时,可以通过$_FILES
关联数组获得它。使用 PHP 附带的标准版本文件上传函数,可以选择路径并将这些函数保存到服务器上,或者按自己的需要处理这些函数。
清单 2. target.php 文件
<?php if($_SERVER['REQUEST_METHOD']=='POST') { move_uploaded_file($_FILES["test_file"]["tmp_name"], "c://sw//wamp//www//" . $_FILES["test_file"]["name"]); echo "<p>File uploaded. Thank you!</p>"; } ?> |
首先,查看来自表单的 POST
变量是否已被设定并表示我们已经收到了表单数据。如果收到表单数据,并且但愿包括文件,则还应当有一个全局数组 $_FILES
。把已上传的文件移到安全位置,这取决于需要对其采取的操作。在本例中,只需把文件移到 /sw/wamp/www(当然,这是完全任意的位置。请随意选择一个所需位置)。完成该操作后,我们将感谢用户。
在这里包括实际文件处理主要是为了实现完整性。由于本文讲述的是进度条,因此收到实际文件后如何处理它无关紧要。
还将需要一个返回实际上传进度的脚本。清单 3 显示了一个非常简单的版本。
清单 3. getprogress.php 文件
<?php if(isset($_GET['progress_key'])) { $status = apc_fetch('upload_'.$_GET['progress_key']); echo $status['current']/$status['total']*100; } ?> |
此脚本首先将查找 progress_key,它是先前讨论的 $id
值(不必担心,您马上就将看到它的来源)。这将导致调用从 APC 缓存返回数据的 apc_fetch()
。我们需要正确的文件信息,因此需要惟一 ID,在本文中表示为 $_GET['progress_key']
。调用带有upload_xxxxxx 参数的 apc_fetch()
,其中 xxxxxx 是惟一 ID;PHP 将自动预先追加 upload_ part。
获得数据后,可以使用 JSON 扩展给信息设定一种更便于在 JavaScript 中使用的格式并返回整个对象(如果需要)。$status
对象是拥有以下字段的数组:
total
- 文件的总大小
current
- 到目前为止收到的文件数
rate
- 上传速度(以字节每秒为单位)
filename
- 文件名
name
- 变量名
temp_filename
- PHP 保存文件的临时副本的位置
cancel_upload
- 上传是已取消 (1),还是未取消 (0)
done
- 上传是已完成 (1),还是尚未完成 (0)
在本例中,只需要完成百分比。您可以在自己的应用程序中选择使用更多信息。
现在已经准备好开始构建实际的进度条。为了简单起见,脚本将使用 CSS 创建一个用于模拟进度条并可以使用 JavaScript 进行控制的 div,如清单 4 所示:
清单 4. 主文件 progress.php
<html> <head><title>Upload Example</title></head> <body> <script type="text/javascript"> var counter = 0; function startProgress(){ document.getElementById("progressouter").style.display="block"; fire(); } function fire(){ if (counter < 101){ document.getElementById("progressinner").style.width = counter+"%"; counter++; setTimeout("fire()",100); } } </script> <div id="progressouter" style= "width: 500px; height: 20px; border: 6px solid red; display:none;"> <div id="progressinner" style= "position: relative; height: 20px; background-color: purple; width: 0%; "> </div> </div> <span onclick="startProgress()">Start me up!</span> </body> </html> |
此页面包含了两个嵌套的 div
元素,外面的那个用作边框。脚本将调整内部 div
相对于边框的大小以显示进度。当用户单击 Start me up! 文本时,startProgress()
脚本将调用 fire()
函数。该函数将检查计数器的值,并且如果该值尚未超过 100,就把内部 div
设为外部 div
宽度的该百分比值。然后它将增加计数器的值并告诉浏览器每十分之一秒就执行一次全部上述过程。
结果将与图 2 非常相似:
图 2. 进度条脚本
![进度条脚本](http://www.ibm.com/developerworks/cn/opensource/os-php-v525/fig02.jpg)
现在只需要有一种获得脚本以更新宽度的方法,此宽度不是任意的数字而是完成百分比。
现在剩下的只是要把所有内容 hook 到一起。您可以通过 progress.php 页面来完成此操作。
清单 5. 最终的 progress.php 页面
<?php $id = uniqid(""); ?> <html> <head><title>Upload Example</title></head> <body> <script src="http://maps.google.com/maps?file=api&v=2&key=<yourkeyhere>" type="text/javascript"></script> <script type="text/javascript"> function getProgress(){ GDownloadUrl("getprogress.php?progress_key=<?php echo($id)?>", function(percent, responseCode) { document.getElementById("progressinner").style.width = percent+"%"; if (percent < 100){ setTimeout("getProgress()", 100); } }); } function startProgress(){ document.getElementById("progressouter").style.display="block"; setTimeout("getProgress()", 1000); } </script> <iframe id="theframe" name="theframe" src="upload.php?id=<?php echo($id) ?>" style="border: none; height: 100px; width: 400px;" > </iframe> <br/><br/> <div id="progressouter" style= "width: 500px; height: 20px; border: 6px solid red; display:none;"> <div id="progressinner" style= "position: relative; height: 20px; background-color: purple; width: 0%; "> </div> </div> </body> </html> |
从底层开始向上层工作,我们已经添加了嵌入清单 1 中的 upload.php 脚本的 iframe
,给它提供了在页面顶部生成的惟一 ID。
现在,是否还记得该表单中的 Submit 按钮?
<input onclick="window.parent.startProgress(); return true;" type="submit" value="Upload!"/> |
该按钮将完成两项工作。提交表单,像普通的 Submit 按钮一样;但在执行该操作之前,它将在主窗口中调用 startProgress()
脚本。startProgress()
脚本将告诉进度条显示自身 —— 开始时无显示属性,然后告诉浏览器等待一秒,然后再执行 getProgress()
脚本。
现在,getProgress()
脚本将使事情变得有趣。记不记得在前面我说过将需要使用 Ajax 或某种类似的方法来检查文件的进度?对,在本例中,表单将采用捷径,调用来自 Google Maps API 库的 GdownloadUrl()
函数(注意,表单将导入位于页面顶部的库。您将需要获得自己的访问此库的密钥,但是它是从 Google 免费获取的)。
此函数将下载 URL 的内容 —— 本例中为 getprogress.php 脚本 —— 并执行在其中定义的匿名函数。函数所接受的第一个参数是从 URL 返回的数据,本例中为百分比,以便使用它更新进度条。最后,如果文件尚未完成下载,则告诉浏览器每十分之一秒重试一次(在实际情况中,可能无法那么快地执行这些调用,但是浏览器将尽其所能进行操作)。
最终结果是页面使用户可以查看文件正被上传的进度。
图 3. Progress.php 的输出
![Progress.php 的输出](http://www.ibm.com/developerworks/cn/opensource/os-php-v525/fig03.jpg)