前端跨域问题在大型网站中是比较常见的问题。本文详细介绍了利用 easyXDM 解决前端跨域的原理细节和使用细节,具体使用时可以在文中代码实例的基础上扩展完成。
0、背景
因个别网络运营商存在 HTTP 劫持的情况,导致网站某些重要的 iframe 弹窗页面被插入了第三方广告,内容完全被遮挡,严重影响用户体验。公司决定将这些页面切换为 HTTPS,切换后发现原来 iframe 浮层自动适应大小的功能失效了,原因是主页面是 HTTP 的,子窗口加载后对父页面浮层大小的操作跨域了,被浏览器限制无法操作。于是就需要跨域解决方案来解决这种情况。
1、跨域问题
介绍一下什么是跨域问题?网站页面间发生数据请求和传输时,只要两个网址中的协议名 protocol、主机 host、端口号 port 三个中的任意一个不同,就构成了跨域。跨域的页面默认情况下不能通过 JavaScript 直接操作对方的页面对象。
各种跨域方案简单对比如下:
上述各种跨域方案本文不做展开,有兴趣的同学可以参考《深入理解前端跨域方法和原理》( http://blog.csdn.net/kongjiea/article/details/44201021 )。
这里着重推荐 easyXDM ,因为 easyXDM 集成了现有的多种跨域解决方案,而且很好地实现了跨浏览器兼容、多个跨域通信并行、跨域请求白名单、通信响应等功能,能完美地解决各种跨域使用的应用场景。
2、easyXDM 使用实例
父页面 index.html
核心代码:
<div id="container"></div>
<div id="output">
<p>蓝色区域为主页面内容输出区</p>
</div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
var rpc = new easyXDM.Rpc({
isHost: true,
remote: 'http://127.0.0.1/easyXDM/iframe.html',
hash: true,
protocol: '1',
container: document.getElementById('container'),
props: {
frameBorder: 0,
scrolling: 'no',
style: {width: '100%', height: '100px'}
}
},
{
local: {
echo: function (message) {
showMsg(message);
}
}
});
</script>
子页面 iframe.html
核心代码:
<p>实线框为子页面区域</p>
<button id="btn" value="">点击给主页面发数据</button>
<div id="output"></div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
window.rpc = new easyXDM.Rpc({
isHost: false,
//acl: '^(https?:\\/\\/)?([a-zA-Z0-9\\-]+\\.)*baixing.com(\\/.*)?$',
protocol: '1'
},
{
remote: {
echo: {}
}
});
document.getElementById('btn').onclick = function () {
rpc.echo('echo from iframe');
};
</script>
访问 http://localhost/easyXDM/index.html
,因为 index.html
和 iframe.html
两个页面的 host 不同,子页面操作主页面内容属于跨域访问。有了 easyXDM 作为通道,这个操作就可以正常进行了,效果如下图所示:
实际应用场景中,修改调用函数就可以让子页面对父页面做任何想做的事情了。
3、easyXDM 原理解析
3.1 原理说明
easyXDM 对不同的底层通信方案进行封装,比如上面实例中使用了 postMessage()
方案来实现跨域双向通信。
3.1.1 子页面发送数据给主页面
easyXDM 将方法调用操作进行打包后通过 postMessage()
发送给主页面,主页面的 message 处理函数收到数据后交由 easyXDM 进行解析后调起调用函数。代码调用和数据流如下图所示:
传递的数据说明:
-
defaultXXX
: 为通道标识符,页面不刷新的情况下,这个值不变 -
id
: 请求编号,自增,每发送一次请求加1 -
method
: 需要调用的方法名 -
params
: 调用方法的参数,以 JSON 格式表示 -
jsonrpc
: 表示 JSON-RPC 消息版本
3.1.2 主页面方法返回响应数据
easyXDM 同样会调用 postMessage
将方法响应发回给子页面,子页面的 message 处理函数收到数据后交由 easyXDM 进行解析,解析后执行对应的响应处理操作。代码调用和数据流如下图所示:
传递的数据说明:
-
defaultXXX
: 为通道标识符,页面不刷新的情况下,这个值不变;与子页面发送的数据一致 -
id
: 与调用方法时发送的 id 一致 -
result
: 方法响应,以 JSON 格式表示 -
jsonrpc
: 表示 JSON-RPC 消息版本
以下依次对主页面和子页面的代码做具体说明。
3.2 主页面调用代码解析
主页面调用 easyXDM.Rpc()
的时候会初始化通信组件,同时会创建 iframe 子页面;具体参数含义介绍如下:
-
isHost
: true,表示创建 iframe 子页面 -
remote
: 创建的 iframe 子页面的 url -
container
: 值为 DOM 对象,创建出来的 iframe 会被包含在 container 中 -
props
: 属性中指定的内容会被附加到 iframe 对象上 -
hash
: 为 true 代表通道相关的 xdm_e / xdm_c / xdm_p 参数会在网址 hash 中记录,为 false 时会变成 url 参数;一般情况下建议设为 true,因为把跨域相关的前端参数传递给后端并不是个很好的方式,但可以解决后面的表单提交后的通道保持问题;所以具体场景具体选择。
通过合理设置以上属性,就可以将原来写死在页面上的 iframe 改为通过 easyXDM.Rpc()
的方式进行加载,从而实现代码的灵活嵌入。
上文实例中父页面 RPC 初始化后的网页元素如下:
<div id="container">
<iframe
name="easyXDM_default5491_provider"
id="easyXDM_default5491_provider"
frameborder="0"
scrolling="no"
src="http://127.0.0.1/easyXDM/iframe.html#xdm_e=http%3A%2F%2Flocalhost&xdm_c=default5491&xdm_p=1"
style="width: 100%; height: 100%;">
</iframe>
</div>
其中 iframe 的 name 和 id 是自动生成的,作用是区分不同的 RPC 通道,也就意味着在一个页面上可以建立多个跨域调用的通道。中间的 xdm_e / xdm_c / xdm_p 参数是初始化后的通道参数。
另外 local 参数配置定义了子页面可以调用的函数方法名和方法实现,方法名、方法参数等都可以任意按需指定。
3.3 子页面调用代码解析
iframe 中的 RPC 参数的解析如下:
-
isHost
: false,代表这是客户端,不创建 iframe 页面 -
protocol
: 通信协议,数字,具体含义见以下通信协议说明部分,可选 -
acl
: 代码调用方的网址白名单,可选
与主页面的 local 参数相对应,子页面的 remote 配置定义了所有子页面需要调用到的主页面的方法名。只有在 remote 里定义了,在子页面上才能通过 RPC 实例调用到。
以上正确配置后,函数跨域调用就和本地调用效果一样了,具体中间的通信已经由 easyXDM 来搞定,如同文中的 rpc.echo()
已经可以直接调用到主页面定义的 echo
方法。
3.4 通信协议说明
关于通信协议,如在代码配置中未指定则会按以下规则依次匹配使用最前面符合的一个
-
4
: 当通信的两端属于同一域时,直接通信 -
1
: 当存在windows.postMessage
或document.postMessage
时(IE8+、Firefox 3+、Opera 9+、Chrome 2+、Safari 4+ 支持),使用postMessage
机制通信 -
6
: 配置中存在 swf 属性,并且支持window.ActiveXObject
时,通过配置的 swf 做通信 -
5
: Gecko( Firefox 1+ )浏览器时,使用window.frameElement
属性做通信 -
2
: 配置中存在 remoteHelper 时,通过配置的 remoteHelper 做通信 -
0
: 默认,所有浏览器都支持;以上规则都不符合时,使用 image 加载机制做通信
4、更多功能
4.1 增加请求响应处理
index.html
页面的 echo 函数增加 return 语句返回值:
<script>
new easyXDM.Rpc({
// ...
},
{
local: {
echo: function (message) {
document.getElementById('output').innerHTML += "<p>" + message + "</p>";
return {'msg': 'echo done from index'};
}
},
remote: {}
});
</script>
iframe.html
调用 RPC 方法时增加回调函数即可:
<script>
// ...
document.getElementById('btn').onclick = function () {
rpc.echo('echo from iframe', function (response) {
showMsg(response.msg);
}, function (errorObj) {
alert('error');
});
};
</script>
效果如下图所示:
4.2 主页面调用子页面方法
在 iframe.html
中 RPC 的 local 中注册访问自己页面内容的方法 pingIframe
:
window.rpc = new easyXDM.Rpc({
// ...
},
{
local: {
pingIframe: function (message) {
showMsg(message);
return {'msg': 'pong from iframe'}
}
},
remote: {
echo: {}
}
});
在 index.html
中 RPC 的 remote 中注册子页面的 pingIframe
方法声明,增加一下按钮调用事件:
<button id="btn" value="">点击给子页面发数据</button>
<script>
// ...
var rpc = new easyXDM.Rpc({
// ...
},
{
local: {
// ...
},
remote: {
pingIframe: {}
}
});
document.getElementById('btn').onclick = function () {
rpc.pingIframe('ping from index', function(response){
showMsg(response.msg);
}, function(errorObj){
alert('error');
});
};
</script>
效果如下图所示:
4.3 主页面与多个页面通信
要做多页面通信,只要重复一下类似的相关代码调用即可。本实例中,复制上面的 iframe.html
为 iframe2.html
并简单修改里面的文字做区分;同时修改 index.html
代码如下:
<div id="container"></div>
<button id="btn" value="">点击给子页面1发数据</button>
<button id="btn2" value="">点击给子页面2发数据</button>
<div id="output">
蓝色区域为主页面内容输出区
</div>
<script src="easyXDM.min.js"></script>
<script>
var showMsg = function (message) {
document.getElementById('output').innerHTML += "<p>" + message + "</p>";
};
var generateRpc = function (url) {
return new easyXDM.Rpc({
isHost: true,
remote: url,
hash: true,
protocol: '1',
container: document.getElementById('container'),
props: {
frameBorder: 0,
scrolling: 'no',
style: {width: '100%', height: '100px'}
}
},
{
local: {
echo: function (message) {
showMsg(message);
return {'msg': 'echo done from index'};
}
},
remote: {
pingIframe: {}
}
});
};
var bindRpc = function(rpc, btnId) {
document.getElementById(btnId).onclick = function () {
rpc.pingIframe('ping from index', function (response) {
showMsg(response.msg);
}, function (errorObj) {
alert('error');
});
};
};
var rpc1 = generateRpc('http://127.0.0.1/easyXDM/iframe.html');
bindRpc(rpc1, 'btn');
var rpc2 = generateRpc('http://127.0.0.1/easyXDM/iframe2.html');
bindRpc(rpc2, 'btn2');
</script>
效果如下图所示:
4.4 iframe 切换页面后保持 RPC 通信
在 hash
设置为 false
时不做额外处理的情况下,当提交子页面里的 form 或点击子页面里的超链接打开新页面后,会发现与父窗口的通信走不通了。究其原因,是因为切换页面后,通信通道相关的 xdm_e / xdm_c / xdm_p 参数丢掉了,导致无法保持通信。解决办法就是,在新打开的页面网址中将通道参数传递过去。为方便起见,引入 jQuery 库,代码如下:
/* 使用方法:
* 1. 将以下代码加入到子页面中
* 2. 在子页面的 form 或 a 标签中增加 easyxdm 类名,将 easyXDM 参数通过网址
* 传递给新页面以保持页面跳转后跨域通信能保持
*/
$(document).ready(function () {
$('form.easyxdm').each(function () {
var $form = $(this);
var action = $form.attr('action');
$form.attr('action', action + window.location.hash);
});
$('a.easyxdm').each(function () {
var $link = $(this);
var href = $link.attr('href');
$link.attr('href', href + window.location.hash);
});
});
5、easyXDM 库的调试
使用 easyXDM 库过程中如果遇到一些未知错误,可以通过加载调试库来做前端调试,步骤如下:
- 从 easyXDM GitHub 库 ( https://github.com/oyvindkinsey/easyXDM ) 拉取完整分支
- 将
src
目录复制到自己的代码目录下 - 在引入 easyXDM 库的地方改为引入
easyXDM.debug.js
- 之后就可以利用 Chrome 浏览器进行 JavaScript 调试了。具体调试方法本文不做展开,有兴趣的同学可以参考《前端 Chrome 浏览器调试总结》( http://www.jianshu.com/p/b25c5b88baf5 ) 的 “Sources 资源页面的断点调试” 部分。
本文完整代码下载:https://pan.baidu.com/s/1cpRlim
6、尾注
因为 easyXDM 库本身 README.md 已经很久没有维护更新,导致一些参数含义无法找到;文档对于原理实现未做讲解,笔者在使用过程遇到了不少问题,只能通过代码调试和阅读代码的方式深入了解其实现原理来解决。本文即是笔者使用 easyXDM 的一些总结,供各位看官参考。
7、参考文档:
- easyXDM官网 http://easyxdm.net
- easyXDM GitHub库 https://github.com/oyvindkinsey/easyXDM
作者:南智敏
简介:百姓网营收技术团队成员。本文仅为作者个人观点,不代表百姓网立场。
题图作者:Pic2.me
本文在 “百姓网技术团队” 微信公众号首发,扫码立即订阅: