js 实现iframe + form异步文件提交方案分析

现有世面上成熟的上传组件一大把,其中著名的有 swfUpload组件, jquery的webupload。


自身业务中一开始也采用了swfupload组件,其优点如进度显示,异步回调,在上传的各过程可控。优点好多,可以参考文章 http://www.cnblogs.com/youring2/archive/2012/07/13/2590010.html 。


然而大家都会在业务中遇到各种迥异的产品需求,亦会在组件使用过程中遇到各种问题。 领导也一直在吐槽swfupload组件的种种劣势,趁着解决一个上传文件的bug的功夫,自写了一个支持异步上传文件的方案。

       

首先,文件上传不能像jquery的get,post那样直接传送数据。至于为什么,网上一大把的关于如何用js来获取input 类型为file的值的问题。在这简单说明一下:由于浏览器的安全限制,ie8及以上浏览器都不会显示文件的绝对路径了,也不能通过获取value的方法去获取file的值。即使你获取到了,也只不过是一个本机的绝对路径,对于你提交后服务端该如何处理毫无用处的, 服务端只会处理你提交的文件数据流,这下明白了吧。 

所以需要用到form表单的提交方式,将form 的 enctype="multipart/form-data"。表示数据将以二进制编码格式传送,只有此种方案才能用于 file 类型的值传送。


form表单的提交会导致location的跳转,就无法做到当前页面的交互处理了。所以我们使用iframe + form的方案实现异步提交方案,具体实现为将form表单的target指向一个src为空的iframe页面,form的action指向你真正要处理图片上传逻辑的地址。

     

<iframe width="0" height="0" frameborder="0" style="display:none" scrolling="no" name="uploadframe" id="uploadframe"></iframe>
<script id="tpl-add" type="text/template">
<div class="formBox">
    <form target="uploadframe" style="display:block" id="uploadform" method="post" enctype="multipart/form-data" action="/fe/uploadApk?asyncjs=1">
        <div id="cert-main" class="c-form c-form-dialog f-pzr" style="width:auto">
            <dl class="f-cb">
                <dt>渠道包名称:</dt>
                <dd><input id="appName" class="c-input required"  type="text" name="appname" minlength="10" maxlength="20" placeholder="长度应为2-20个字符" />
                    <label class="error" for="appName" generated="true" style="display: none;"></label>
                </dd>
            </dl>
            <dl class="f-cb">
                <dt>上传渠道包 </dt>
                <dd style="position:relative">
                    <div id="uploadDiv">
                        <input type="hidden" value="<%$Q%>" name="Q"/>
                        <input type="hidden" value="<%$T%>" name="T"/>
                        <a class="doupload btngray" href="javascript:;">选择文件<input type="file" name="uploadfile" class="uploadfile" value=""/></a>
                        <span id="apkLoding" class="apk-loading" style="display:none">文件上传中</span>
                    </div>
                    <div>
                        <input id="androidApk" class="upload apk ignore"  name="apk" type="hidden"/>
                        <input id="androidAppHash" name="apkhash" type="hidden"/>
                        <p style="width:250px;word-wrap:break-word;word-break:break-all"><span id="apkSuccess" class="apk-success" style="display:none"><span></span>上传成功!</span></p>
                        <label id="apkError" class="error" style="display: none;padding:0;">请上传APK文件</label>
                    </div>
                </dd>
            </dl>
            <dl class="f-cb">
                <dt> </dt>
                <dd>
                <a class="submit c-btn1 uploadsubmit"  href="javascript:void(0)">保存</a>
                <a class="cancel c-btn2" href="javascript:void(0)">取消</a>
                </dd>
            </dl>
        </div>
    </form>
</div>
</script>


由于前端表单校验使用的jquery validate组件,之前的dom结构造成了一个问题。那就是用于上传文件的form外层有一个用于提交一般数据的form, 所以当内层的form submit时,触发了 jquery validate组件的验证事件。导致了上传文件的form不能 submit,也导致了验证组件的报错。此坑点做记录,以备后续再遇。


前端js和服务端的处理方案有两种,一种是通过传统的window.属性进行父页面的赋值,此种方案兼容性较好ie6、7都支持完好。 另一种是采用html5的postmessage方法进行父窗口信息传送,ie7及以下版本,以及火狐等低版本浏览器不支持。


两种方案的web端js处理程序为:


