最近在做一个前后端完全分离模式下,ueditor编辑器多图上传的优化,遇到一些似是而非的坑。
之前采用的跨域方案是用postMessage,大致先将图片上传事件发回接口所在的同域工程,再将得到的上传结果返回给静态域名下的编辑器,实现编辑器代码与后端工程完全分离。
这次场景有点不同,由于主体入口文件被后端工程直接引用,但ueditor的很多弹窗页面都是引用于静态域名下的页面(包括多图片上传模块),这造成了单图上传OK,多图上传模块跨域的情况。这次的思路是想通过CORS直接跨域访问接口,下面把主要问题记录如下:
1、跨域iframe之间的js代码相互调用
由于一般公司的主域都是相同的,可以通过设置domain为相同的主域来实现(两边都要设置)
// 弹窗跨域设置
document.domain = window.location.hostname.match(/(\w+.com)$/)[0]; // 正则获取到主域名
2、iframe页面的跨域访问拦截
在做demo测试时,用form表单实现跨域图片提交,无意中发现了一个后端安全拦截策略,代码如下:
// form所在的iframe的域名是:http://www.xx.com:8000
<form action="http://www.yy.com:9000/upload" enctype="multipart/form-data" method="post">
<!-- 如果接口支持多图,加上multiple="multiple" -->
<input type="file" name="imageFile">
<!-- 其它参数 -->
<input type="hidden" name="uploadType" value="p">
</form>
当执行提交时,控制台若显示'X-Frame-Options' to 'deny'之类的提示时,后研究发现iframe页面的请求被后台框架的iframe安全策略拦截,而原因是由于当前请求用iframe下form提交的,而后台框架正好做了DENY或默认(即是拒绝)的安全策略。
解决方案:
1、将form表单提交换成下文(3)中的ajax/formData的方法
2、修改后台安全配置,如java,需要修改spring安全配置文件:src/main/resources/spring-security.xml
policy有三个值:DENY(默认值), SAMEORIGIN(同域), ALLOW-FROM(允许指定值,chrome貌似支持性不好)
<frame-options policy="SAMEORIGIN" />
// 或下面这样设置,但实际测试感觉两者效果差不多
<frame-options policy="SAMEORIGIN" strategy="whitelist" value="http://www.xx.com:8000" />
当设置ALLOW-FROM时,由于chrome支持性不好,控制台会出现'ALLOW-FROM DENY' is not a recognized directive. The header will be ignored(无法识别指令,头信息被忽略),效果同上面的SAMEORIGIN。
注意的是:如果是ajax形式的提交,则不受上面任何策略的影响(包括DENY)。
3、CORS跨域后台设置
response.setHeader("Access-Control-Allow-Origin", "http://www.xx.com:8000");
response.setHeader("Access-Control-Allow-Credentials", "true");
一般情况下,设置上面的头后,cors跨域基本上能成功了。
需要注意的是,当设置了Credentials,上面的Origin值不能再为*了,需要指定一个具体的值。
4、OPTIONS请求preflight(预检)
在跨域时,若出现options类型的请求,大概情形分以下两种:
1.) 非简单跨域请求
http简单请求包括GET、POST、HEAD三种,像PUT、DELETE、OPTIONS就属于非简单请求,另外要注意的是,当POST请求时,Content-Type必须是text/plain、application/x-www-form-urlencoded、 multipart/form-data中的一个值,若为application/json时,则不属于简单请求,会出现options预检。
2.) 自定义HTTP头部
当前端在跨域时通过xhr.setRequestHeader自定义了一个头,在发起正式的请求前,会产生一个OPTIONS预检请求,该请求中会有两个字段:Access-Control-Request-Method和Access-Control-Request-Headers,分别描述了使用的方法和自定义了哪些头信息。
举个粟子,如下(前端使用了delete,自定义头Test和非简单请求application/json):
$.ajax({
...
type: 'DELETE', // 此处大小写都可以,但后台的Access-Control-Allow-Methods里一定要大写
beforeSend: function (xhr) {
xhr.setRequestHeader('Test', 'myValue'); // 添加了自定义头
},
contentType: 'application/json', // contentType内容非简单请求
发起options预检请求后,可以从Request Headers中可以看出使用的方法和自定义了哪些头:
Access-Control-Request-Headers: content-type,test
Access-Control-Request-Method: DELETE
则后端java添加相应的允许:
response.AddHeader("Access-Control-Allow-Origin": "*");
response.AddHeader("Access-Control-Allow-Methods", "DELETE"); // 此处值一定要大写,OPTIONS本身值不用加
response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Test"); // 此处值大小写无所谓
后端有没有设置成功或有没有进入方法里可以从options预检请求的Response Headers里看出。
Access-Control-Allow-Headers:content-type, test
Access-Control-Allow-Methods:DELETE
5、通过ajax/formData批量上传
// 通过FormData将文件转成二进制数据
var formData = new FormData();
formData.append('imageFile', file);
$.ajax({
// UE.Editor.prototype.apiDomain把java的接口域名挂在UE上读取了
url: UE.Editor.prototype.apiDomain + '/upload',
type: 'POST',
data: formData, // 上传formdata封装的数据
dataType: 'JSON',
cache: false, // 不缓存
processData: false, // jQuery不要去处理发送的数据
xhrFields: {
withCredentials: true // 前端设置是否带cookie
},
contentType: false, // jQuery不要去设置Content-Type请求头
success: function(res) {
console.log(res);
},
error(res) {
console.log(res);
}
});
注意:processData,contentType都需设置false,不然jquery会做处理;另外若接口不支持多文件上传,可定时循环发送图片。
6、图片预览临时URL的生成
实现图片预览有两种方法:
方法一:
FileReader,使用的base64方式实现
<input type="file" name="imageFile">
<script>
document.querySelector('input[type=file]').onchange = function(e) {
var file = e.target.files[0];
var fileReader = new FileReader();
fileReader.onload = function(e) {
var image = new Image();
image.src = e.target.result;
document.body.append(image);
}
fileReader.readAsDataURL(file);
}
</script>
方法二:
通过html5的window.URL.createObjectURL方法,使用的是blob的方式实现。
使用方法:window.URL.createObjectURL(blob || file);
兼容写法:window[window.webkitURL ? 'webkitURL' : 'URL'].createObjectURL.(blob || file);
<input type="file" name="uploadFile">
<script>
document.querySelector('input[type=file]').onchange = function(e) {
var file = e.target.files[0];
var url = window.URL.createObjectURL(file);
var image = new Image();
image.src = url;
document.body.append(image)
}
</script>
当图片临时url被替换成正式url时,记得销毁临时url,方法:
window.URL.revokeObjectURL(url);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。