Javascrpt无刷新文件上传

最近工作中遇到上传文件问题,主要需求是一步点击上传,兼容ie8+,当时用的dojox/form/uploader控件,这两天扒了一下源码,明白了原理拿出来分享一下。
总体思路如下:
1、对于支持XMLHttpRequest2的浏览器使用FormData通过ajax上传
2、对于ie10一下的浏览器使用iframe异步上传,还需后台服务器做相应处理,这部分也是dojo/request/iframe上传文件的原理。
 
一、使用FormData上传文件
  FormData最频繁使用的功能就是表单序列化及创建与表单格式相同的数据。append方法接收两个参数,字段名与字段值,字段值可以是FileBlob、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。

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1, maximum-scale=1,user-scalable=no">
    <title>FormData</title>
  </head>
  <body>
      <form id="uploader" action="/upload" enctype="multipart/form-data">
          <input id="app" type="file" multiple>
          <input type="submit" value="Submit">
      </form>
      <script>
        var form = document.getElementById('uploader');
        var app = document.getElementById('app');
        form.addEventListener('submit', function(evt) {
            evt.preventDefault();//组织页面刷新
            var data = new FormData();
            for (var i = 0, len = app.files.length; i < len; i++) {
                //file property: name, size, type, lastModifiedDate
                var file = app.files[i];
                data.append(file.name, file);
            }

            var xhr = new XMLHttpRequest();
            xhr.onload = function() {
                alert(JSON.parse(xhr.responseText).success);
            };
            xhr.onerror = function(err) {
                console.error(err);
            };
            xhr.open('post', './upload', true);
            xhr.send(data);
        }, false);
    </script>
  </body>
</html>

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

var http = require('http');
var url = require('url');
var fs = require('fs');
var qs = require('querystring');
var request = require('request');
var formidable = require('formidable');

http.createServer(function(req, res){
    var _url = url.parse(req.url);
    if (_url.pathname === '/index') {
        fs.readFile('./index.html', function(err, data) {
          res.writeHead(200, {"Content-Type": "text/html; charset=UTF-8"});
            res.write(data);
            res.end();
        });
    } else if (_url.pathname === '/upload') {
        console.log(req.headers['content-type']);
        handle(req, res);
    }
}).listen(8888);
var handle = function(req, res) {
    if (req.headers['content-type'].indexOf('multipart/form-data') >= 0) {
        var formStream = new formidable.IncomingForm();
        formStream.uploadDir = './tmp';
        formStream.parse(req, function(err, fields, files) {
            res.writeHead(200, {"Content-Type": "application/json"});
            if (err) {
                res.write('{"success": false}');
            } else {
                res.write('{"success": true}');
            }
            res.end();
        });
    }
}

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

Javascrpt无刷新文件上传

  兼容性问题

Javascrpt无刷新文件上传

 
二、使用iframe上传文件
  兼容旧版本的ie浏览器实现无刷新上传,只能借由iframe来实现,大多数类库的做法是动态插入一个iframe元素,将form元素的target属性设置为新添加的iframe,这样只刷新了iframe的内容而避免页面跳转到form元素的action属性所指定的url。这里我们根据dojo/request/iframe模块的原理来实现上传文件。
  该模块需要后台返回响应的格式来配合。将需要返回的信息放在`textarea`标签内。然后绑定iframe的load事件,通过`doc.getElementsByTagName('textarea')`取得textarea中的数据。

<html>
  <body>
    <textarea>
      uploadInfo
    </textarea>
  </body>
</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>

var http = require('http');
var url = require('url');
var fs = require('fs');
var qs = require('querystring');
var formidable = require('formidable');

http.createServer(function(req, res) {
  var _url = url.parse(req.url);
  if (_url.pathname === '/index') {
    fs.readFile('./index.html', function(err, data) {
      res.writeHead(200, {
        "Content-Type": "text/html; charset=UTF-8"
      });
      res.write(data);
      res.end();
    });
  } else if (_url.pathname === '/upload') {
    var formStream = new formidable.IncomingForm();
    formStream.uploadDir = './tmp';
    formStream.parse(req, function(err, fields, files) {
      console.log(fields);
      console.log(files);
      var info = null;
      var accept = req.headers.accept;
      if (err) {
        info = {success: false};
      } else {
        info = {success: true};
      }
      if (accept.indexOf('application/json') > -1) {
        res.writeHead(200, {
          "Content-Type": "application/json;charset=utf-8"
        });
        res.write(JSON.stringify(info));
      } else {
        res.writeHead(200, {
          "Content-Type": "text/html; charset=UTF-8"
        });
        var responseText = '<html><body><textarea>' +
          JSON.stringify(info) +
          '</textarea></body></html>';
        res.write(responseText);
      }
      res.end();
    });
  }
}).listen(8888);

  后台代码需要注意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无刷新文件上传


<!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">
            <label id="placeholder" for="appInput">upload</label>
            <input id="appInput" name="app" type="file" style="position:absolute;left:-800px;">
        </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 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>

参考资料

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

Uploading Files with AJAX

ie javascript form submit with file input




上一篇:重要通知 | WanaCrypt0r 2.0及Onion等勒索软件安全建议


下一篇:2013-C++第13周项目参考解答链接集