转载

Javascrpt无刷新文件上传

最近工作中遇到上传文件问题,主要需求是一步点击上传,兼容ie8+,当时用的dojox/form/uploader控件,这两天扒了一下源码,明白了原理拿出来分享一下。

总体思路如下:

1、对于支持XMLHttpRequest2的浏览器使用FormData通过ajax上传

2、对于ie10一下的浏览器使用iframe异步上传,还需后台服务器做相应处理,这部分也是dojo/request/iframe上传文件的原理。

一、使用FormData上传文件

FormData最频繁使用的功能就是表单序列化及创建与表单格式相同的数据。append方法接收两个参数,字段名与字段值,字段值可以是 File 、 Blob 、String.

1 var data = new FormData(form); 2 data.append("name", "woodtree"); 3 data.append(file.name, file); 4 data.append(name, Blob);

如果直接向FormData的构造函数中传入表单元素,可以将表单元素的数据预先填入。

1 new FormData(document.forms[0])

FormData的另一个便利之处就是不用明确指定Content-Type头部,xhr对象能够根据FormData实例自动配置适当的头部。下面是一个简单的上传文件demo。

Javascrpt无刷新文件上传
 1 <!doctype html>  2 <html>  3   <head>  4     <meta charset="utf-8">  5     <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">  6     <title>FormData</title>  7   </head>  8   <body>  9       <form id="uploader" action="/upload" enctype="multipart/form-data"> 10           <input id="app" type="file" multiple> 11           <input type="submit" value="Submit"> 12       </form> 13       <script> 14         var form = document.getElementById('uploader'); 15         var app = document.getElementById('app'); 16         form.addEventListener('submit', function(evt) { 17             evt.preventDefault();//组织页面刷新 18             var data = new FormData(); 19             for (var i = 0, len = app.files.length; i < len; i++) { 20                 //file property: name, size, type, lastModifiedDate 21                 var file = app.files[i]; 22                 data.append(file.name, file); 23             } 24  25             var xhr = new XMLHttpRequest(); 26             xhr.onload = function() { 27                 alert(JSON.parse(xhr.responseText).success); 28             }; 29             xhr.onerror = function(err) { 30                 console.error(err); 31             }; 32             xhr.open('post', './upload', true); 33             xhr.send(data); 34         }, false); 35     </script> 36   </body> 37 </html>
View Code

server端代码使用formidable模块将文件暂存在tmp目录下。

Javascrpt无刷新文件上传
 1 var http = require('http');  2 var url = require('url');  3 var fs = require('fs');  4 var qs = require('querystring');  5 var request = require('request');  6 var formidable = require('formidable');  7   8 http.createServer(function(req, res){  9     var _url = url.parse(req.url); 10     if (_url.pathname === '/index') { 11         fs.readFile('./index.html', function(err, data) { 12           res.writeHead(200, {"Content-Type": "text/html; charset=UTF-8"}); 13             res.write(data); 14             res.end(); 15         }); 16     } else if (_url.pathname === '/upload') { 17         console.log(req.headers['content-type']); 18         handle(req, res); 19     } 20 }).listen(8888); 21 var handle = function(req, res) { 22     if (req.headers['content-type'].indexOf('multipart/form-data') >= 0) { 23         var formStream = new formidable.IncomingForm(); 24         formStream.uploadDir = './tmp'; 25         formStream.parse(req, function(err, fields, files) { 26             res.writeHead(200, {"Content-Type": "application/json"}); 27             if (err) { 28                 res.write('{"success": false}'); 29             } else { 30                 res.write('{"success": true}'); 31             } 32             res.end(); 33         }); 34     } 35 }
View Code

查看请求,xhr自动为我们设置请求头部。

Javascrpt无刷新文件上传

兼容性问题

Javascrpt无刷新文件上传

二、使用iframe上传文件

兼容旧版本的ie浏览器实现无刷新上传,只能借由iframe来实现,大多数类库的做法是动态插入一个iframe元素,将form元素的target属性设置为新添加的iframe,这样只刷新了iframe的内容而避免页面跳转到form元素的action属性所指定的url。这里我们根据dojo/request/iframe模块的原理来实现上传文件。

该模块需要后台返回响应的格式来配合。将需要返回的信息放在`textarea`标签内。然后绑定iframe的load事件,通过`doc.getElementsByTagName('textarea')`取得textarea中的数据。

1 <html> 2   <body> 3     <textarea> 4       uploadInfo 5     </textarea> 6   </body> 7 </html>

下面是简单的demo

