1. 程式人生 > >基於AJAX的檔案上傳顯示進度條實現

基於AJAX的檔案上傳顯示進度條實現

      基於Ajax的檔案上傳要實現的功能要求,要在使用者提交了上傳按鈕請求後,客戶端其頁面要顯示檔案上傳進度條。

      其整個功能時序圖如圖所示。

基於AJAX檔案上傳時序圖

      簡單的說,要實現在客戶端顯示進度條,需要做的是:當客戶端提交上傳檔案請求後,伺服器在上傳檔案的過程中,將上傳進度情況儲存到Session中,客戶端週期性的傳送請求來獲取儲存在Session中值,以獲取上傳檔案的進度資訊。

1. 新建web工程AjaxUpload。

2. 將commons-fileupload-1.2.1-bin.zip包中的commons-fileupload-1.2.1.jar檔案和commons-io-1.4-bin.zip包中的commons-io-1.4.jar檔案拷貝到web工程下的WEB-INF\lib目錄下。

3. 由於本例項涉及到多個類,處理此類問題最好是給相應的類打包進行管理。在web工程src目錄下新建一個包com.ncu.upload。

4. 伺服器端實現。

首先要建立一個用來儲存檔案上傳狀態的類 FileUploadStatus。其原始碼如下:

package com.ncu.upload;

import java.util.*;

public class FileUploadStatus {
	//上傳總量
	private long uploadTotalSize=0;
	//讀取上傳總量
	private long readTotalSize=0;
	//當前上傳檔案號
	private int currentUploadFileNum=0;
	//成功讀取上傳檔案數
	private int successUploadFileCount=0;
	//狀態
	private String status="";
	//處理起始時間
	private long processStartTime=0l;
	//處理終止時間
	private long processEndTime=0l;
	//處理執行時間
	private long processRunningTime=0l;
	//上傳檔案URL列表
	private List uploadFileUrlList=new ArrayList();
	//取消上傳
	private boolean cancel=false;
	//上傳base目錄
	private String baseDir="";
	
	public String getBaseDir() {
		return baseDir;
	}
	public void setBaseDir(String baseDir) {
		this.baseDir = baseDir;
	}
	public boolean getCancel() {
		return cancel;
	}
	public void setCancel(boolean cancel) {
		this.cancel = cancel;
	}
	public List getUploadFileUrlList() {
		return uploadFileUrlList;
	}
	public void setUploadFileUrlList(List uploadFileUrlList) {
		this.uploadFileUrlList = uploadFileUrlList;
	}
	public long getProcessRunningTime() {
		return processRunningTime;
	}
	public void setProcessRunningTime(long processRunningTime) {
		this.processRunningTime = processRunningTime;
	}
	public long getProcessEndTime() {
		return processEndTime;
	}
	public void setProcessEndTime(long processEndTime) {
		this.processEndTime = processEndTime;
	}
	public long getProcessStartTime() {
		return processStartTime;
	}
	public void setProcessStartTime(long processStartTime) {
		this.processStartTime = processStartTime;
	}
	public long getReadTotalSize() {
		return readTotalSize;
	}
	public void setReadTotalSize(long readTotalSize) {
		this.readTotalSize = readTotalSize;
	}
	public int getSuccessUploadFileCount() {
		return successUploadFileCount;
	}
	public void setSuccessUploadFileCount(int successUploadFileCount) {
		this.successUploadFileCount = successUploadFileCount;
	}
	public int getCurrentUploadFileNum() {
		return currentUploadFileNum;
	}
	public void setCurrentUploadFileNum(int currentUploadFileNum) {
		this.currentUploadFileNum = currentUploadFileNum;
	}
	public String getStatus() {
		return status;
	}
	public void setStatus(String status) {
		this.status = status;
	}
	public long getUploadTotalSize() {
		return uploadTotalSize;
	}
	public void setUploadTotalSize(long uploadTotalSize) {
		this.uploadTotalSize = uploadTotalSize;
	}
	
}
 

      由於要在客戶端要顯示進度條,所以在上傳過程中伺服器端需要監視和維護上傳狀態的資訊,此過程需要處理的資料資訊是:不斷更新Session中儲存的FileUploadStatus例項的資訊,如:已經上傳的位元組數,上傳檔案的總大小等。FileUpload現在的1.2版本為監視上傳進度提供了內建的支援,可以直接繼承類ProgressListener,然後過載update()方法,在該方法中新增自己要處理的程式碼,最後在檔案上傳處理程式碼(後面會講到)中通過為ServletFileUpload物件註冊建立的監聽類。監聽類UploadListener的原始碼如下:

package com.ncu.upload;

import javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.ProgressListener;

public class UploadListener implements ProgressListener {
	
	private HttpSession session=null;
	
