我和我四次版本的上傳模塊
功能說明
我已經做了很多個功能模塊了,現在想來整理一下做過的功能,一些web應用的基礎功能。
第一個就是上傳模塊,說起來,上傳看起來一件簡單的事情,如果是以前,能上傳就是一個了不起的功能了。可是現在不一樣了,我們要給用戶更好的體驗,所以就有了我四個版本的上傳模塊,分別如下:
1. 簡單
2. 異步
3. 進度條
4. 斷點
簡單
簡單上傳就是一個傳統的form表單上傳,一個簡單粗暴的上傳方式。用戶必須開着頁面,等待上傳,而且上傳極有可能不成功,特別是大文件的時候。
但是也是有優點的,那就是代碼量特少。這並不好,我一直覺得寧願我多寫兩行代碼,讓用戶更加方便,也不要省那兩行。
上傳頁面
simple_upload.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>簡單提交頁面</title>
</head>
<body>
<form method="post" action="simple_upload" enctype="multipart/form-data">
<input name="file" type="file" accept="image/gif,image.jpg"/>
<input name="token" type="hidden"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
說明教程外鏈:W3School
後臺處理
我主要提供兩種後端處理的代碼,第一種就是使用org.apache.commons.fileupload.*
package com.chen.servlet;
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.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
/**
* Created by CHEN on 2016/8/10.
*/
@WebServlet("/simple_upload")
public class SimpleUploadFile extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//檢查token,要是錯誤的token,禁止上傳,返回錯誤頁面之類的
//TODO 很多代碼
//磁盤文件項目工廠
DiskFileItemFactory factory=new DiskFileItemFactory();
//設置文件閥值,當文件超過5M的時候,產生臨時文件並存儲在臨時的目錄中
factory.setSizeThreshold(5*1024*1024);
//設置緩存路徑
factory.setRepository(new File(req.getServletContext().getRealPath("/temp")));
//上傳的工具
ServletFileUpload upload=new ServletFileUpload(factory);
//設置上傳的文件最大值
upload.setSizeMax(50*1024*1024);
try {
List<FileItem> items=upload.parseRequest(req);
if(items.size()<0) { //一個表單項都沒有
} else {
for(FileItem item:items) {
if(item.isFormField()) {//表單項,而不是文件上傳項
}else {//文件項
String filename=item.getName();
//後綴檢查
String filesuffix=filename.substring(filename.lastIndexOf("."));
//TODO 很多代碼
StringBuffer buffer=new StringBuffer(req.getServletContext().getRealPath("/file"));
buffer.append("/");
buffer.append(UUID.randomUUID().toString());
buffer.append(filesuffix);
filename= buffer.toString();
//
FileOutputStream out=new FileOutputStream(filename);
InputStream in=item.getInputStream();
//開始寫文件
byte[] b=new byte[1024];
int len=0;
while((len=in.read(b))>0) {
out.write(b,0,len);
}
in.close();
out.close();
item.delete();//如果有臨時文件就刪除
}
}
}
} catch (FileUploadException e) {
//TODO 異常處理之類的
e.printStackTrace();
}
//成功的頁面
resp.sendRedirect("success_page");
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//跳轉鏈接
resp.sendRedirect("simple_upload.html");
}
}
像上面的代碼,是一個簡單的上傳模塊。
異步
有時候,用戶可不想在那裏傻傻的等待你跳轉頁面,他想要有其他事情同時可以做。那這時候,就要使用異步的方式,但是其實異步也和之前的無兩樣,只是把表單提取出來,提交而已。
上傳頁面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>異步上傳頁面</title>
</head>
<body>
<form>
<span id="message"></span>
<input id="file" name="file" type="file"/>
<input id="token" name="token" type="hidden"/>
</form>
<script src="https://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script src="/js/ajaxfileupload.js"></script>
<script>
$("#file").on("change",function() {
$.ajaxFileUpload({
url: "ajax_upload",
fileElementId : 'file',//上傳文件的ID
dataType:"json",//返回數據格式
success :function(data) {
$("#message").html(data['message']);
},
error:function(status) {
}
});
});
</script>
</body>
</html>
後臺處理
後臺處理基本差不多,就是把跳轉換成了輸出JSON。
package com.chen.servlet;
import net.sf.json.JSONObject;
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.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;
import java.util.UUID;
/**
* Created by CHEN on 2016/8/10.
*/
@WebServlet("/ajax_upload")
public class AjaxUploadFile extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
//檢查token,要是錯誤的token,禁止上傳,返回錯誤頁面之類的
//TODO 很多代碼
//磁盤文件項目工廠
DiskFileItemFactory factory=new DiskFileItemFactory();
//設置文件閥值,當文件超過5M的時候,產生臨時文件並存儲在臨時的目錄中
factory.setSizeThreshold(5*1024*1024);
//設置緩存路徑
factory.setRepository(new File(req.getServletContext().getRealPath("/temp")));
//上傳的工具
ServletFileUpload upload=new ServletFileUpload(factory);
//設置上傳的文件最大值
upload.setSizeMax(50*1024*1024);
try {
List<FileItem> items=upload.parseRequest(req);
if(items.size()<0) { //一個表單項都沒有
} else {
for(FileItem item:items) {
if(item.isFormField()) {//表單項,而不是文件上傳項
}else {//文件項
String filename=item.getName();
//後綴檢查
String filesuffix=filename.substring(filename.lastIndexOf("."));
//TODO 很多代碼
StringBuffer buffer=new StringBuffer(req.getServletContext().getRealPath("/file"));
buffer.append("/");
buffer.append(UUID.randomUUID().toString());
buffer.append(filesuffix);
filename= buffer.toString();
//
FileOutputStream out=new FileOutputStream(filename);
InputStream in=item.getInputStream();
//開始寫文件
byte[] b=new byte[1024];
int len=0;
while((len=in.read(b))>0) {
out.write(b,0,len);
}
in.close();
out.close();
item.delete();//如果有臨時文件就刪除
}
}
}
} catch (FileUploadException e) {
//TODO 異常處理之類的
e.printStackTrace();
}
//成功提示
JSONObject data=new JSONObject();
data.put("message","success");
PrintWriter out=resp.getWriter();
out.print(data);
out.close();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//跳轉鏈接
resp.sendRedirect("simple_upload.html");
}
}
進度條
異步模塊看起來已經是不錯的了,但是 沒有一個進度條之類的,就讓用戶在那裏等着,直到上傳成功,就突然跳出一句上傳成功,感覺這樣的交互,不好,所以在第三次中,我增加了一個進度條的功能,使用的是jQuery中的Uploadify。
詳細文檔:Uploadify
上傳頁面
<%@ page language="java" contentType="text/html; charset=utf-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Upload</title>
<!--裝載文件-->
<link href="uploadify-v3.1/uploadify.css" type="text/css" rel="stylesheet" />
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="uploadify-v3.1/jquery.uploadify-3.1.js"></script>
<script type="text/javascript" src="uploadify-v3.1/swfobject.js"></script>
<!--ready事件-->
<script type="text/javascript">
$(document).ready(function () {
$("#uploadify").uploadify({
'debug':'true',
'method':'post',
'formData':{'chen':'chen'},//發送給服務端的參數,格式:{key1:value1,key2:value2}
'swf': 'uploadify-v3.1/uploadify.swf',
'uploader':'file_upload',
//'script':'upload!doUpload.action?name=yangxiang',
//'script': 'servlet/Upload?name=yangxiang',
//'cancel' : 'uploadify-v3.1/uploadify-cancel.png',
'queueID' : 'fileQueue', //和存放隊列的DIV的id一致
//'fileDataName': 'fileupload', //必須,和以下input的name屬性一致
'auto' : false, //是否自動開始
'multi': true, //是否支持多文件上傳
'buttonText': '選擇文件', //按鈕上的文字
'simUploadLimit' : 1, //一次同步上傳的文件數目
//'sizeLimit': 19871202, //設置單個文件大小限制,單位爲byte
'fileSizeLimit' : '6000MB',
'queueSizeLimit' : 10, //隊列中同時存在的文件個數限制
//'fileTypeExts': '*.jpg;*.gif;*.jpeg;*.png;*.bmp;*.iso',//允許的格式
//'fileTypeDesc': '支持格式:jpg/gif/jpeg/png/bmp/iso.', //如果配置了以下的'fileExt'屬性,那麼這個屬性是必須的
'onUploadSuccess': function ( fileObj, response, data) {
alert("文件:" + fileObj.name + "上傳成功");
},
'onUploadError': function(event, queueID, fileObj) {
alert("文件:" + fileObj.name + "上傳失敗");
},
'onCancel': function(event, queueID, fileObj){
alert("取消了" + fileObj.name);
}
});
});
</script>
</head>
<body>
<div id="fileQueue" style="width: 30%"></div>
<input type="file" name="file" id="uploadify" />
<p>
<a href="javascript:jQuery('#uploadify').uploadify('upload','*')">開始上傳</a>
<a href="javascript:$('#uploadify').uploadify('cancel',$('.uploadifive-queue-item').first().data('file'))">取消上傳</a>
<a href="javascript:$('#uploadify').uploadify('cancel','*')">清空所有的上傳文件</a>
<a href="javascript:$('#uploadify').uploadify('stop','*')">暫停</a>
<!-- 如果填入true則表示禁用上傳按鈕 -->
<a href="javascript:$('#uploadify').uploadify('disable','true')">禁用</a>
<a href="javascript:$('#uploadify').uploadify('debug')">調試</a>
</p>
</body>
</html>
後臺處理
import net.sf.json.JSONObject;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by CHEN on 2016/8/8.
*/
@WebServlet("/file_upload")
public class FileServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map map = new HashMap();
request.setCharacterEncoding("utf-8");
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setRepository(new File("I:\\file\\temp"));
factory.setSizeThreshold(1024*1024) ;
ServletFileUpload upload = new ServletFileUpload(factory);
try {
//可以上傳多個文件
List<FileItem> list = (List<FileItem>)upload.parseRequest(request);
if(list.size()<0) {
} else {
for(FileItem item : list){
if(!item.isFormField()){
String name = item.getName() ;
String fileSuffix = name.substring(name.lastIndexOf(".")+1,name.length());
String oldName = name.replaceAll("." + fileSuffix,"");
String fileName = ""+System.currentTimeMillis();
String newName = fileName + "." + fileSuffix;
OutputStream out = new FileOutputStream(new File("I:\\file\\",newName));
InputStream in = item.getInputStream() ;
int length = 0 ;
byte [] buf = new byte[1024] ;
while( (length = in.read(buf) ) != -1){
out.write(buf, 0, length);
}
in.close();
out.close();
/**將上傳處理後的數據返回**/
map.put("fileSuffix",fileSuffix);
map.put("fileName",oldName);
map.put("filePath",fileName);
item.delete();
break;
}
}
}
}catch (Exception e) {
System.out.println("出錯了:" + e.getMessage());
}
response.setContentType("text/xml; charset=UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
PrintWriter out = response.getWriter();
JSONObject jsonObject = JSONObject.fromObject(map);
String msg = jsonObject.toString();
out.print(msg);
out.close();
}
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
doPost(req, resp);
}
}
上面的代碼順帶解決了前端傳參給servlet的難題。
斷點續傳
總的來說,這個功能看起來已經不錯了,都有進度條了,很友好。可是還是不夠了,假如用戶上傳2G的文件呢,上傳到差不多90%了,可是手一抖,關了瀏覽器,那就什麼都沒有了。
在博客園,看到了一個很好的博文,呂大豹的《支持斷點續傳的文件上傳插件——Huploadify-V2.0來了》
提出了很好的解決方案,那就是對上傳的文件進行分割,每次只上傳一部分,最後進行追加,合併。
代碼如下
上傳頁面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Title</title>
<link rel="stylesheet" type="text/css" href="Huploadify.css"/>
</head>
<body>
<div id="upload"></div>
<button id="btn1">暫停</button>
<button id="btn2">上傳</button>
<button id="btn3">取消</button>
<button id="btn4">disable</button>
<button id="btn5">ennable</button>
<button id="btn6">destroy</button>
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="jquery.Huploadify.js"></script>
<style type="text/css">
</style>
<script type="text/javascript">
$(function(){
var up = $('#upload').Huploadify({
auto:true,
fileTypeExts:'*.vmfk;*.jpg;*.png;*.exe;*.mp3;*.mp4;*.zip;*.doc;*.docx;*.ppt;*.pptx;*.xls;*.xlsx;*.pdf',
multi:true,
fileSizeLimit:99999999,
breakPoints:true,
saveInfoLocal:true,
showUploadedPercent:true,//是否實時顯示上傳的百分比,如20%
showUploadedSize:true,
removeTimeout:9999999,
uploader:'uploadfile',
onUploadStart:function(){
//up.settings('formData', {aaaaa:'1111111',bb:'2222'});
var filename=$('.up_filename').eq(0).text();
up.Huploadify('settings','formData', {'filename':filename});
},
onUploadSuccess:function(file){
//alert('上傳成功');
},
onUploadComplete:function(){
//alert('上傳完成');
},
/*getUploadedSize:function(file){
var data = {
data : {
fileName : file.name,
lastModifiedDate : file.lastModifiedDate.getTime()
}
};
var url = 'http://49.4.132.173:8080/admin/uploadfile/index/';
var uploadedSize = 0;
$.ajax({
url : url,
data : data,
async : false,
type : 'POST',
success : function(returnData){
returnData = JSON.parse(returnData);
uploadedSize = returnData.uploadedSize;
}
});
return uploadedSize;
} */
});
$('#btn1').click(function(){
up.stop();
});
$('#btn2').click(function(){
up.upload('*');
});
$('#btn3').click(function(){
up.cancel('*');
});
$('#btn4').click(function(){
//up.disable();
up.Huploadify('disable');
});
$('#btn5').click(function(){
up.ennable();
});
$('#btn6').click(function(){
up.destroy();
});
});
</script>
</body>
</html>
基本是他的例子,有一點特殊的就是,我向後臺傳了一個文件名的參數。
後臺處理
import net.sf.json.JSONObject;
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.ServletFileUpload;
import org.apache.commons.fileupload.servlet.ServletRequestContext;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Created by CHEN on 2016/8/8.
*/
@WebServlet("/hu_load/uploadfile")
public class UploadFile extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req,resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("utf-8");
resp.setCharacterEncoding("utf-8");
Map map=new HashMap();
DiskFileItemFactory factory=new DiskFileItemFactory();
factory.setSizeThreshold(10*1024);
factory.setRepository(new File(req.getServletContext().getRealPath("/temp")));
ServletFileUpload upload=new ServletFileUpload(factory);
upload.setHeaderEncoding("utf-8");
List<FileItem> list= null;
try {
list = upload.parseRequest(new ServletRequestContext(req));
} catch (FileUploadException e) {
e.printStackTrace();
}
if(list.size()<0) {
}else {
String filename="";
for(FileItem item:list) {
if("".equals(filename)&&"filename".equals(item.getFieldName())) {
filename=new String(item.getString().getBytes("iso-8859-1"),"utf-8");
break;
}
}
for(FileItem item:list) {
if(!item.isFormField()) {
File file=new File("I:\\file\\"+filename);
OutputStream out;
InputStream in = item.getInputStream() ;
if(file.exists()) {
out=new FileOutputStream(file,true);
}else {
out = new FileOutputStream(file);
}
int length = 0 ;
byte [] buf = new byte[1024] ;
while( (length = in.read(buf) ) != -1){
out.write(buf, 0, length);
}
in.close();
out.close();
/**將上傳處理後的數據返回**/
/* map.put("fileSuffix",fileSuffix);
map.put("fileName",oldName);
map.put("filePath",fileName);*/
item.delete();
break;
}
}
}
//可有可無
resp.setContentType("text/xml; charset=UTF-8");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("Pragma", "no-cache");
PrintWriter out = resp.getWriter();
JSONObject jsonObject = JSONObject.fromObject(map);
String msg = jsonObject.toString();
out.print(msg);
out.close();
}
}
一開始我也沒有辦法獲得前端傳來的參數,後來我打開他的源碼,看了這樣的一個函數
//發送文件塊函數
var sendBlob = function(url,xhr,file,formdata){
xhr.open(option.method, url, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
var fd = new FormData();
fd.append(option.fileObjName,file);
if(formdata){
for(key in formdata){
fd.append(key,formdata[key]);
}
}
xhr.send(fd);
}
一開始我對他進行了修改,改成了xhr.send(“filename”,filename),然後後臺用getPart()去接受參數,經過了很多次折騰。後臺終於能拿到前端參數了,就像我寫的那樣,首先要遍歷所有的FileItem,拿到文件名,然後纔開始寫文件。
後言
上傳功能算是結束了嗎,不,還沒有。還有類如拍照上傳、拖拽上傳等等沒有實現,這些就留着你去思考了。提示一下,基本要依靠HTML5的特性。