jquery.form.js中ajax文件上传的原理

jquery.form.js是比较常用的ajax提交插件,它集成了更多功能,更加方便的解决了ajax提交复杂表单和数据的需求。在日常的开发工作中,一般使用jQuery自带的ajax就可以基本满足需要,但是它在遇到文件的异步上传时,就显得比较疲软,尤其是要在老版本的IE浏览器上使用,xhr2.0就无法实现了。因此,jquery.form.js可以非常简单的完成这个工作。

于是我在工作之余,仔细的阅读了一下它的代码,下面来简单分享一下jquery.form.js中对ajax上传是怎么处理的。

原理

对于老版本的IE浏览器,由于不支持xhr2.0,因此,使用ajax提交文件的方式就需要依靠iframe来实现。大致思路可以理解为:

  • 建立一个看不见的iframe
  • 让要进行ajax提交的form表单的target指向这个iframe。
  • form.submit();

关于form表单,有一个target属性,它允许浏览器在提交表单后,让结果在哪里显示。你可以查看详细的说明w3school form target。我们将target设置为当前页面的一个iframe之后,当前页面就不会因为执行submit()导致页面跳转了。

所以,对于老版本的IE浏览器来说,实现异步ajax文件提交其实并不是真正的ajax,而是页面的同步提交,只是,使用iframe不让页面进行跳转。这样,我们再把iframe隐藏起来,用户就无法察觉了。

示例

首先我们做一个最简单的表单界面,它用了默认的表单提交页面,请看下面的代码:

<body>  
    <form id="form" action="do.php" method="post" enctype="multipart/form-data">
        <label for="username">用户名:</label>
        <input type="text" name="username">
        <label for="file">上传文件:</label>
        <input type="file" name="file">
    </form>
    <button id="sub">ajax submit</button>
</body>  

这是一个常规的可以上传文件的表单,长这个样子:

然后我们在ajax submit按钮上帮上一个最简单的提交事件:

$('#sub').on('click', function (){
    var $form = $('#form');
    $form.get(0).submit();
});

在我们点击这个提交按钮之后,它会进行常规的同步提交。例如:我用php简单的写了一个上传文件的程序并返回一个字符串

那么我们怎么把这个过程变成异步的呢?

我们只需要将提交的按钮的操作增加几行:

$('#sub').on('click', function (){
    var $iframe = $('<iframe name="test" src="about:blank" />'); //创建一个iframe
    var $form = $('#form');
    $iframe.appendTo('body'); //加到当前页面
    $form.attr('target', 'test'); //让当前的表单的target指向这个iframe
    $form.get(0).submit(); //提交
});

再试,我们会发现,当前页面没有发生跳转,而是将带有文件的form提交到了iframe中。 图中下面新出现的方框就是iframe,iframe中的文字就是后台返回的结果。所以,我们只需要将这个iframe隐藏起来,用户就看不到了,这样,一个类似ajax的文件提交就完成了。

jquery.form.js

jquery.form.js关于文件提交的部分,大致就是这样处理的,它的所有实现过程,都在fileUploadIframe()中定义。源码的第241行开始,说明了它支持的方式:

if (options.iframe !== false && (options.iframe || shouldUseFrame)) {  
    if (options.closeKeepAlive) {
        $.get(options.closeKeepAlive, function() {
            jqxhr = fileUploadIframe(a);
        });
    }
    else {
        jqxhr = fileUploadIframe(a);
    }
}
else if ((hasFileInputs || multipart) && fileAPI) {  
    jqxhr = fileUploadXhr(a);
}
else {  
    jqxhr = $.ajax(options);
}

可以看出对于支持xhr2的浏览器,都会直接使用xhr进行ajax提交,但是如果没有xhr2的支持,则必须要用iframe方式提交了。

576行开始片段如下:

if (!s.iframeTarget) {  
    // add iframe to doc and submit the form
    $io.appendTo('body');
}
if (io.attachEvent) {  
    io.attachEvent('onload', cb);
}
else {  
    io.addEventListener('load', cb, false);
}
setTimeout(checkState,15);

try {  
    form.submit();
} catch(err) {
    // just in case form has element with name/id of 'submit'
    var submitFn = document.createElement('form').submit;
    submitFn.apply(form);
}

在将iframe append到body中之后,由于是模仿ajax提交的表单,因此,我们需要获取到后台的响应状态,已经返回数据,这样才能让使用者感觉他们是真正的在用ajax,而不是submit。因此它在后面给该iframe绑定load事件,用于响应事件的响应,其中bc就是那个回调函数,回调函数会再回调自己构造的假的xhr的success、complete等ajax回调函数。

综上,为了能把思路整理的更清晰,我们就将我写的简单的例子改一下,用来演示它如何让用户用真正ajax的方式进行编程吧。

$('#sub').on('click', function (){
    var $iframe = $('<iframe name="test" src="about:blank" />');
    var $form = $('#form');
    $iframe.appendTo('body');
    $iframe.on('load', function (){
        var xhr = {}; //自己构造一个xhr对象
        //... 设置xhr的内容,让它和真正的ajax的jQxhr一样。
        var result = $(this).get(0).contentWindow.document.body.innerHTML; //获得到后台的返回
        if (typeof xhr.success === 'function'){
            xhr.success.call(xhr, result); //执行回调,让用户写的success回调函数生效
        }
    });
    $form.attr('target', 'test');
    $form.get(0).submit();
});