	public UploadListener (HttpSession session){
		this.session=session;
	}
	/**
	 * 更新狀態
	 * @param pBytesRead 讀取位元組總數
	 * @param pContentLength 資料總長度
	 * @param pItems 當前正在被讀取的field號
	 */
	public void update(long pBytesRead, long pContentLength, int pItems) {
		FileUploadStatus fuploadStatus = UploadServlet.takeOutFileUploadStatusBean(this.session);
		fuploadStatus.setUploadTotalSize(pContentLength);
		//讀取完成
		if (pContentLength == -1) {
			fuploadStatus.setStatus("完成對" + pItems + "個檔案的讀取:讀取了 " + pBytesRead + "/"  + pContentLength+ " bytes.");
			fuploadStatus.setReadTotalSize(pBytesRead);
			fuploadStatus.setCurrentUploadFileNum(pItems);
			fuploadStatus.setProcessEndTime(System.currentTimeMillis());
			fuploadStatus.setProcessRunningTime(fuploadStatus.getProcessEndTime());
		}else{//讀取過程中
		       fuploadStatus.setStatus("當前正在處理第" + pItems+"個檔案:已經讀取了 " + pBytesRead + " / " + pContentLength+ " bytes.");
		       fuploadStatus.setReadTotalSize(pBytesRead);
		       fuploadStatus.setCurrentUploadFileNum(pItems);
		       fuploadStatus.setProcessRunningTime(System.currentTimeMillis());
		}
		//System.out.println("已經讀取:" + pBytesRead);
		UploadServlet.storeFileUploadStatusBean(this.session, fuploadStatus);
	}

}
 

     有了前面兩個類的基礎,下來我們可以動手去實現真正處理整個操作Servlet類。原始碼如下。

package com.ncu.upload;

import java.io.*;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.*;

