现有世面上成熟的上传组件一大把,其中著名的有 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:xiaoang666@163.com
*此父页面与上传的iframe跨域通信
*ie 只能捕获window第一次onload时的iframe onload事件
*故针对ie需要侦听iframe.onreadystatechange事件,当等于complate时代表加载完成
*同样现代浏览器又只能侦听onload事件,故使用onload事件去捕获
*此方案兼容性较好,可以考虑使用
*/
var iframeupload = document.getElementById('uploadframe');
if(!document.all){
iframeupload.onload=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:xiaoang666@163.com
*此父页面与上传的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.onload=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.onload=function(){window.parent.postMessage('.json_encode($data).',"*")}</script>';
*echo '<script>window.onload=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。 以后实践后,再做记录。