阅读:        作者:木的树

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。

 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目录下。

 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自动为我们设置请求头部。

  兼容性问题

 
二、使用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

<!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
 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类型会把它当成文件下载下来。这里和这里

 

  不知大家有没有注意到,上面的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”错误。

  

 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

Tags: 无刷新   文件上传   文件   刷新   上传