/**
 * Servlet implementation class for Servlet: UploadServlet
 *
 */
 public class UploadServlet extends javax.servlet.http.HttpServlet implements javax.servlet.Servlet {
   static final long serialVersionUID = 1L;
   
	 public static final String UPLOAD_STATUS="UPLOAD_STATUS";
	 public static final String UPLOAD_DIR="/upload";
   
	public UploadServlet() {
		super();
	}  
	
	/**
	 * 從檔案路徑中取出檔名
	 * @param filePath
	 * @return
	 */
	private String takeOutFileName(String filePath){
		int pos=filePath.lastIndexOf(File.separator);
		if (pos>0){
			return filePath.substring(pos+1);
		}
		else{
			return filePath;
		}
	}
	
	/**
	 * 從request中取出FileUploadStatus Bean
	 * @param request
	 * @return
	 */
	public static FileUploadStatus takeOutFileUploadStatusBean(HttpSession session){
		Object obj=session.getAttribute(UPLOAD_STATUS);
		if (obj!=null){
			return (FileUploadStatus)obj;
		}
		else{
			return null;
		}
	}
	
	/**
	 * 把FileUploadStatus Bean儲存到session
	 * @param request
	 * @param uploadStatusBean
	 */
	public static void storeFileUploadStatusBean(
			HttpSession session,
			FileUploadStatus uploadStatusBean){
		session.setAttribute(UPLOAD_STATUS,uploadStatusBean);
	}
	
	/**
	 * 刪除已經上傳的檔案
	 * @param request
	 */
	private void deleteUploadedFile(HttpServletRequest request){
		FileUploadStatus fUploadStatus=takeOutFileUploadStatusBean(request.getSession());
		for(int i=0;i<fUploadStatus.getUploadFileUrlList().size();i++){
			File uploadedFile = new File(request.getRealPath(UPLOAD_DIR)+
					File.separator+fUploadStatus.getUploadFileUrlList().get(i));
			uploadedFile.delete();
		}
		fUploadStatus.getUploadFileUrlList().clear();
		fUploadStatus.setStatus("刪除已上傳的檔案");
		storeFileUploadStatusBean(request.getSession(),fUploadStatus);
	}
	
	/**
	 * 上傳過程中出錯處理
	 * @param request
	 * @param errMsg
	 * @throws IOException 
	 * @throws ServletException 
	 */
	private void uploadExceptionHandle(
			HttpServletRequest request,
			String errMsg) throws ServletException, IOException{
		//首先刪除已經上傳的檔案
		deleteUploadedFile(request);
		FileUploadStatus fUploadStatus=takeOutFileUploadStatusBean(request.getSession());
		fUploadStatus.setStatus(errMsg);
		storeFileUploadStatusBean(request.getSession(),fUploadStatus);
	}
	
	/**
	 * 初始化檔案上傳狀態Bean
	 * @param request
	 * @return
	 */
	private FileUploadStatus initFileUploadStatusBean(HttpServletRequest request){
		FileUploadStatus fUploadStatus=new FileUploadStatus();
		fUploadStatus.setStatus("正在準備處理");
		fUploadStatus.setUploadTotalSize(request.getContentLength());
		fUploadStatus.setProcessStartTime(System.currentTimeMillis());
		fUploadStatus.setBaseDir(request.getContextPath()+UPLOAD_DIR);
		return fUploadStatus;
	}
	
	/**
	 * 處理檔案上傳
	 * @param request
	 * @param response
	 * @throws IOException 
	 * @throws ServletException 
	 */
	private void processFileUpload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
		DiskFileItemFactory factory = new DiskFileItemFactory();
		//設定記憶體閥值,超過後寫入臨時檔案
		//factory.setSizeThreshold(10240000*5);
		//設定臨時檔案儲存位置
		//factory.setRepository(new File(request.getRealPath("/upload/temp")));
		ServletFileUpload upload = new ServletFileUpload(factory);
		//設定單個檔案的最大上傳size
		//upload.setFileSizeMax(10240000*5);
		//設定整個request的最大size
		//upload.setSizeMax(10240000*5);
		//註冊監聽類
		upload.setProgressListener(new UploadListener(request.getSession()));
		//儲存初始化後的FileUploadStatus Bean
		storeFileUploadStatusBean(request.getSession(),initFileUploadStatusBean(request));

		try {
			List items = upload.parseRequest(request);
			//處理檔案上傳
			for(int i=0;i<items.size();i++){
				FileItem item=(FileItem)items.get(i);

				//取消上傳
				if (takeOutFileUploadStatusBean(request.getSession()).getCancel()){
					deleteUploadedFile(request);
					break;
				}
				//儲存檔案
				else if (!item.isFormField() && item.getName().length()>0){
					String fileName=takeOutFileName(item.getName());
					File uploadedFile = new File(request.getRealPath(UPLOAD_DIR)+File.separator+fileName);
					item.write(uploadedFile);
					//更新上傳檔案列表
					FileUploadStatus fUploadStatus=takeOutFileUploadStatusBean(request.getSession());
					fUploadStatus.getUploadFileUrlList().add(fileName);
					storeFileUploadStatusBean(request.getSession(),fUploadStatus);
					Thread.sleep(500);
				}
			}
		
		} catch (FileUploadException e) {
			e.printStackTrace();
			//uploadExceptionHandle(request,"上傳檔案時發生錯誤:"+e.getMessage());
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			//uploadExceptionHandle(request,"儲存上傳檔案時發生錯誤:"+e.getMessage());
		}
	}
	
	/**
	 * 迴應上傳狀態查詢
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	private void responseFileUploadStatusPoll(HttpServletRequest request,HttpServletResponse response) throws IOException{
		FileUploadStatus fUploadStatus=(FileUploadStatus)request.getSession().getAttribute(UPLOAD_STATUS);
		//計算上傳完成的百分比
		long percentComplete = (long)Math.floor(((double) fUploadStatus.getReadTotalSize()/(double) fUploadStatus.getUploadTotalSize())*100.0);
		System.out.println("com:"+percentComplete);
		response.setContentType("text/xml");
		response.setCharacterEncoding("UTF-8");
		response.setHeader("Cache-Control", "no-cache");
		if ( ((long)fUploadStatus.getReadTotalSize() == (long)fUploadStatus.getUploadTotalSize()) || (fUploadStatus.getCancel() == true)){
		response.getWriter().write(fUploadStatus.getStatus().toString()+"success");
		}else{
			response.getWriter().write(fUploadStatus.getStatus().toString()+"<div class=\"prog-border\"><div class=\"prog-bar\" style=\"width: "
								+ percentComplete + "%;\"></div></div>");
		}
	}
	/**
	 * 處理取消檔案上傳
	 * @param request
	 * @param response
	 * @throws IOException
	 */
	private void processCancelFileUpload(HttpServletRequest request,HttpServletResponse response) throws IOException{
		FileUploadStatus fUploadStatus=(FileUploadStatus)request.getSession().getAttribute(UPLOAD_STATUS);
		fUploadStatus.setCancel(true);
		request.getSession().setAttribute(UPLOAD_STATUS, fUploadStatus);
		responseFileUploadStatusPoll(request,response);

	}
	
	/**
	 * 在上傳檔案列表中查詢與檔名相關的id
	 * @param request
	 * @param fileName 檔名
	 * @return 找到返回id,否則返回-1
	 */
	private int findFileIdInFileUploadedList(HttpServletRequest request,String fileName){
		FileUploadStatus fileUploadStatus=takeOutFileUploadStatusBean(request.getSession());
		for(int i=0;i<fileUploadStatus.getUploadFileUrlList().size();i++){
			if (fileName.equals((String)fileUploadStatus.getUploadFileUrlList().get(i))){
				return i;
			}
		}
		return -1;
	}
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		doPost(request,response);
	}  	
	

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		boolean isMultipart = ServletFileUpload.isMultipartContent(request);
		
		if (isMultipart) {
			processFileUpload(request,response);
		}else{
			request.setCharacterEncoding("UTF-8");
			
			if (request.getParameter("uploadStatus")!=null){
				responseFileUploadStatusPoll(request,response);
			}
			if (request.getParameter("cancelUpload")!=null){
				processCancelFileUpload(request,response);
			}
		}
		
	}   	  	    
}
 

