【Javascript】完美解决前端跨域之 easyXDM 的使用和解析
前端跨域问题在大型网站中是比较常见的问题。本文详细介绍了利用 easyXDM 解决前端跨域的原理细节和使用细节,具体使用时可以在文中代码实例的基础上扩展完成。
0、背景
因个别网络运营商存在 http
劫持的情况,导致网站某些重要的 iframe
弹窗页面被插入了第三方广告,内容完全被遮挡,严重验证影响用户体验。公司决定将这些页面切换为 https
,切换后发现原来 iframe
浮层自动适应大小的功能失效了,原因是主页面是http的,出现子窗口加载后操作父页面的浮层大小跨域了,被浏览器限制无法操作。于是就需要跨域解决方案来解决这种情况。
1、跨域问题
介绍一下什么是跨域问题?网站页面间发生数据请求和传输时,只要两个网址中的协议名 protocol
、主机 host
、端口号 port
三个中的任意一个不同,就构成了跨域。跨域的页面默认情况下不能通过 JavaScript
直接操作对方的页面对象。
解决方案 | 原理简介 | 优点 | 缺点 |
---|---|---|---|
JSONP | 通过 script 标签直接加载跨域的 JavaScript 代码进行执行,因为 script 标签引入的 JavaScript 代码是不受跨域限制的 |
简单 | 单向跨域 |
Flash URLLoader | 利用 Flash 的跨域策略来进行跨域数据加载 | - | 单向跨域 |
CORS | 在服务器上设置 Access-Control-Allow-Origin 允许跨域访问 |
配置后客户端不需要做任何修改 | 需要服务器配置和浏览器支持 |
window.name | 利用 iframe 的 window.name 做跨域通信 |
简单 | 仅支持文本消息 |
location.hash | 利用 iframe 的 location.hash 做跨域通信 |
简单 | 仅支持文本消息 |
document.domain | 将子域的 document.domain 修改为主域 |
简单 | 只适合于域与子域的跨域场景 |
postMessage | 利用 window.postMessage() 方法和 window 的 message 事件处理来做跨域通信 |
支持文本和对象消息 | 仅高级浏览器 Internet Explorer 8+ , Chrome ,Firefox , Opera 和 Safari 支持 |
各种跨域方案的详细介绍本文不做展开,有兴趣的同学可以参考《深入理解前端跨域方法和原理》( 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"> 蓝色区域为主页面内容输出区<br /> </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 库过程中如果遇到一些未知错误,可以通过加载调试库来做前端调试,步骤如下:
1. 从 easyXDM GitHub 库( https://github.com/oyvindkinsey/easyXDM )拉取完整分支
2. 将 src
文件复制到自己的代码目录下
3. 在引入 easyXDM 库的地方改为引入 easyXDM.debug.js
4. 之后就可以利用 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
还没有人抢沙发呢~