ps:flash實現的效果是好得多,但這不是我研究的範圍,也沒什麼可比性。
相容:ie6/7/8, firefox 3.5.5, opera 10.01, safari 4.0.3, chrome 3.0
效果預覽
檔案上傳 |
選擇檔案 |
重新命名 |
操作 |
狀態 |
|
|
重設 |
選擇檔案 |
|
|
重設 |
選擇檔案 |
|
|
重設 |
選擇檔案 |
|
ps:由於需要後台,要測試系統請下載執行個體測試。
ps2:在完整執行個體檔案中,還有一個檔案屬性查看執行個體。
程式說明
【upload】
程式中最重要的方法就是upload了,調用它就可以進行無重新整理上傳。
upload的過程是這樣的,首先用stop方法停止上一次上傳,並判斷是否選擇檔案。
然後分別調用_setIframe,_setForm和_setInput,產生需要的iframe,form和input。
如果設定了timeout屬性的話,會自動化佈建計時器: 複製代碼 代碼如下:if ( this.timeout > 0 ) {
this._timer = setTimeout( $$F.bind(this._timeout, this), this.timeout * 1000 );
}
ps:經測試,小於0的延時時間,ie會取消執行,而其他瀏覽器會當成0執行。
程式有一個_sending屬性用來判斷上傳狀態。
在stop(停止),dispose(銷毀),_finis(完成),_timeout(逾時)時會把它設為false。
而在上傳開始前要把它設定為true。
最後提交表單就開始上傳了。
【iframe】
程式使用_setIframe函數來建立無重新整理需要的iframe。
由於ie中iframe的name不能修改的問題,要這樣建立iframe: 複製代碼 代碼如下:var iframename = "QUICKUPLOAD_" + QuickUpload._counter++,
iframe = document.createElement( $$B.ie ? "<iframe name=\"" + iframename + "\">" : "iframe");
iframe.name = iframename;
iframe.style.display = "none";
ps:關於iframe的name的問題參考這裡的iframe部分。
ie8已經可以修改name了,但在非標準(怪辟)模式下還是不能修改。
其中使用了一個QuickUpload函數自身的_counter屬性做計算機,這就能保證各個執行個體的iframe的name就不會重複。
為了能在檔案上傳完成後執行回呼函數,會在iframe的onload中執行_finish函數: 複製代碼 代碼如下:var finish = this._fFINISH = $$F.bind(this._finish, this);
if ( $$B.ie ) {
iframe.attachEvent( "onload", finish );
} else {
iframe.onload = $$B.opera ? function(){ this.onload = finish; } : finish;
}
在ie需要用attachEvent來綁定onload,因為在ie中直接設定onload是無效的。
除了用attachEvent還可以用onreadystatechange代替。
至於原因我也不清楚,詳細參考“判斷 iframe 是否載入完成的完美方法”。
iframe的載入還有一個問題,測試以下代碼:
複製代碼 代碼如下:<body><div id="msg">狀態:</div></body>
<script>
var msg = document.getElementById("msg");
var iframe = document.createElement("iframe");
iframe.onload = function(){ msg.innerHTML += "onload,"; }
document.body.appendChild(iframe);
iframe.src = "http://cloudgamer.cnblogs.com/"
</script>
結果safari, chrome都會觸發onload兩次,而opera, ff和ie(請自行相容)都是1次。
估計在safari和chrome在appendChild之後就進行第一次載入,並且在設定src之前載入完畢,所以觸發了兩次。
如果在插入body之前給iframe隨便設定一個src(除了空值),間接加長第一次載入,那麼也只觸發一次了。
ps:不設定或空值的src相當於連結到“about:blank”(空白頁)。
那麼opera, ff和ie可能是第一次載入太慢,第二次覆蓋了第一次的,所以只觸發了一次onload。
ps:也可能是其他原因,例如瀏覽器最佳化之類的,我也不確定。
針對載入過快的問題,可以在onload的時候根據_sending確定之前是否上傳狀態來解決。
雖然沒測試出來,會不會有_sending設定之後submit之前剛好觸發第一次onload的情況呢?
針對這個問題,在upload方法中會把_sending放在submit之後設定。
那如果在submit之後_sending設定之前就觸發了onload呢?(...囧)
這個情況基本不會出現,如果真的出現,就把_sending設定放到submit前面吧。
opera還有一個麻煩的問題,測試下面代碼: 複製代碼 代碼如下:<body>
<div id="msg">狀態:</div>
<form action="http://cloudgamer.cnblogs.com/" target="ifr">
</form>
</body>
<script>
var msg = document.getElementById("msg");
var iframe = document.createElement("iframe");
iframe.name = "ifr";
iframe.onload = function(){ msg.innerHTML += "onload,"; }
document.body.appendChild(iframe);
msg.innerHTML += "submit,";
document.forms[0].submit();
</script>
ie和ff顯示submit,onload,safari和chrome顯示的是onload,submit,onload,跟上面的分析一致。
而opera卻顯示submit,onload,onload,兩次onload都是在submit之後觸發的。
這個情況就不能單純用_sending來解決了。
是不是submit不能使iframe取消載入呢?
在appendChild之前設一個src,結果正常的只觸發onload一次,看來是可以的啊。
雖然不知道原因,辦法還是有的,一個是appendChild前設一個src,還可以在第一次onload中重新設定onload,像程式那樣。
但這兩個方法都存在不確定性,不能完全解決問題,但也找不到更好的方法了。
ff的onload還有一個問題,在出現ERROR_INTERNET_CONNECTION_RESET(檔案大小超過伺服器限制)之類的伺服器錯誤時,即使載入完成也不會觸發onload,暫時找不到解決辦法。
iframe有一個缺陷是只能用onload判斷載入完成,但沒有辦法判斷是否載入成功。
沒有類似XMLHTTP的status的東西,遇上404之類的錯誤也沒辦法判別出來。
在使用時要做好這方面的處理,例如說明允許上傳檔案大小,逾時時間,如何處理長時間無響應等。
【form】
程式使用_setForm函數來建立用來提交資料的form。
要實現無重新整理上傳,要對form進行特殊的處理: 複製代碼 代碼如下:$$.extend(form, {
target: this._iframe.name, method: "post", encoding: "multipart/form-data"
});
ps:詳細看這裡的無重新整理上傳部分。
由於form是手動插入的,為了不影響原來頁面配置還要設定一下form樣式,使它“隱形”起來: 複製代碼 代碼如下:$$D.setStyle(form, {
padding: 0, margin: 0, border: 0,
backgroundColor: "transparent", display: "inline"
});
還要注意的是,同一個表單控制項只能對應一個form。
如果file控制項本身已經有一個form的話,必須在提交前移除:
file.form && $$E.addEvent(file.form, "submit", $$F.bind(this.dispose, this));
dispose方法是用來銷毀程式的,包括移除form。
ps:如果提交前submit被覆蓋的話要手動執行一次dispose方法。
最後把form插入到dom:
file.parentNode.insertBefore(form, file).appendChild(file);
先把form插入到file控制項之前,然後把file插入到form,這樣就能保證file在原來的位置上了。
【input】
如果有其他參數要傳遞,程式會使用_setInput函數來建立傳遞資料的表單控制項。
由於產生的form裡面只有file控制項,要傳遞其他參數只能用程式產生了。
程式用一個_inputs集合來儲存當前在form中產生的表單控制項。
首先根據自訂的parameter屬性建立表單控制項:
複製代碼 代碼如下:for ( name in this.parameter ) {
var input = form[name];
if ( !input ) {
input = document.createElement("input");
input.name = name; input.type = "hidden";
form.appendChild(input);
}
input.value = this.parameter[name];
newInputs[name] = input;
delete oldInputs[name];
}
當form中沒有對應name的控制項時,會自動產生一個hidden控制項插入到form中。
其中newInputs是用來記錄當前產生的控制項的,而oldInputs就是_inputs集合。
當設定過對應name的控制項後,就從oldInputs中刪除對應控制項的關聯。
然後移除oldInputs關聯的控制項:
for ( name in oldInputs ) { form.removeChild( oldInputs[name] ); }
這樣就能移除上一次產生的無用的控制項了。
最後重新記錄當前控制項到_inputs方便下次使用。
【stop】
如果想停止當前上傳操作,可以調用stop方法。
一般來說當iframe發生重載時,會取消上一次的載入,那麼只要重新設定src就能取消上傳了。
測試以下代碼:
複製代碼 代碼如下:<body>
<iframe id="ifr" name="ifr"></iframe>
<form action="http://cloudgamer.cnblogs.com/" target="ifr">
</form>
</body>
<script>
document.forms[0].submit();
document.getElementById("ifr").src = "";
</script>
結果都能取消載入,除了opera,未知什麼原因。
有兩個方法解決,一個是通過form隨便用一個action提交一次,還有就是直接移除iframe。
後一個方法比較方便,程式中用_removeIframe方法直接移除iframe。
ps:有更好方法的話記得告訴我。
【dispose】
當使用結束或其他原因要銷毀程式時,可以調用dispose方法。
dispose裡面主要做的是移除iframe和form。
移除iframe用的是_removeIframe方法,首先把onload移除,再把iframe從body移除:
var iframe = this._iframe;
$$B.ie ? iframe.detachEvent( "onload", this._fFINISH ) : ( iframe.onload = null );
document.body.removeChild(iframe); this._iframe = null;
十分簡單,但在ff有一個問題,測試以下代碼: 複製代碼 代碼如下:<form target="ifr" action="x">
<input id="btn" type="submit" value="click">
</form>
<iframe name="ifr" id="ifr"></iframe>
<script>
document.getElementById("btn").onclick = function(){
document.getElementById("ifr").onload = function(){
this.parentNode.removeChild(this);
};
}
</script>
提交後都能移除iframe,但ff還一直顯示“載入中”的狀態。
不過解決方案也很簡單,用setTimeout設定一個延時,讓iframe執行完整就可以了。
所以在dispose中是這樣調用_removeIframe的: 複製代碼 代碼如下:if ( $$B.firefox ) {
setTimeout($$F.bind(this._removeIframe, this), 0);
} else {
this._removeIframe();
}
至於form的移除就比較簡單,在_removeForm這樣處理: 複製代碼 代碼如下:var form = this._form, parent = form.parentNode;
if ( parent ) {
parent.insertBefore(this.file, form); parent.removeChild(form);
}
this._form = this._inputs = null;
要判斷一下parentNode,否則如果parentNode不存在的話後面的會執行出錯。
【file的reset】
在執行個體裡,有一個用來重設file控制項的ResetFile函數。
重設file控制項一般的辦法是使所在的form執行reset,但問題是會把其他表單控制項也重設了。
以前由於安全問題,file的value是不允許修改的。
但現在ff,chrome和safari可以把它設為空白值來實現重設:
file.value = "";
當然其他值還是不允許的。
ps:記憶中以前是不行的,不知有沒有記錯。
對於opera,有一個變通的方法,利用它的type屬性:
file.type = "text"; file.type = "file";
通過修改type得到的file控制項,value會自動還原成空值,這樣就間接把file控制項清空了。
ps:利用這個方法可以間接得到檔案路徑,但由於變回去後值就清空了,所以沒什麼用。
而ie的表單控制項的type設定後是不允許修改的,不能用opera的辦法。
不過還是有以下方法解決:
1,建立一個form,把file插進入後reset,再移除: 複製代碼 代碼如下:with(file.parentNode.insertBefore(document.createElement('form'), file)){
appendChild(file); reset(); removeNode(false);
}
好處是使用原生的reset,穩定可靠,但效率低。
ps:removeNode只有ie和opera支援,如需相容可改用removeChild的方式。
2,利用outerHTML,重建一個file控制項:
file.outerHTML = file.outerHTML;
好處是高效,但由於是新建立的file控制項,之前關聯的東西都丟失了。
ps:ff支援不支援outerHTML。
3,利用cloneNode,複製一個file控制項:
file.parentNode.replaceChild(file.cloneNode(false), file);
跟上一個方法差不多,但效率更低。
4,利用select方法選中file控制項的文本域,再進行清空:
file.select(); document.selection.clear();
或
file.select(); document.selection.clear();
看來沒什麼問題,但file必須能被select(不能是隱藏狀態)。
ps:這兩個方法都只能在ie使用。
由於程式中file是需要關聯的,所以方法2和3都不能用。
方法4貌似也不錯,但有一個致命問題,在ie測試以下代碼: 複製代碼 代碼如下:<form><input id="test" name="file" type="file"></form>
<script>
document.getElementById("test").onchange = function(){
this.select(); document.selection.clear();
this.form.submit();
}
</script>
執行到submit會顯示“拒絕訪問”的錯誤,原因不清楚,不知是ie故意的還是bug。
看來也只能使用方法1了: 複製代碼 代碼如下:function ResetFile(file){
file.value = "";//ff chrome safari
if ( file.value ) {
if ( $$B.ie ) {//ie
with(file.parentNode.insertBefore(document.createElement('form'), file)){
appendChild(file); reset(); removeNode(false);
}
} else {//opera
file.type = "text"; file.type = "file";
}
}
}
ps:有更好方法的話記得告訴我啊。
這個函數並不夠通用,最好還是根據實際情況選擇需要的方法。
提示
【上傳檔案數】
在檔案上傳執行個體中,各個檔案是同時上傳的。
經測試,瀏覽器能同時上傳的檔案數如下:
ie 2
ff 8
opera 8
chrome 6
safari 6
由於ie最多同時只能傳2個,所以設定更多檔案也只能排隊,而不能達到同時上傳的效果的。
ps:只是目測結果,有錯請提出。
【傳遞參數】
上傳檔案執行個體中,可以傳遞對應的修改檔案名稱,在使用“一般上傳”多個檔案一起上傳時也能找到對應的檔案名稱。
因為表單控制項值傳遞到後台後,擷取資料的順序跟前台表單控制項的排列順序是一致的。
只要保證前台file控制項跟對應表單控制項的排列順序一致就能利用這個特性擷取對應的值了。
詳細參考後台代碼。
【回呼函數】
有兩個方法可以響應上傳完成回呼函數。
一種是後台上傳完成後,在iframe輸出並執行回呼函數或通過parent調用父視窗的回呼函數。
這種比較方便,但必須在iframe裡面執行處理,例如檔案屬性查看執行個體。
另一種是在iframe的onload中執行回呼函數。
好處是可以把所有處理放在父視窗,iframe可以不做任何處理或用來反饋資訊。
缺點是有相容性問題,而且會有載入後沒有觸發onload的情況(上面的iframe部分有說明)。
上傳檔案執行個體中就是在onFinish中處理在iframe中輸出的資料。
由於可能出現一些意外情況導致響應很久,甚至沒有響應,所以一定要設定timeout以防萬一。
【處理返回資料】
上面提到,可以在onFinish中處理在iframe中輸出的資料。
要從iframe的body中擷取資料,有以下幾個方法:
iframe.contentWindow.document.body.innerHTML
iframe.contentDocument.body.innerHTML
window.frames[iframename].document.body.innerHTML
其中前兩種差不多,後者比較簡便,但ie不支援contentDocument,可惜。
第三種是利用frames對象來擷取,注意這樣擷取的對象直接就是window對象。
由於程式能直接獲得iframe對象,所以用的是第一種方式。
不過有一個問題在iframe的部分也提過,就是返回錯誤資訊頁面的問題。
在上傳檔案執行個體中,在iframe中輸出的是json形式的檔案資訊資料。
在onFinish中是這樣處理的: 複製代碼 代碼如下:try{
var info = eval("(" + iframe.contentWindow.document.body.innerHTML + ")");
show("上傳完成");
}catch(e){
show("上傳失敗"); stop(); return;
}
只有返回正確的json格式資料才能正常運行,否則就拋出錯誤,間接地排除了404等錯誤資訊。
ps:有更好方法的話歡迎提出。
【銷毀程式】
程式中有不少dom操作,在不需要繼續使用的時候最好執行一次dispose方法來銷毀程式。
例如移除file之後,關閉視窗之前,提交表單之前,曆遍表單元素前等等。
既可以節省資源,防止dom的記憶體流失,又能避免表單嵌套時的衝突問題。
【可用性】
看過“ppk談javascript”後,更加註重了可用性。
上傳執行個體在瀏覽器不支援js的情況下也能正常上傳,各位可以自行測試。
【編碼】
上一個無重新整理上傳系統,很多人反映上傳後檔案名稱亂碼,後來發現是編碼的問題。
當有中文資訊傳遞時,要注意前背景編碼必須統一,包括charset,檔案編碼,web.config的配置等。
【asp版本】
asp版本跟.net版本功能是一樣的,使用無組件上傳類。
不過上傳類本身有一個缺陷導致提交同名file控制項的話會出錯,經過修改後現在可以正常使用了。
使用說明
執行個體化時,第一個必要參數是file控制項對象:
new QuickUpload(file);
第二個選擇性參數用來設定系統的預設屬性,包括
屬性: 預設值//說明
parameter: {},//參數對象
action: "",//設定action
timeout: 0,//設定逾時(秒為單位)
onReady: function(){},//上傳準備時執行
onFinish: function(){},//上傳完成時執行
onStop: function(){},//上傳停止時執行
onTimeout: function(){}//上傳逾時時執行
還提供了以下方法:
upload:執行上傳操作;
stop:停止上傳操作;
dispose:銷毀程式。
程式源碼 複製代碼 代碼如下:
var QuickUpload = function(file, options) {
this.file = $$(file);
this._sending = false;//是否正在上傳
this._timer = null;//定時器
this._iframe = null;//iframe對象
this._form = null;//form對象
this._inputs = {};//input對象
this._fFINISH = null;//完成執行函數
$$.extend(this, this._setOptions(options));
};
QuickUpload._counter = 1;
QuickUpload.prototype = {
//設定預設屬性
_setOptions: function(options) {
this.options = {//預設值
action: "",//設定action
timeout: 0,//設定逾時(秒為單位)
parameter: {},//參數對象
onReady: function(){},//上傳準備時執行
onFinish: function(){},//上傳完成時執行
onStop: function(){},//上傳停止時執行
onTimeout: function(){}//上傳逾時時執行
};
return $$.extend(this.options, options || {});
},
//上傳檔案
upload: function() {
//停止上一次上傳
this.stop();
//沒有檔案返回
if ( !this.file || !this.file.value ) return;
//可能在onReady中修改相關屬性所以放前面
this.onReady();
//設定iframe,form和表單控制項
this._setIframe();
this._setForm();
this._setInput();
//設定逾時
if ( this.timeout > 0 ) {
this._timer = setTimeout( $$F.bind(this._timeout, this), this.timeout * 1000 );
}
//開始上傳
this._form.submit();
this._sending = true;
},
//設定iframe
_setIframe: function() {
if ( !this._iframe ) {
//建立iframe
var iframename = "QUICKUPLOAD_" + QuickUpload._counter++,
iframe = document.createElement( $$B.ie ? "<iframe name=\"" + iframename + "\">" : "iframe");
iframe.name = iframename;
iframe.style.display = "none";
//記錄完成程式方便移除
var finish = this._fFINISH = $$F.bind(this._finish, this);
//iframe載入完後執行完成程式
if ( $$B.ie ) {
iframe.attachEvent( "onload", finish );
} else {
iframe.onload = $$B.opera ? function(){ this.onload = finish; } : finish;
}
//插入body
var body = document.body; body.insertBefore( iframe, body.childNodes[0] );
this._iframe = iframe;
}
},
//設定form
_setForm: function() {
if ( !this._form ) {
var form = document.createElement('form'), file = this.file;
//設定屬性
$$.extend(form, {
target: this._iframe.name, method: "post", encoding: "multipart/form-data"
});
//設定樣式
$$D.setStyle(form, {
padding: 0, margin: 0, border: 0,
backgroundColor: "transparent", display: "inline"
});
//提交前去掉form
file.form && $$E.addEvent(file.form, "submit", $$F.bind(this.dispose, this));
//插入form
file.parentNode.insertBefore(form, file).appendChild(file);
this._form = form;
}
//action可能會修改
this._form.action = this.action;
},
//設定input
_setInput: function() {
var form = this._form, oldInputs = this._inputs, newInputs = {}, name;
//設定input
for ( name in this.parameter ) {
var input = form[name];
if ( !input ) {
//如果沒有對應input建立一個
input = document.createElement("input");
input.name = name; input.type = "hidden";
form.appendChild(input);
}
input.value = this.parameter[name];
//記錄當前input
newInputs[name] = input;
//刪除已有記錄
delete oldInputs[name];
}
//移除無用input
for ( name in oldInputs ) { form.removeChild( oldInputs[name] ); }
//儲存當前input
this._inputs = newInputs;
},
//停止上傳
stop: function() {
if ( this._sending ) {
this._sending = false;
clearTimeout(this._timer);
//重設iframe
if ( $$B.opera ) {//opera通過設定src會有問題
this._removeIframe();
} else {
this._iframe.src = "";
}
this.onStop();
}
},
//銷毀程式
dispose: function() {
this._sending = false;
clearTimeout(this._timer);
//清除iframe
if ( $$B.firefox ) {
setTimeout($$F.bind(this._removeIframe, this), 0);
} else {
this._removeIframe();
}
//清除form
this._removeForm();
//清除dom關聯
this._inputs = this._fFINISH = this.file = null;
},
//清除iframe
_removeIframe: function() {
if ( this._iframe ) {
var iframe = this._iframe;
$$B.ie ? iframe.detachEvent( "onload", this._fFINISH ) : ( iframe.onload = null );
document.body.removeChild(iframe); this._iframe = null;
}
},
//清除form
_removeForm: function() {
if ( this._form ) {
var form = this._form, parent = form.parentNode;
if ( parent ) {
parent.insertBefore(this.file, form); parent.removeChild(form);
}
this._form = this._inputs = null;
}
},
//逾時函數
_timeout: function() {
if ( this._sending ) { this._sending = false; this.stop(); this.onTimeout(); }
},
//完成函數
_finish: function() {
if ( this._sending ) { this._sending = false; this.onFinish(this._iframe); }
}
}
完整執行個體下載
完整執行個體下載(asp版本)
相關應用:JavaScript 圖片上傳預覽效果
轉載請註明出處:http://www.cnblogs.com/cloudgamer/