Javascrpt无刷新文件上传
<!DOCTYPE HTML> <html>  <head>   <meta http-equiv="Content-Type" content="text/html; charset=utf-8">   <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />  <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />   <title>ArcGIS Web Application</title>  </head>  <body class="claro">   <form id="uploader" method="post" action="/upload" target="appFrame" encoding="multipart/form-data" enctype="multipart/form-data">    <input id="appInput" name="app" type="file" >   </form>   <iframe id="frame" name="appFrame" src="" style="visibility:hidden;"></iframe>   <script type="text/javascript">    var upload = document.getElementById('placeholder');    var uploader = document.getElementById('uploader');    var app = document.getElementsByName('app')[0];    var clickLietener = function() {     app.click();    }    var changeListener = function() {     uploader.submit();    }    if (app.addEventListener) {     app.addEventListener('change', changeListener, false);    } else if (app.attachEvent) {     app.attachEvent('onchange', changeListener);    }    var appFrame = document.getElementById('frame');    var listener = function() {     var doc = appFrame.contentWindow.document;     var textAreas = doc.getElementsByTagName('textarea');     if (textAreas && textAreas.length > 0) {      var response = textAreas[0].value;      alert(response);     }    }    if (appFrame.addEventListener) {     appFrame.addEventListener('load', function(evt) {      listener();     }, false);    } else if(appFrame.attachEvent) {     appFrame.attachEvent('onload', function() {      listener();     });    }   </script>  </body> </html> 
View Code
Javascrpt无刷新文件上传
 1 var http = require('http');  2 var url = require('url');  3 var fs = require('fs');  4 var qs = require('querystring');  5 var formidable = require('formidable');  6   7 http.createServer(function(req, res) {  8   var _url = url.parse(req.url);  9   if (_url.pathname === '/index') { 10     fs.readFile('./index.html', function(err, data) { 11       res.writeHead(200, { 12         "Content-Type": "text/html; charset=UTF-8" 13       }); 14       res.write(data); 15       res.end(); 16     }); 17   } else if (_url.pathname === '/upload') { 18     var formStream = new formidable.IncomingForm(); 19     formStream.uploadDir = './tmp'; 20     formStream.parse(req, function(err, fields, files) { 21       console.log(fields); 22       console.log(files); 23       var info = null; 24       var accept = req.headers.accept; 25       if (err) { 26         info = {success: false}; 27       } else { 28         info = {success: true}; 29       } 30       if (accept.indexOf('application/json') > -1) { 31         res.writeHead(200, { 32           "Content-Type": "application/json;charset=utf-8" 33         }); 34         res.write(JSON.stringify(info)); 35       } else { 36         res.writeHead(200, { 37           "Content-Type": "text/html; charset=UTF-8" 38         }); 39         var responseText = '<html><body><textarea>' + 40           JSON.stringify(info) + 41           '</textarea></body></html>'; 42         res.write(responseText); 43       } 44       res.end(); 45     }); 46   } 47 }).listen(8888);
View Code

后台代码需要注意Content-Type响应头的设置,ie8、9碰到不知如何渲染的MIME类型会把它当成文件下载下来。 这里 和 这里

Javascrpt无刷新文件上传

Javascrpt无刷新文件上传

Javascrpt无刷新文件上传

不知大家有没有注意到,上面的demo是一步上传,选择好文件后直接上传到服务器,ie8以上的浏览器没问题,如果是在ie8中情况就有些棘手。ie中文件上传控件长成这个样子,单击一下button会弹出文件选择框,如果单击的是text部分,没有反映,你需要双击才会弹出选择框。一个办法是让鼠标尽量单击button部分,button的大小跟font-size有关。但如果你的可点击区域太大。。。。。

所幸还是有解决办法的,这时需要在form中加一个label标签,for属性指向file。这样点击label时会触发for指向元素的click事件,这时label的自然行为。同时把file移除屏幕外。注意一定不能用input[type=button],在点击button时候调用file的click事件,然后在file change事件中调用form.submit方法, 这种行为在ie中是被禁止的,回报“access denied”错误。

Javascrpt无刷新文件上传

Javascrpt无刷新文件上传
 1 <!DOCTYPE HTML>  2 <html>  3     <head>  4         <meta http-equiv="Content-Type" content="text/html; charset=utf-8">  5         <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />  6     <meta http-equiv="X-UA-Compatible" content="IE=EDGE" />  7         <title>ArcGIS Web Application</title>  8     </head>  9     <body class="claro"> 10         <form id="uploader" method="post" action="/upload" target="appFrame" encoding="multipart/form-data" enctype="multipart/form-data"> 11             <label id="placeholder" for="appInput">upload</label> 12             <input id="appInput" name="app" type="file" style="position:absolute;left:-800px;"> 13         </form> 14         <iframe id="frame" name="appFrame" src="" style="visibility:hidden;"></iframe> 15         <script type="text/javascript"> 16             var upload = document.getElementById('placeholder'); 17             var uploader = document.getElementById('uploader'); 18             var app = document.getElementsByName('app')[0]; 19             var changeListener = function() { 20                 uploader.submit(); 21             } 22             if (app.addEventListener) { 23                 app.addEventListener('change', changeListener, false); 24             } else if (app.attachEvent) { 25                 app.attachEvent('onchange', changeListener); 26             } 27             var appFrame = document.getElementById('frame'); 28             var listener = function() { 29                 var doc = appFrame.contentWindow.document; 30                 var textAreas = doc.getElementsByTagName('textarea'); 31                 if (textAreas && textAreas.length > 0) { 32                     var response = textAreas[0].value; 33                     alert(response); 34                 } 35             } 36             if (appFrame.addEventListener) { 37                 appFrame.addEventListener('load', function(evt) { 38                     listener(); 39                 }, false); 40             } else if(appFrame.attachEvent) { 41                 appFrame.attachEvent('onload', function() { 42                     listener(); 43                 }); 44             } 45              46         </script> 47     </body> 48 </html>
View Code

参考资料

文件上传的渐进式增强 - 阮一峰的网络日志

Uploading Files with AJAX

ie javascript form submit with file input

正文到此结束
Loading...