PHP基于session+ajax实现文件上传进度条

一、场景及方案介绍

Web应用中常常需要提供文件上传功能,典型的应用场景有用户头像、相册照片上传等。当要上传的文件比较大的时候,为了更好的用户体验,显示文件上传进度是必要。

在PHP5.4之前,php实现文件上传进度条,主要有三种方法:

  • 使用flash、java、activeX
  • 使用PHP的APC扩展
  • 使用HTML5的FILE API

第一种方法依赖第三方的浏览器插件,通用性不好,并且存在安全隐患。不过由于flash的使用范围较广,因此很多网站使用flash作为解决方案。
第二种方法不足在于需要安装PHP的PAC扩展库,要求用户能够控制服务器的配置。
第三中方法应该是最理想的方法,不需要服务器的支持,仅仅在浏览器使用JavaScript即可。但由于HTML5标准尚未完全确立,各个浏览器的支持不同,所以这种方法不能大规模使用。

在PHP5.4版本中引入基于session的上传进度监视功能(session.upload.progress),是一种服务器端文件上传进度解决方案。可以不用安装APC扩展,使用原生的PHP加ajax即可实现上传进度条。这里使用的是session+jQuery ajax实现。

二、原理介绍

当浏览器向服务器上传一个文件时,PHP会吧此文件上传的详细信息(上传时间、进度)存储在session中。跟随上传的进行,周期的更新session中的信息。浏览器可以使用ajax周期的请求服务器端脚本,取得session中的进度信息,从而客户端显示进度条。

文件上传信息要存储在session中,需要在php.ini的配置文件中进行以下设置:

1
2
3
4
5
6
7
8
9
10
11
session.upload_progress.enabled = On

session.upload_progress.cleanup = On

session.upload_progress.prefix = “upload_progress_”

session.upload_progress.name = “PHP_SESSION_UPLOAD_PROGRESS”

session.upload_progress.freq = “1%”

session.upload_progress.min_freq = “1

其中,enabled控制upload_progress功能开启与关闭,默认开启;cleanup设置当文件上传请求提交完成后,是否清除session相关信息,默认开启;prefix和name分别设置进度信息在session中存储的变量名和键名;freq和min_freq,两项用来设置服务器对进度的更新频率。合理的配置可以减轻服务器的负载。
另外在文件上传表单中,需要为该上传设置一个标识符,并在接下来的过程中使用该标识符来引用进度信息。具体操作,在上传表单中添加一个隐藏域,它的name属性为php.ini中的session.upload_progress.name的值。该隐藏域的值是一个你自己定义的标识符,代码如下:

1
2
<input type=”hidden” name=”<?php echo ini_get(‘session.upload_progress.name’); ?>
value=”test” />

接着文件上传的表单后,PHP会在$_SESSION变量中新建键,键名是一个将session.upload_progress.prifix的值与上面你所定义的标识符连接后的字符串,可以通过以下代码得到:

1
2
3
$i = ini_get(‘session.upload_progress.name’);
$key = ini_get(‘session.upload_progress.prefix’).$_POST[$i];
$_SESSION[$key];

SESSION[SESSION[key]的变量结构如下:

1
2
3
4
5
6
7
8
9
10
$_SESSION[‘upload_progress_test’] = array(
“start_time” => 1234567890, //开始时间
“content_length” => 57343257, //POST请求的总数据长度
“bytes_processed” => 453489, //已收到的数据长度
“done” => false, //请求是否完成,true表示完成,false表示未完成
“files” => array( //文件信息
0 => array(…),
1 => array(…), //同一请求中包含多个文件
),
);

通过$_SEESION中的conten_length和bytes_processed可以求出文件上传百分比。

三、具体实现


index.php表单内容如下:(注意在文件最前面使用session_start()开启session)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
<div id="article">
<form action="upload.php" method="POST" id="upload-form" enctype="multipart/form-data" target="hidden-iframe">
<input type="hidden" value="test" name="<?php echo ini_get('session.upload_progress.name');?>">
<p><input type="file" name="file1"></p>
<p><input type="submit" value="上传"></p>
</form>
<div>
<div id="progress" class="progress" style="margin-bottom: 15px; display: none;">
<div class="bar" style="width: 0%;"></div>
</div>
<div class="handle"></div>
</div>
<div class="label"></div>
</div>
<iframe id="hidden-iframe" name="hidden-iframe" src="about:blank" style="display: none;"></iframe>

此处表单中session.upload_progress.name隐藏域的值设为了test。表单中可以添加多个文件上传表单,如果需要的话。还有就是,表单的target属性,指向了当前页面的一个隐藏iframe,作用是防止提交表单后页面跳转。
id为progress这个div是用来你显示进度条的。
处理文件上传的文件时upload.php,与通常文件上传操作没有什么不同:

1
2
3
4
<?php
if (is_uploaded_file($_FILES['file1']['tmp_name'])) {
move_uploaded_file($_FILES['file1']['tmp_name'], "./{ $_FILES['file1']['name'] }");
}

使用ajax获取进度信息:这是最为关键的一步,需要建立progress.php文件,读取session中的信息;然后在index.php增加js代码,向progress.php发起ajax请求,根据获取的进度信息更新进度条。progress.php的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
session_start();
$i = ini_get('session.upload_progress.name');
$key = ini_get('session.upload_progress.prefix') . $_GET[$i];

if (!empty($_SESSION[$key])) {
$current = $_SESSION[$key]['bytes_processed'];
$total = $_SESSION[$key]['content_length'];
echo $current < $total ? ceil($current / $total * 100) : 100;
} else {
echo 100;
}

最后在index.php中加入如下js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script type="text/javascript">
$(function() {
function fetch_progress() {
$.get('progress.php', { '<?php echo ini_get("session.upload_progress.name");?>': 'test'}, function(data) {
var progress = parseInt(data);
$('.handle').show().css('left', 2 * progress + 'px');
$('.label').html(progress + '%');
$('#progress .bar').css('width', 2 * progress + 'px');
if (progress < 100) {
setTimeout('fetch_progress()', 1);
} else {
$('.label').html('上传完成');
}
});
}

$('#upload-form').submit(function() {
$('#progress').show();
setTimeout('fetch_progress()', 1);
});
});
</script>