至此,伺服器端的程式碼已經基本完成。

5. 客戶端實現

由於在上傳檔案時需要在同一頁面顯示對應的進度條控制元件,因此,在提交表單時當前頁面不能被重新整理。我們可以通過將表單提交至一個隱藏的 iframe 中來實現。關於Ajax的技術前面講過,這裡就不再細說,直接給出原始碼如下:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>基於Ajax的上傳檔案顯示進度條</title>
 <style>
  .prog-border {
  height: 15px;
  width: 205px;
  background: #fff;
  border: 1px solid #000;
  margin: 0;
  padding: 0;
  }
  .prog-bar {
  height: 11px;
  margin: 2px;
  padding: 0px;
  background: #178399;
  font-size: 10pt;
  }
  body{
	font-family: Arial, Helvetica, sans-serif;
	font-size: 10pt;
  }
  </style>
<script language="javascript" type="text/javascript">
<!--
    //var userName=document.getElementById("userName").value;
    //建立跨瀏覽器的XMLHttpRequest物件
    var timer;
function startListener(){
	var xmlhttp;
	try{
	//IE 5.0 
		xmlhttp = new ActiveXObject('Msxm12.XMLHTTP');
	}catch(e){
		try{
		//IE 5.5 及更高版本
			xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
		}catch(e){
			try{
			//其他瀏覽器
				xmlhttp = new XMLHttpRequest();
			}catch(e){}
		}
	}
	var progressStatusText = document.getElementById("progressBar");
	xmlhttp.open("get","UploadServlet?uploadStatus=true",true);
	/**此處Header設定非常重要,必須設定Content-type型別,負責會報錯誤
	*/
	 xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	 xmlhttp.onreadystatechange = function(){
		if(xmlhttp.readyState == 4){
			if(xmlhttp.status == 200){
			progressStatusText.innerHTML = "";
			progressStatusText.innerHTML = xmlhttp.responseText;
			var temp = xmlhttp.responseText.indexOf("success");
			if (  temp > 0 ){
			window.clearTimeout(timer);
			}else{
			timer = window.setTimeout(startListener,1000);
			}
			}
		}
	}
	xmlhttp.send(null);
}
function startUpload(){
    timer = window.setTimeout(startListener,1000);
	return true;
}
function cancelUpload(){
	var xmlhttp;
	try{
	//IE 5.0 
		xmlhttp = new ActiveXObject('Msxm12.XMLHTTP');
	}catch(e){
		try{
		//IE 5.5 及更高版本
			xmlhttp = new ActiveXObject('Microsoft.XMLHTTP');
		}catch(e){
			try{
			//其他瀏覽器
				xmlhttp = new XMLHttpRequest();
			}catch(e){}
		}
	}
	var progressStatusText = document.getElementById("progressBar");
	xmlhttp.open("get","UploadServlet?cancelUpload=true",true);
	 xmlhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
	//xmlhttp.setRequestHeader("Content-type", "multipart/form-data");
	xmlhttp.onreadystatechange = function(){
		if(xmlhttp.readyState == 4){
			if(xmlhttp.status == 200){
			progressStatusText.innerHTML = "";
			progressStatusText.innerHTML = xmlhttp.responseText;
			}
		}
	}
	xmlhttp.send(null);
	return false;
}
//-->
</script>
</head>
<body>
<div id="controlPanel">
	<!-- 這個是隱藏的<iframe>作為表單提交後處理的後臺目標
  		通過表單form的target屬性指定該<iframe>將返回資訊顯示在<iframe>框架中
  -->
  <iframe id='target_upload' name='target_upload' src='' style='display: none'></iframe>
	<form id="fileUploadForm" name="fileUploadForm" action="UploadServlet" 
		enctype="multipart/form-data" method="post" onsubmit="return startUpload();" target="target_upload">
	<input type="file" name="file" id="file" size="40"/><br>
	<input type="submit" name="uploadButton" id="uploadButton" value="開始上傳"/>
	<input type="button" name="cancelUploadButton" id="cancelUploadButton" value="取消上傳" onclick="return cancelUpload();"/><br>
	</form>	
	<div id="progressBar">
   </div>  
</div>
</body>
</html>
 

     至此,整個檔案上傳的實現到此完成,讀者可以在此基礎上,發揮自己的創新能力,去完善此例項。

Good Luck!