/*
 *第一种通过子页面为父窗口window属性赋值方法
 *autor:[email protected]
 *此父页面与上传的iframe跨域通信
 *ie 只能捕获window第一次onload时的iframe onload事件
 *故针对ie需要侦听iframe.onreadystatechange事件,当等于complate时代表加载完成
 *同样现代浏览器又只能侦听onload事件,故使用onload事件去捕获
 *此方案兼容性较好,可以考虑使用
 */
var iframeupload = document.getElementById('uploadframe');
if(!document.all){
    iframeupload.οnlοad=function(e){
        try{
            var result = window.uploadresult;
            doupload(result);
        }catch(e){}
    }
}else{
    iframeupload.onreadystatechange=function(e){
        if (this.readyState && this.readyState != 'complete') return;
        else{
            try{
                var result = window.uploadresult;
                doupload(result);
            }catch(e){}
        }
    }
}


/*
 *第二种通过捕获子页面h5 postmessage到父页面的message
 *autor:[email protected]
 *此父页面与上传的iframe跨域通信
 *此方案只针对支持h5 postmessge的浏览器ie7及以下浏览器无法工作
 *需要通过原生window.addEventListener添加事件监听方法去监听message事件
 *因为jquery的on和window.on都无法捕获到
 *捕获的对象为固定的是data
 */
if(window.addEventListener){
    window.addEventListener('message', function(e){
       console.log(e.data);
       doupload(e.data);
    },false)
}else if(window.attachEvent){
    window.attachEvent('onmessage', function(e){
       doupload(e.data);
    })
}else{
    window.onmessage = function(e){
       doupload(e.data);
    }
}


        服务端php处理程序为:


public function actionUploadApk() {
    $from = $this->_request->getParam('asyncjs');
    if($from){
        $data = Array();
        $data['error'] = '0';
        $data['msg'] = ''; 
        $data['url'] = 'http://***.cn';
        
        /*
        *第一种通过子页面为父窗口window属性赋值方法
        *ie下不能使用onload和onreadystatechange来返回值,因为web端无法捕获
        *echo '<script>window.οnlοad=function(){window.top.imgurl="http://up1---512.apk"}</script>';
        *echo '<script>window.onreadystatechange=function(){window.top.imgurl="http://up1---01512.apk"}</script>';
        *ie下直接使用top.--- 或 parent.---指明传值
        *使用json_encode方法编码多此一举,web端还需要进行parseJSON处理
        *echo '<script>window.top.uploadresult = '.json_encode('{"code":200,"msg":"","data":{"app_url":"http://u----201512.apk","app_hash":""}}').'</script>';
        *所以最终的window属性方案为下面,直接赋值为对象
        */
        
        echo '<script>window.top.uploadresult = {"code":200,"msg":"","data":{"app_url":"http://u**01512.apk","app_hash":""}}</script>';
           
        
        /*
        *第二种使用h5的postmessage方法向父窗口传递信息
        *注意第一个参数一般为string类型,当然也可以为object
        *第二个参数为接收message的domain,设置为*的话适用于广播,出于安全考虑只限测试时候使用
        *使用jsonencode,web端可以直接解析json对象
        *postMessage(data,origin)方法接受两个参数
         *1.data:要传递的数据,html5规范中提到该参数可以是JavaScript的任意基本类型或可复制的对象,然而并不是所有浏览器都做到了这点儿,部分浏览器只能处理字符串参数,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化,在低版本IE中引用json2.js可以实现类似效果。
        *2.origin:字符串参数,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage()方法只会将message传递给指定窗口,当然如果愿意也可>以建参数设置为"*",这样可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。
        *echo '<script>window.οnlοad=function(){window.parent.postMessage('.json_encode($data).',"*")}</script>';
        *echo '<script>window.οnlοad=function(){window.parent.postMessage({"errno":0,"msg":"","data":{"app_url":"http://****201512.apk","app_hash":""}},"*")}</script>';
        *此方案需要对json对象进行json_encode处理,方便web端处理
        */
        
        echo '<script>window.parent.postMessage('.json_encode('{"code":200,"msg":"","data":{"app_url":"http://u**01512.apk","app_hash":""}}').',"http://xi**0.cn")</script>';
        
    }
}


iframe传值与浏览器兼容性的知识点较多,都记叙在了注释里。

除了上述两种方案,还想到了使用websocket,或localstorage。 以后实践后,再做记录。

     

      

猜你喜欢

转载自blog.csdn.net/xiaoxigua666/article/details/50538518
今日推荐