之前寫過一個仿163網盤無刷新多文件上傳系統,已經對無刷新上傳文件的原理做了詳細的分析。 兼容:ie6/7/8, firefox 3.5.5, opera 10.01, safari 4.0.3, chrome 3.0 12月2號是我女朋友的生日,祝她開心快樂每一天,一起創造幸福的未來,并感謝各位的祝福!
ps:由于需要后臺,要測試系統請下載實例測試。
【upload】 程序中最重要的方法就是upload了,調用它就可以進行無刷新上傳。 如果設置了timeout屬性的話,會自動設置計時器: if ( this.timeout > 0 ) {
this._timer = setTimeout( $$F.bind(this._timeout, this), this.timeout * 1000 ); } ps:經測試,小于0的延時時間,ie會取消執行,而其他瀏覽器會當成0執行。 程序有一個_sending屬性用來判斷上傳狀態。 最后提交表單就開始上傳了。
程序使用_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部分。 為了能在文件上傳完成后執行回調函數,會在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; }
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在appendChild之后就進行第一次加載,并且在設置src之前加載完畢,所以觸發了兩次。 那么opera, ff和ie可能是第一次加載太慢,第二次覆蓋了第一次的,所以只觸發了一次onload。 針對加載過快的問題,可以在onload的時候根據_sending確定之前是否上傳狀態來解決。 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>
雖然不知道原因,辦法還是有的,一個是appendChild前設一個src,還可以在第一次onload中重新設置onload,像程序那樣。 ff的onload還有一個問題,在出現ERROR_INTERNET_CONNECTION_RESET(文件大小超過服務器限制)之類的服務器錯誤時,即使加載完成也不會觸發onload,暫時找不到解決辦法。 iframe有一個缺陷是只能用onload判斷加載完成,但沒有辦法判斷是否加載成功。
程序使用_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" });
file.form && $$E.addEvent(file.form, "submit", $$F.bind(this.dispose, this));
dispose方法是用來銷毀程序的,包括移除form。 最后把form插入到dom: file.parentNode.insertBefore(form, file).appendChild(file);
先把form插入到file控件之前,然后把file插入到form,這樣就能保證file在原來的位置上了。
如果有其他參數要傳遞,程序會使用_setInput函數來創建傳遞數據的表單控件。 由于生成的form里面只有file控件,要傳遞其他參數只能用程序生成了。 首先根據自定義的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]; }
然后移除oldInputs關聯的控件: for ( name in oldInputs ) { form.removeChild( oldInputs[name] ); }
這樣就能移除上一次生成的無用的控件了。 最后重新記錄當前控件到_inputs方便下次使用。
如果想停止當前上傳操作,可以調用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>
當使用結束或其他原因要銷毀程序時,可以調用dispose方法。 dispose里面主要做的是移除iframe和form。 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>
if ( $$B.firefox ) {
setTimeout($$F.bind(this._removeIframe, this), 0); } else { this._removeIframe(); }
var form = this._form, parent = form.parentNode;
if ( parent ) { parent.insertBefore(this.file, form); parent.removeChild(form); } this._form = this._inputs = null;
在實例里,有一個用來重置file控件的ResetFile函數。 重置file控件一般的辦法是使所在的form執行reset,但問題是會把其他表單控件也重置了。 file.value = "";
當然其他值還是不允許的。 對于opera,有一個變通的方法,利用它的type屬性: file.type = "text"; file.type = "file";
通過修改type得到的file控件,value會自動還原成空值,這樣就間接把file控件清空了。 而ie的表單控件的type設置后是不允許修改的,不能用opera的辦法。 with(file.parentNode.insertBefore(document.createElement('form'), file)){
appendChild(file); reset(); removeNode(false); } 好處是使用原生的reset,穩定可靠,但效率低。 2,利用outerHTML,重建一個file控件: file.outerHTML = file.outerHTML;
好處是高效,但由于是新創建的file控件,之前關聯的東西都丟失了。 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(不能是隱藏狀態)。 由于程序中file是需要關聯的,所以方法2和3都不能用。 ![]() <form><input id="test" name="file" type="file"></form> <script> document.getElementById("test").onchange = function(){ this.select(); document.selection.clear(); this.form.submit(); } </script>
看來也只能使用方法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:有更好方法的話記得告訴我啊。 這個函數并不夠通用,最好還是根據實際情況選擇需要的方法。
【上傳文件數】 在文件上傳實例中,各個文件是同時上傳的。 【傳遞參數】 上傳文件實例中,可以傳遞對應的修改文件名,在使用“一般上傳”多個文件一起上傳時也能找到對應的文件名。 【回調函數】 有兩個方法可以響應上傳完成回調函數。 【處理返回數據】 上面提到,可以在onFinish中處理在iframe中輸出的數據。 try{
var info = eval("(" + iframe.contentWindow.document.body.innerHTML + ")"); show("上傳完成"); }catch(e){ show("上傳失敗"); stop(); return; } 只有返回正確的json格式數據才能正常運行,否則就拋出錯誤,間接地排除了404等錯誤信息。 【銷毀程序】 程序中有不少dom操作,在不需要繼續使用的時候最好執行一次dispose方法來銷毀程序。 【可用性】 看過“ppk談javascript”后,更加注重了可用性。 【編碼】 上一個無刷新上傳系統,很多人反映上傳后文件名亂碼,后來發現是編碼的問題。 【asp版本】 asp版本跟.net版本功能是一樣的,使用無組件上傳類。
實例化時,第一個必要參數是file控件對象: new QuickUpload(file);
還提供了以下方法:
![]() 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); } } } |
|