最近项目中要求使用HTTP做文件上传,而且要求有进度显示,在网上东找西寻了半天,解决方案倒也不少,比如使用Ajax上传,但感觉这种方式的进度提示太麻烦,所以没有采用,后来看到了SWFUpload,就找了些资料来看,觉得符合自己的要求,研究了足有两天,略有心得,忙不迭地记录下来,以防止时间长了遗忘,如果不小心帮助了别人,那就更好了,呵呵...
下面摘抄一段SWFUpload官网的介绍(别人翻译过来的):
SWFUpload 最初是由Vinterwebb.se 开发的客户端文件上传工具。它结合javascript和flash在浏览器中提供一个优于传统上传标签 <input type="file" /> 的功能(和良好的用户体验)。
SWFUpload 的主要特性:
SWFUpload 的设计理念与其他基于flash的上传工具不同。SWFUpload 给开发者尽可能多的UI控制能力. 开发者可以使用 XHTML, CSS, JavaScript 来使它更符合自己网站的样式风格. 它提供一组简单的js事件更新上传状态,开发者可以根据这些事件来在网页中显示文件上传进度
好了,夸奖的话不多说了,既然它这么受欢迎,想必是有一定优势的。
下面记录一下我做的一个小示例,先预览一下程序运行效果:
有朋友索要例子源码,其实我早已放在我的资源里了,并没有设置下载积分,有需要的朋友请到这里下载:http://download.csdn.net/detail/zhangyihui1986/4538748
现在SWFUpload项目托管于Google code下,URL为:http://code.google.com/p/swfupload/ ,请自行下载,下载的压缩文件中含有一个文档,里面详细介绍了SWFUpload的配置参数、事件支持、支持方法等。
这里是官网上的几个例子:http://demo.swfupload.org/
网络上关于SWFUpload的博客资源很多,但很多博客质量不太高,而且转来转去,内容重复,所以我在查资料的时候没有找到太合适的资源,后来找到了这里:http://webdeveloperplus.com/jquery/multiple-file-upload-with-progress-bar-using-jquery/ ,这是一个外国人写的关于SWFUpload的小教程,UI做地也挺好,于是我就参照着做了个小例子。
不过,上面链接的教程是针对SWFUpload一个针对JQuery的插件写的,这个插件地址为:http://blogs.bigfish.tv/adam/2009/06/14/swfupload-jquery-plugin/,该插件好像仅仅是完善了一下SWFUpload的事件机制,使用它可采用JQuery的链式写法,但我没有用它,而是用的原生的SWFUpload2.2版本。
不多说,先贴出来我做的例子,然后再针对代码作适当解释。
例子使用了Struts2.3.4作为后台,当然,SWFUpload是不选择后台的,您可以使用任意的技术,只要是能实现HTTP协议即可。
1)将下载的压缩文件解压并拷到web项目中(此处我放到项目的js目录下),建立其它资源(如JSP、Action等)后目录结构如下图所示:
2)在JSP文件中引入SWFUpload依赖的JS文件
<link href="<%=path %>/js/ligerUI/skins/Aqua/css/ligerui-all.css" type="text/css" rel="stylesheet" /> <script type="text/javascript" src="<%=path %>/js/jquery-1.7.2.min.js"></script> <script type="text/javascript" src="<%=path %>/js/ligerUI/js/core/base.js"></script> <script type="text/javascript" src="<%=path %>/js/ligerUI/js/plugins/ligerDrag.js"></script> <script type="text/javascript" src="<%=path %>/js/ligerUI/js/plugins/ligerDialog.js"></script> <script type="text/javascript" src="<%=path %>/js/swfupload/swfupload.js"></script> <script type="text/javascript" src="<%=path %>/js/swfupload/plugins/swfupload.queue.js"></script>
在此处我用到了ligerUI框架,如果不使用它,那么仅需要引jquery和swfupload.js两个文件即可。
3)JSP文件中HTML结构
<div id="swfupload"> <span id="spanButtonPlaceholder"></span> <p id="queueStatus"></p> <ol id="logList"></ol> </div>
#logList { margin: 0; padding: 0; width: 500px } #logList li { list-style-position: inside; margin: 2px; border: 1px solid #ccc; padding: 10px; color: #333; font-size: 15px; background: #FFF; position: relative; } #logList li .progressBar { border:1px solid #333; height:5px; background:#fff; } #logList li .progressValue { color: red; margin-left: 5px } #logList li .progress { background:#999; width:0%; height:5px; } #logList li p { margin:0; line-height:18px; } #logList li.success { border:1px solid #339933; background:#ccf9b9; } #logList li span.cancel { background:url('../images/delete.gif') no-repeat; position:absolute; top:5px; right:5px; width:16px; height:16px; cursor:pointer }
var contextPath; var queueErrorArray; $(function(){ contextPath = $("#contextPath").val(); var swfUpload = new SWFUpload({ upload_url: contextPath + '/swfupload/upload!upload.action', flash_url: contextPath + '/js/swfupload/Flash/swfupload.swf', file_post_name: 'fileData', use_query_string: true, post_params: { param1: 'Hello', param2: encodeURI('你好',"utf-8") }, file_types: "*.rar;*.zip", file_types_description: '上报数据文件', file_size_limit: '102400', // file_upload_limit: 5, file_queue_limit: 3, // handlers file_dialog_start_handler: fileDialogStart, file_queued_handler: fileQueued, file_queue_error_handler: fileQueueError, file_dialog_complete_handler: fileDialogComplete, upload_start_handler: uploadStart, upload_progress_handler: uploadProgress, upload_success_handler: uploadSuccess, upload_complete_handler: uploadComplete, button_placeholder_id: 'spanButtonPlaceholder', button_text: '<span class="whiteFont">选择文件</span>', button_text_style: '.whiteFont{ color: #FFFFFF; }', button_text_left_padding: 40, button_text_top_padding: 6, button_image_url: contextPath + '/images/button.png', button_width: 133, button_height: 33, button_cursor: SWFUpload.CURSOR.HAND, button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT, debug: false, custom_settings: {} }); }); //======================================== 回调函数Handlers =================================== /** * 打开文件选择对话框时响应 */ function fileDialogStart() { if (queueErrorArray) { queueErrorArray = null; } } /** * 文件被加入上传队列时的回调函数,增加文件信息到列表并自动开始上传.<br /> * <p></p> * SWFUpload.startUpload(file_id)方法导致指定文件开始上传, * 如果参数为空,则默认上传队列第一个文件;<br /> * SWFUpload.cancelUpload(file_id,trigger_error_event)取消指定文件上传并从队列删除, * 如果file_id为空,则删除队列第一个文件,trigger_error_event表示是否触发uploadError事件. * @param file 加入队列的文件 */ function fileQueued(file) { var swfUpload = this; var listItem = '<li id="' + file.id + '">'; listItem += '文件:<em>' + file.name + '</em>(' + Math.round(file.size/1024) + ' KB)'; listItem += '<span class="progressValue"></span>' + '<div class="progressBar"><div class="progress"></div></div>' + '<p class="status" >Pending</p>' + '<span class="cancel" > </span>' + '</li>'; $("#logList").append(listItem); $("li#" + file.id + " .cancel").click(function(e) { swfUpload.cancelUpload(file.id); $("li#" + file.id).slideUp('fast'); }) // swfUpload.startUpload(); } /** * 文件加入上传队列失败时触发,触发原因包括:<br /> * 文件大小超出限制<br /> * 文件类型不符合<br /> * 上传队列数量限制超出等. * @param file 当前文件 * @param errorCode 错误代码(参考SWFUpload.QUEUE_ERROR常量) * @param message 错误信息 */ function fileQueueError(file,errorCode,message) { if (errorCode == SWFUpload.QUEUE_ERROR.QUEUE_LIMIT_EXCEEDED) { alert("上传队列中最多只能有3个文件等待上传."); return; } if (!queueErrorArray) { queueErrorArray = []; } var errorFile = { file: file, code: errorCode, error: '' }; switch (errorCode) { case SWFUpload.QUEUE_ERROR.FILE_EXCEEDS_SIZE_LIMIT: errorFile.error = '文件大小超出限制.'; break; case SWFUpload.QUEUE_ERROR.INVALID_FILETYPE: errorFile.error = '文件类型受限.'; break; case SWFUpload.QUEUE_ERROR.ZERO_BYTE_FILE: errorFile.error = '文件为空文件.'; break; default: alert('加载入队列出错.'); break; } queueErrorArray.push(errorFile); } /** * 选择文件对话框关闭时触发,报告所选文件个数、加入上传队列文件数及上传队列文件总数 * @param numSelected 选择的文件数目 * @param numQueued 加入队列的文件数目 * @param numTotalInQueued 上传文件队列中文件总数 */ function fileDialogComplete(numSelected,numQueued,numTotalInQueued) { var swfupload = this; if (queueErrorArray && queueErrorArray.length) { var table = $('<table><tr><td>文件</td><td>大小</td></tr></table>'); for(var i in queueErrorArray) { var tr = $('<tr></tr>'); var info = '<td>' + queueErrorArray[i].file.name + '<span style="color:red">(' + queueErrorArray[i].error + ')</span></td>' + '<td>' + queueErrorArray[i].file.size + 'bytes</td>'; table.append(tr.append(info)); } $.ligerDialog.open({ width: 500, content: table, title: '文件选择错误提示', buttons: [{ text: '确定', onclick: function(btn,dialog,index) { $("#queueStatus").text('选择文件: ' + numSelected + ' / 加入队列文件: ' + numQueued); swfupload.startUpload(); dialog.close(); } }] }); queueErrorArray = []; } else { this.startUpload(); } } /** * 文件开始上传时触发 * @param file 开始上传目标文件 */ function uploadStart(file) { if (file) { $("#logList li#" + file.id).find('p.status').text('上传中...'); $("#logList li#" + file.id).find('p.progressValue').text('0%'); } } /** * 文件上传过程中定时触发,更新进度显示 * @param file 上传的文件 * @param bytesCompleted 已上传大小 * @param bytesTotal 文件总大小 */ function uploadProgress(file,bytesCompleted,bytesTotal) { var percentage = Math.round((bytesCompleted / bytesTotal) * 100); $("#logList li#" + file.id).find('div.progress').css('width',percentage + '%'); $("#logList li#" + file.id).find('span.progressValue').text(percentage + '%'); } /** * 文件上传完毕并且服务器返回200状态码时触发,此时文件的上传周期并未完成, * 不能在此事件监听函数开始下一个文件的上传 * @param file 上传的文件 * @param serverData 服务器在执行完接收文件方法后返回的数据 * @param response Boolean类型,表示是否服务器返回数据 */ function uploadSuccess(file,serverData,response) { var item = $("#logList li#" + file.id); item.find('div.progress').css('width','100%'); item.find('span.progressValue').css('color','blue').text('100%'); item.addClass('success').find('p.status').html('上传完成!'); } /** * 在一个上传周期结束后触发(uploadError及uploadSuccess均触发) * 在此可以开始下一个文件上传(通过上传组件的uploadStart()方法) * @param file 上传完成的文件对象 */ function uploadComplete(file) { this.uploadStart(); }
SWFUpload组件的实例化很简单,就是new一个JS对象,如
var swfUpload = new SWFUpload({settings});我们需要传给它一个配置对象,这个配置对象的内容很多,详细介绍请参照使用说明文档,在此仅介绍几个重要一些的配置参数,以注释的形式写在代码里。
upload_url: '', // 上传操作后台处理URL,相当于form的action属性 flash_url: '', // swf文件的位置,指向JS目录下的swfupload.swf文件即可 file_post_name: '', // 提交到后台的文件的名字,相当于<input type='file' />域的name值,默认为“Filedata” file_types: "*.rar;*.zip", // 可上传的文件类型 file_types_description: '', // 可上传文件类型的描述信息 file_size_limit: '', // 上传文件大小限制,接受值和单位,默认单位是KB,如'1024 MB' button_placeholder_id: 'spanButtonPlaceholder', // 设置一个HTML元素,用以渲染Flash的Button button_image_url: '', // 按钮图片,Flash使用,可以有多种状态(mouseout、hover等) button_width: 270, // 按钮的宽,必须要设置,不设置按钮无法显示 button_height: 20, // 按钮的高,必须要设置,不设置按钮无法显示 button_cursor: SWFUpload.CURSOR.HAND, // 鼠标移到按钮上的光标样式 button_window_mode: SWFUpload.WINDOW_MODE.TRANSPARENT, // Flash剪辑的WMODE属性
需要注意的是按钮的高和宽一定要指定,否则flash无法显示。
swfupload仅实现了后台操作,前台的处理空间留给了开发者,它采用事件触发机制,让开发者捕获特定事件,自定义对应的事件处理函数进行相应处理。即当swfupload内部某一特定事件发生,便触发相应的JS函数,并传入合适的相关参数,开发者就可以拿着这些参数来更新UI界面以提示上传进度了。Swfupload通过固定的事件函数名属性值寻找自定义的JS函数,所以在初始化工作中,将自定义的JS函数名赋值给swfupload指定的对应属性即可。
// handlers:事件监听函数,请参考使用说明文档 file_dialog_start_handler: fileDialogStart,// 文件选择对话框打开时触发,传入SWFUpload定义的File参数 file_queued_handler: fileQueued,// 文件被加入队列时触发 file_queue_error_handler: fileQueueError,// 文件加入队列出错时触发,包括大小限制,类型限制,空文件等均会触发 file_dialog_complete_handler: fileDialogComplete,// 文件选择对话框在文件选择完成并关闭时触发 upload_start_handler: uploadStart,// 文件开始上传时触发 upload_progress_handler: uploadProgress,// 上传过程中定时触发,此方法在更新进度条时比较重要 upload_success_handler: uploadSuccess,// 文件上传成功时触发 upload_complete_handler: uploadComplete,// 文件上传完成时触发,包括上传成功与上传失败,此方法可以开始下一文件的上传
默认情况下,你选择了文件并关闭了文件选择对话框文件并不会自动开始上传,你需要调用SWFUpload的startUpload()方法,该方法接收一个file_id作为参数,如果参数为空,则自动开始上传文件队列中的第一个文件。
另外,在所有的事件监听器中,this关键字都是指向SWFUpload对象,所以你可以在file_queued_handler或file_dialog_complete_handler等事件监听器中这样开始文件上传:
this.startUpload();
默认情况下,你在第3步中开始了一个文件上传,但是如果你选择的是多个文件,那么你会发现这个文件上传后其它文件并不会自动开始上传,这是为什么呢?原来Swfupload虽支持批量上传,但本质仍是单个文件依次上传,所以如果你的上传是支持多个文件上传那只有你自己去开始其它文件的自动上传了,我们可以在upload_complete_handler事件监听器中再调用一个startUpload()方法:
this.startUpload();
因为upload_complete_handler事件是在一个文件上传后触发,不管该文件是否上传成功都会触发该事件,所以我们在这里再调用一次startUpload方法是合适的,这样SWFUpload组件在前一个文件上传完成后就会自动开始下一个文件的上传。
显示出上传文件进度列表能够增强用户体验,因为用户将看见选择的文件信息以及该文件的上传进度,这些东西就完全由开者来定制了,SWFUpload组件将丰富的文件信息及上传进度在调用事件监听函数时以参数的形式传进来,我们就可以利用这些信息动态更新文件上传进度UI了。如前面的例子我们在file_queued_handler事件监听函数中将文件信息及其进度条拼接到页面的ol列表中,在upload_start_handler事件监听函数中初始化进度条显示,然后在文件上传过程中会不停地触发upload_progress_handler事件,我们就在这个事件监听函数中获取上传进度,并将其更新到进度列表,这样便可实现文件上传的进度提示。在SWFUpload丰富的事件机制支持下,实现这些东西是很容易的事。
在SWFUpload的配置参数中有一个post_params参数是用来向后台传递参数的,默认情况好像是不能传递参数的,但是如果你将另一个配置参数use_query_string设置为true之后就可以传递参数了。
该参数接收一个JS对象,类似于这样:
use_query_string: true, post_params: { param1: 'Hello', param2: '你好' }
但是如果参数值中含有中文的话,那么后台会报错,也取不到值,可以这样解决:
在JS中先用UTF-8进行中文编码
use_query_string: true, post_params: { remark: encodeURI('中文',"utf-8") }
然后在后台再转回来,在Java中就体现为
URLDecoder.decode(request.getParameter("remark"), "utf-8");
如此便可解决中文参数传递问题。
假设有这种需求:文件上传完成后跳转到指定的页面,比如文件信息查看页面,那我们可能会一时找不到好的方法,因为SWFUpload上传类似于Ajax上传,涉及到的上传交互操作并非是浏览器所发,而是由Flash发出的上传请求,那么后台给出的跳转信息浏览器自然接收不到,而是由Flash接收到了,既然浏览器接收不到跳转信息,那么自动跳转就不会发生。
对于这种需求,我们可以参照Ajax提交跳转的方式,在后台成功接收文件后输出一些信息,比如跳转的页面,或者仅仅是一个字符串,而把跳转的任务交由Javascript来完成,那么我们应该在什么事件监听函数中处理呢!您可以看看upload_success_handler这个事件的回调函数有什么参数,如下:
uploadSuccess(file object, server data, received response)
这个事件发生后会传三个参数到我们定义的监听函数中,第一个是上传完成的文件对象(关于文件对象可参看SWFUpload的文档,其实就是一个JS对象,包含一些重要的文件信息),第二个其实是服务器返回的数据,如果后台用的直接跳转,那么这里的server data就会是跳转页面的HTML结构,document.write()将其输出,也算是完成跳转了吧!如果你仅仅是向前台输出个字符串,并没有跳转,那么这里就应该是你输出的字符串,在这里就可以用location.href=“来实现跳转;第三个参数是Boolean类型,表示是否接收到服务器传回的数据。
需要注意的是该事件在每个文件上传成功均会触发,如果同时在上传多个文件,那么第一个文件上传完成后页面就直接跳转了,后台的文件也就得不到机会上传了,所以这里需要判断一下队列里是否还有没有上传完成的文件,如果没有了再跳转,就可以了,至于如何判断队列里是否还有未上传的文件,这里用到了SWFUpload的另一个对象Stats。该对象提供了上传队列的状态信息,访问实例的getStats方法可获取此对象,该对象中有一些属性,其中有一个files_queued属性可以表示是否还有未上传的文件,如果该属性为0则表示全部上传,可以这样做
if(this.getStats().files_queued = 0) { // jump code }
最近在使用SWFUpload过程中发现一个问题,文件上传成功后我需要将其移动到指定目录下,由于目录信息是在Session中的,这时在非IE浏览器中出现了问题,Session中没有东西,用户登录信息也没有了。经过调试才发现文件上传成功后在后台取到的Session与用户登录完成后的Session相比根本就不是一个Session了,它们的ID不同,也就是说文件上传另生成了一个Session,而这个Session是全新的,当然没有了登录时存放的信息了。
经过一番搜索,这是由于Flash Player在非IE浏览器下一个Bug引起的,这里有一段描述:
Cookie issue
On Windows the Non-IE Flash Player plugin (FireFox, Opera, Safari, etc) sends the IE cookies regardless of the browser used. This breaks authentication and sessions for many server-side scripting technologies.
Developers should manually pass Session and Authentication cookie information and manually restore Sessions on the Server Side if they wish to use Sessions
The SWFUpload package contains work-around sample code for PHP and ASP.Net
也就是说非IE浏览器下Flash Player插件发送的也是IE浏览器当前页面的cookie,并且Session是靠Cookie中保存的SessionId实现的,因此后台处理程序另新建了Session,程序也就出现了上述错误。
找到的解决办法是手动将SessionID传到后台服务端。
在上传路径URL里加上jsessionid变量即可,如下:
upload_url: contextPath + '/web/user-upload!upload.action;jsessionid=<%=request.getSession().getId() %>'
这样就可以解决问题了,有人还说可以使用SWFUpload组件的一个插件:swfupload.cookies.js,但我没有弄好,看它的源码是分析浏览器的cookie然后将它们拼接到post_params配置项中,我手动拼上jsessionid也是不起作用,不知道是不是自己没弄对,反正是这个插件我没有成功使用。