這個仿163網盤無刷新文件上傳系統,並沒有用使用.net的控件,完全的手工製作。前臺基本上是靜態的,跟後臺沒有關係,所以後臺用什麼語言做都可以(後面有各個版本的實例下載)。
本來覺得這個系統會很複雜,但把每個部分都分析清楚後,其實需要的技術並不高。不過當我把各個功能函數都整理好準備進行封裝時,卻發現要把程序封裝不是那
麼容易,因爲程序跟html的耦合度太高。然後我逐步把程序中操作html相關的部分分離出來,首先把簡單的分離,接着是文件列表,然後是file控件,
最後是一些提示性程序。經過幾次嘗試才把整個結構封裝好,現在程序結構應該算比較清晰,有什麼不明白的地方歡迎留言。
效果預覽
這裏的預覽只是前臺的效果,要整個系統測試請下載完整實例。
程序說明
【無刷新上傳】
要實現文件上傳,form必須設置幾個屬性:
1.action:設爲要處理數據的頁面地址;
2.method:設爲"post";
3.enctype/encoding:必須設爲"multipart/form-data",這裏要注意的是在ie中用js修改form的enctype屬性是沒有效果的,只能修改encoding;
後面兩個屬性程序初始化時都有設置:
this .Form.encoding = " multipart/form-data " ;
要注意這裏的無刷新不是ajax哦,而是利用“古老”的iframe。
由於ajax提交數據必須先獲取數據,而js(一般情況下)是不能操作客戶端文件,要獲取文件數據就更不用說了,所以只能用iframe來做。
先說說iframe實現無刷新上傳的原理:利用form的target屬性,把數據提交到頁面中一個(通常爲隱藏的)iframe上。
直觀點說就是把“刷新”留給iframe。
其實原理跟一般用iframe實現無刷新提交表單是一樣的,只是這裏換成是文件。
這裏關鍵就是把form的target設爲iframe的name:
【iframe】
如果沒有自定義iframe,程序在初始化時會自動創建無刷新所需的iframe的。
首先必須選擇一個iframe名,這在無刷新時是必須的,爲了每個實例能創建各自的iframe,這裏用了一個隨機數:
也可以用一個遞增的計算器來代替隨機數。
接着創建iframe,本以爲用document.createElement("iframe")創建再設置它的name屬性就行了。
卻發現這樣設置的name在ie居然不認(有說name是隻讀屬性),還好在網上找到一個方法:“IE 創建元素,還有一個特點,就是可以連同屬性一同創建”。
例如我想給動態創建的iframe設置name,可以這樣:
不過這個方式在ff會報錯:
uncaught exception: String contains an invalid character (NS_ERROR_DOM_INVALID_CHARACTER_ERR)
估計是用createElement時不能帶name,標準應該也是這樣,所以兼容的方式這樣寫:
var oFrame = isIE ? document.createElement( " <iframe name=/ "" + this._FrameName + " / " > " ) : document.createElement( " iframe " );
// 爲ff設置name
oFrame.name = this ._FrameName;
oFrame.style.display = " none " ;
關於這方面更詳細的內容請看這裏
。
創建完還需要插入到body中,一般的做法是使用document.body.appendChild,但在ie中會有“已終止操作”錯誤,可以用下面這段代碼測試:
< body >
< div >
< script >
document.body.appendChild(document.createElement( " div " ));
< / script>
< / div>
< / body>
網上找到一個解析:“原來FF下的實現機制是當頁面還沒有完全讀取完時body元素就已經存在了,而IE只有頁面完全讀取結束body元素纔會存在,所以在頁面中插入上面這條語句在IE下就會出現錯誤”。
我在web開發未解之謎
中也提到了這個現象,我這裏使用了insertBefore代替:
在服務器端文件傳送完(或失敗)之後,怎麼通知客戶端呢?
這裏說說我的方法,首先我在客戶端定義一個函數:
很簡單,就是顯示提示並重新加載頁面(如果使用reload會導致ff中iframe重複加載數據)。
那服務器端如何通知客戶端的問題,就是iframe如何跟主頁面交互。
答案是通過window.parent或window.top,在iframe中parent和top屬性“分別返回立即父窗口和最上層的祖先窗口”。
例如我在服務器端處理完數據之後會輸出:
就會執行主頁面的Finish函數了。
【多文件上傳】
對於多文件上傳,這裏的目的是如何做到163網盤那樣,只用一個file控件就實現多文件上傳。
這裏參考了163網盤的思路,下面說說如何實現:
首先必須有一個文件空間(我自己定的名字),例如程序中的"idFile"對象,這個空間不需要內容甚至一個div就可以,主要是用來存放file控件,程序中Folder屬性就是這個文件空間對象。
ps:這裏的要求是把file控件都控制在文件空間裏,即使不是單file控件的情況。
再說說Files屬性,這個屬性放的是file控件集合,方便獲取file控件,在下面“文件列表”就會用到。
處理這些file控件的程序主要在Ini函數中:
首先是處理文件空間中的file控件:
this .Files = [];
// 整理文件空間,把有值的file放入文件集合
Each( this .Folder.getElementsByTagName( " input " ), Bind( this , function (o){
if (o.type == " file " ){ o.value && this .Files.push(o); this .onIniFile(o); }
}))
可以看到這裏主要是把file控件放入到Files中,並執行附加函數onIniFile,我是這樣定義這個函數的:
這裏爲了實現單file控件,把原來有值的file都隱藏了,還有那個“單file控件”呢?
別急,接着就在文件空間插入一個新的file控件:
var file = document.createElement( " input " );
file.name = this .FileName; file.type = " file " ; file.onchange = Bind( this , function (){ this .Check(file); this .Ini(); });
this .Folder.appendChild(file);
可以看到file控件的name是FileName屬性的值,默認是空的,如果服務器端需要這個name的話就可以設置。
這裏可以看到每個file控件都有onchange來執行檢測函數Check,這樣每次選擇文件後都會用Check檢測一次,這裏說說這個Check函數:
// 檢測變量
var bCheck = true ;
// 進行空值、文件數、後綴名、同值檢測
if ( ! file.value){
bCheck = false ; this .onEmpty();
} else if ( this .Limit && this .Files.length >= this .Limit){
bCheck = false ; this .onLimite();
} else if ( !! this .ExtIn.length && ! RegExp( " /.( " + this .ExtIn.join( " | " ) + " )$ " , " i " ).test(file.value)){
// 檢測是否允許後綴名
bCheck = false ; this .onNotExtIn();
} else if ( !! this .ExtOut.length && RegExp( " /.( " + this .ExtOut.join( " | " ) + " )$ " , " i " ).test(file.value)) {
// 檢測是否禁止後綴名
bCheck = false ; this .onExtOut();
} else if ( !! this .Distinct) {
Each( this .Files, function (o){ if (o.value == file.value){ bCheck = false ; } })
if ( ! bCheck){ this .onSame(); }
}
裏面有一個檢測變量bCheck,然後進行空值、文件數限制、後綴名、相同文件的檢測,當其中一個步驟不通過bCheck就會設爲false,一個常用的檢測結構。
這裏說說檢測後綴名,由於js不能像後臺那樣獲取文件的文件類型,所以只能根據後綴名來判斷,例如用正則判斷:
這樣判斷顯然是不夠的,所以如果要做文件類型判斷的話一定要在後臺用ContentType再判斷一次。
最後如果沒有通過檢測就會執行onFail函數:
我在onFail函數中設定了移除沒有通過檢測的file控件:
這樣就基本實現(正確的說是模擬)了單file控件上傳多個文件的效果了。
【文件列表】
在上面的Ini函數中,最後執行了一個附加函數onIni,這個函數是用戶自己定義的,我就在這個函數中添加文件列表。
在之前先說說添加文件列表的函數AddList,這個函數是用來把file控件的值列在一個table裏面。
函數的參數是一個二維數組,其中第一維是行(tr),第二維是列(td)。
首先獲取列表對象FileList,再定義一個文檔碎片oFragment來操作dom:
然後用兩個Each把二維數組插入到文檔碎片中:
Each(rows, function (cells){
var row = document.createElement( " tr " );
Each(cells, function (o){
var cell = document.createElement( " td " );
if ( typeof o == " string " ){ cell.innerHTML = o; } else { cell.appendChild(o); }
row.appendChild(cell);
});
oFragment.appendChild(row);
})
其中用了一個判斷if(typeof o == "string"),如果是文本就直接用innerHTML插入td,如果不是文本(這裏不是文本就是一個對象)就用appendChild插入到td。
當數據都插入到文檔碎片,就準備把文檔碎片插入到FileList中,不過還有一個步驟就是清空FileList中原有的數據。
本來把innerHTML設爲空來清空FileList會更有效率,但ie的table中只有td支持innerHTML,所以只好用removeChild來清空:
之後就可以把文檔碎片插入了:
繼續看onIni函數,現在只需要把要顯示的數據組成一個二維數組,再用AddList就能顯示文件列表了,這時存放file控件集合的Files屬性就大有用處了。
首先定義一個放顯示數據的數組:
然後根據Files對這個數組賦值:
if ( this .Files.length){
var oThis = this ;
Each( this .Files, function (o){
var a = document.createElement( " a " ); a.innerHTML = " 取消 " ; a.href = " javascript:void(0); " ;
a.onclick = function (){ oThis.Delete(o); return false ; };
arrRows.push([o.value, a]);
});
} else { arrRows.push([ " <font color='gray'>沒有添加文件</font> " , " " ]); }
AddRow(arrRows);
當Files沒有控件時只是輸出“沒有添加文件”,有控件時就會把每個file控件的要顯示數據放到一個數組中,可以看到這個數組其實就是td內容的集合,接着把這個數組加入到arrRows中形成二維數組,最後把得到的arrRows給AddRow函數顯示數據就行了。
爲了能取消指定的file控件,這裏插入了一個a來觸發刪除函數Delete,這裏也有一個技巧,這裏把href設爲"javascript:void(0);",並在onclick中返回false,這樣能最大程度的實現僅僅執行js而不去跳轉。
在表單提交時也要重新顯示文件列表,表單提交後就不允許刪除文件了,只顯示文件路徑就行了:
$( " idBtnupload " ).onclick = function (){
// 顯示文件列表
var arrRows = [];
Each(fu.Files, function (o){ arrRows.push([o.value, " " ]); });
AddList(arrRows);
fu.Folder.style.display = " none " ;
$( " idProcess " ).style.display = "" ;
$( " idMsg " ).innerHTML = " 正 在添加文件到您的網盤中,請稍候……<br />有可能因爲網絡問題,出現程序長時間無響應,請點擊“<a href='?'& gt;<font color='red'>取消</font></a>”重新上傳文件 " ;
fu.Form.submit();
}
說到表單提交要注意一個問題,就是表單是不能嵌套的,最好是把表單放到服務器表單之外,沒有辦法才使用服務器表單作爲提交表單(由於程序會修改提交表單的屬性,所以儘量不要這樣使用)。
這樣文件列表就完成了,有興趣的話也可以自己封裝一下這個功能。
【file樣式】
到此,程序的功能都已經實現了,但在163網盤中還有一個特別的地方,就是file控件的樣式。
如果有用過163網盤上傳文件,就知道那個file控件就像一個按鈕,但功能確實是一個file控件。
但當自己嘗試修改file控件的樣式時,發現單單設置file控件的樣式並不能實現想要的效果。
於是我想了另一個辦法,用一個button來模擬,結果發現也不行,用js根本操作不了file控件,應該是考慮到安全問題吧。
最後是參考了163網盤和muxrwc模擬126附件添加的效果
,總結了這個方法:
1.指定用一個容器(例如程序中的idFile)。
容器最好指定高和寬,並且overflow爲hidden,不是塊級元素的最好設display爲block(爲了高和寬的正確呈現);
2.在容器裏放一個file控件,並設置樣式,使能觸發彈出選擇文件框的部分覆蓋整個容器,並設置成全透明。
容器指定準確的高和寬就是爲了能通過file控件中不多的能設置的樣式來覆蓋整個容器;
3.現在已經把容器模擬成file控件了,可以直接設置容器的樣式來模擬設置file控件的樣式了。
在程序中主要用file控件的margin-left和font-size來實現覆蓋整個容器:
a.files input {
margin-left : -350px ;
font-size : 30px ;
cursor : pointer ;
filter : alpha(opacity=0) ;
opacity : 0 ;
}
至於容器,我使用了有僞類hover的a元素(雖然CSS2中hover可以應用於任何對象,但ie6不支持)。
這裏用了一個常用的小技巧,就是用一張圖片作爲背景通過在hover時修改background-position來實現兩張圖片的效果:
a.files {
width : 90px ;
height : 30px ;
overflow : hidden ;
display : block ;
border : 1px solid #BEBEBE ;
background : url(img/fu_btn.gif) left top no-repeat ;
text-decoration : none ;
}
a.files:hover {
background-color : #FFFFEE ;
background-position : 0 -30px ;
}
在點擊這個a時後會出現一個虛線框,在這裏顯然不太美觀,可以把outline設爲none來去掉,可是ie又不支持,在網上找到一個方法ie可以 把hideFocus設爲true來隱藏聚焦(即不顯示這個虛線框,hideFocus可以在js或html中設置,也可以通過expression放到 css中:
a.files, a.files input {
outline : none ; /* ff* /
hide-focus:expression(this.hideFocus=true);/*ie* /
}
這樣完全模擬了163網盤的效果了。
【後臺】
前臺基本完成了,就到後臺啦。後臺的功能很簡單,就是處理傳遞過來的文件數據。
這裏像js + .Net 圖片切割系統
那樣使用ashx文件處理IHttpHandler發送過來的數據。
程序很簡單,就直接貼代碼了:
int iTotal = context.Request.Files.Count;
if (iTotal == 0 )
{
_msg = " 沒有數據 " ;
}
else
{
int iCount = 0 ;
for ( int i = 0 ; i < iTotal; i ++ )
{
HttpPostedFile file = context.Request.Files[i];
if (file.ContentLength > 0 || ! string .IsNullOrEmpty(file.FileName))
{
// 保存文件
file.SaveAs(System.Web.HttpContext.Current.Server.MapPath( " ./file/ " + Path.GetFileName(file.FileName)));
// 這裏可以根據實際設置其他限制
if ( ++ iCount > UploadFileLimit)
{
_msg = " 超過上傳限制: " + UploadFileLimit;
break ;
}
}
}
}
這裏只檢測了有無文件和文件數限制,其他檢測如文件大小等可以自己擴展,應該不難。
處理完數據之後就通知客戶端:
這個在上面iframe的內容中已經說明了。
使用說明
基本使用很簡單,實例化一個file對象,其中參數分別是form對象,文件空間對象:
這樣就實現了一個簡單的無刷新上傳文件表單。
還可以使用這幾個屬性:
Form//表單
Folder//文件控件存放空間
Files//文件集合
更多的功能可以選擇設置這些屬性:
屬性名:默認值//說明
FileName:"",//文件上傳控件的name,配合後臺使用
FrameName:"",//iframe的name,要自定義iframe的話這裏設置name
onIniFile:function(){},//整理文件時執行(其中參數是file對象)
onEmpty:function(){},//文件空值時執行
Limit:0,//文件數限制,0爲不限制
onLimite:function(){},//超過文件數限制時執行
Distinct:true,//是否不允許相同文件
onSame:function(){},//有相同文件時執行
ExtIn:[],//允許後綴名
onNotExtIn:function(){},//不是允許後綴名時執行
ExtOut:[],//禁止後綴名,當設置了ExtIn則ExtOut無效
onExtOut:function(){},//是禁止後綴名時執行
onFail:function(){},//文件不通過檢測時執行(其中參數是file對象)
onIni:function(){}//重置時執行
使用方法可以參考實例。
程序中提供了下面幾個方法:
Ini 整理空間
Check 檢測file對象
Delete 刪除指定file
Clear 刪除全部file
程序代碼
var isIE = (document.all) ? true : false ;
var $ = function (id) {
return " string " == typeof id ? document.getElementById(id) : id;
};
var Class = {
create: function () {
return function () {
this .initialize.apply( this , arguments);
}
}
}
var Extend = function (destination, source) {
for ( var property in source) {
destination[property] = source[property];
}
}
var Bind = function (object, fun) {
return function () {
return fun.apply(object, arguments);
}
}
var Each = function (list, fun){
for ( var i = 0 , len = list.length; i < len; i ++ ) { fun(list[i], i); }
};
//
var FileUpload = Class.create();
FileUpload.prototype = {
// 表單對象,文件控件存放空間
initialize: function (form, folder, options) {
this .Form = $(form); // 表單
this .Folder = $(folder); // 文件控件存放空間
this .Files = []; // 文件集合
this .SetOptions(options);
this .FileName = this .options.FileName;
this ._FrameName = this .options.FrameName;
this .Limit = this .options.Limit;
this .Distinct = !! this .options.Distinct;
this .ExtIn = this .options.ExtIn;
this .ExtOut = this .options.ExtOut;
this .onIniFile = this .options.onIniFile;
this .onEmpty = this .options.onEmpty;
this .onNotExtIn = this .options.onNotExtIn;
this .onExtOut = this .options.onExtOut;
this .onLimite = this .options.onLimite;
this .onSame = this .options.onSame;
this .onFail = this .options.onFail;
this .onIni = this .options.onIni;
if ( ! this ._FrameName){
// 爲每個實例創建不同的iframe
this ._FrameName = " uploadFrame_ " + Math.floor(Math.random() * 1000 );
// ie不能修改iframe的name
var oFrame = isIE ? document.createElement( " <iframe name=/ "" + this._FrameName + " / " > " ) : document.createElement( " iframe " );
// 爲ff設置name
oFrame.name = this ._FrameName;
oFrame.style.display = " none " ;
// 在ie文檔未加載完用appendChild會報錯
document.body.insertBefore(oFrame, document.body.childNodes[ 0 ]);
}
// 設置form屬性,關鍵是target要指向iframe
this .Form.target = this ._FrameName;
this .Form.method = " post " ;
// 注意ie的form沒有enctype屬性,要用encoding
this .Form.encoding = " multipart/form-data " ;
// 整理一次
this .Ini();
},
// 設置默認屬性
SetOptions: function (options) {
this .options = { // 默認值
FileName: "" , // 文件上傳控件的name,配合後臺使用
FrameName: "" , // iframe的name,要自定義iframe的話這裏設置name
onIniFile: function (){}, // 整理文件時執行(其中參數是file對象)
onEmpty: function (){}, // 文件空值時執行
Limit: 0 , // 文件數限制,0爲不限制
onLimite: function (){}, // 超過文件數限制時執行
Distinct: true , // 是否不允許相同文件
onSame: function (){}, // 有相同文件時執行
ExtIn: [], // 允許後綴名
onNotExtIn: function (){}, // 不是允許後綴名時執行
ExtOut: [], // 禁止後綴名,當設置了ExtIn則ExtOut無效
onExtOut: function (){}, // 是禁止後綴名時執行
onFail: function (){}, // 文件不通過檢測時執行(其中參數是file對象)
onIni: function (){} // 重置時執行
};
Extend( this .options, options || {});
},
// 整理空間
Ini: function () {
// 整理文件集合
this .Files = [];
// 整理文件空間,把有值的file放入文件集合
Each( this .Folder.getElementsByTagName( " input " ), Bind( this , function (o){
if (o.type == " file " ){ o.value && this .Files.push(o); this .onIniFile(o); }
}))
// 插入一個新的file
var file = document.createElement( " input " );
file.name = this .FileName; file.type = " file " ; file.onchange = Bind( this , function (){ this .Check(file); this .Ini(); });
this .Folder.appendChild(file);
// 執行附加程序
this .onIni();
},
// 檢測file對象
Check: function (file) {
// 檢測變量
var bCheck = true ;
// 空值、文件數限制、後綴名、相同文件檢測
if ( ! file.value){
bCheck = false ; this .onEmpty();
} else if ( this .Limit && this .Files.length >= this .Limit){
bCheck = false ; this .onLimite();
} else if ( !! this .ExtIn.length && ! RegExp( " /.( " + this .ExtIn.join( " | " ) + " )$ " , " i " ).test(file.value)){
// 檢測是否允許後綴名
bCheck = false ; this .onNotExtIn();
} else if ( !! this .ExtOut.length && RegExp( " /.( " + this .ExtOut.join( " | " ) + " )$ " , " i " ).test(file.value)) {
// 檢測是否禁止後綴名
bCheck = false ; this .onExtOut();
} else if ( !! this .Distinct) {
Each( this .Files, function (o){ if (o.value == file.value){ bCheck = false ; } })
if ( ! bCheck){ this .onSame(); }
}
// 沒有通過檢測
! bCheck && this .onFail(file);
},
// 刪除指定file
Delete: function (file) {
// 移除指定file
this .Folder.removeChild(file); this .Ini();
},
// 刪除全部file
Clear: function () {
// 清空文件空間
Each( this .Files, Bind( this , function (o){ this .Folder.removeChild(o); })); this .Ini();
}
}
【asp版本補充】
由於很多人問我asp版本的後臺該如何寫,所以決定寫一個給大家。
這裏我用了化境HTTP上傳程序2.1版(應該是最新版了)的無組件上傳類,但用的時候發現幾個問題(不知是我不會用還是asp本身的問題):
1,當file控件的name是空時,後臺會找不到文件;
2,文件名比較短時(例如我用"f"),後臺也找不到文件;
3,當有多個file控件,如果使用相同的name,後臺只會保存一個文件;
4,我在上傳文件後輸出的中文是亂碼(有時又正常)。
針對前3條,我加了一個RanName屬性,設爲true的話會自動生成隨機的file控件名,對於第4條,我發現如果字是直接寫在文檔上就不會亂碼,所以我這裏把輸出的文字都直接寫在文檔上沒有用變量。如果有兄弟知道怎麼解決這些問題記得告訴我哦。
下載完整測試代碼(.net)
下載完整測試代碼(asp)
感謝由csdn網友mengshan1986提供的php和jsp版,klniuer的php修正版:
下載完整測試代碼(php)
下載完整測試代碼(jsp)
ps:請注意程序中的文件保存路徑,很多人的錯誤都是沒有設置好文件保存路徑。
其他上傳系統:
轉載請註明出處:http://www.cnblogs.com/cloudgamer/
程序中包含的js工具庫CJL.0.1.min.js,原文在這裏 。