一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第18天,点击查看活动详情。
前情提要
我们都知道前端之所以会有跨域的情况是基于浏览器的同源策略。那我们又为什么要跨域呢?
实际上有些时候我们需要向已知的自己人的其他站请求资源。这个概念就像我们自己家跟邻居家的关系。门锁就是同源策略。门锁防住了一般的坏人,但同时也防住了来探访的友人,而这个时候就需要通过一些方式来进行跨域处理。
场景
- 前后端分离的开发模式,有一部分情况会将后端代码与前端代码放在不同的服务器上,此时前端请求后端就会遇到跨域而请求不到数据。
- 因某种业务需求,我们需要向其他站点请求数据。
- ...
跨域处理的方向
后端
- node中间层
- 首先我们要知道同源策略的原理是浏览器检测到请求的地址跟当前的地址不同源,那无论成功与否都不会接收返回的信息,进而更不会返回给用户。那如果在前端所在的环境创建一个服务,然后通过新创建的服务做接口转发,这样也就不存在跨域的情况了。
- 之所以选择node作为中间层进行转发是因为node更为轻量。
- nginx反向代理
- 原理是前端请求后端接口的时候,域名使用的与前端服务所在的域是同源的,然后在nginx上配置,当有请求过来的时候,就转发到真实的我们要去获取数据的网站。
- 原理就是骗过浏览器,不去触发浏览器的同源策略,让浏览器一直以为我们是从同一个域下的后端服务获取数据。
- 与node中间层都是一种方式骗过同源策略。
- websocket
- 原理:利用HTML5新增的websocket协议不受同源策略的影响来实现跨域。
- 方式:后端创建ws服务,前端通过ws模块与后端发起长链接,有需要跨域的内容都通过即时推送数据即可。
- 具体的代码实例后期会出详细的文章。
- cors:跨域资源共享 也就是通过后端设置响应头信息来实现跨域
- Access-Control-Allow-Origin:
* || 单个的域名- 允许所有域名的脚本访问该资源 / 只允许某个域名通过脚本访问该资源
- Access-Control-Expose-Headers:
<header-name>,<header-name>...- 允许跨域请求包含content-type头,即自定义请求头信息
- Access-Control-Max-Age:
0 || 1800- 用来指定本次预检请求的有效期,单位为秒。
- 因为同源策略,浏览器对于跨域请求时会先发出options请求,来检测是否允许跨域请求,如果允许才会发起第二次的真实请求,如果不允许则拦截第二次请求。
- Access-Contorl-Allow-Methods
- 允许跨域请求的方法:GET,POST,DELETE,OPTIONS,PUT...
- Access-Control-Allow-Credentials: true
- 允许客户端携带验证信息。
- Access-Control-Allow-Origin:
前端
- jsonp
- 原理:script标签请求没有跨域的限制。
- 方式:通过script标签的src属性发送带有callback参数的get请求。服务端接口根据相关处理逻辑把要返回的信息提传给callback函数中,返回给浏览器。浏览器解析执行,这样前端就能拿到callback返回的数据了。
- 缺点:只能发送get请求,接口返回的为字段字符串信息。并且需要后端接口配合。
- 补充:jsonp的底层不属于XMLHttpRequest请求
- 原生js的示例
<script> var script = document.createElement('script'); script.type = 'text/javascript'; // 设置跨域请求的目标地址加参数,以及必不可少的回调方法,不然得不到返回值。 script.src="http://xxx.com:xx/xx?params=xx&&callback=resCallback"; document.head.appendChild(script); function resCallback(res) { console.log(JSON.parse(res)) } // 使用完记得删掉动态新增的script标签 document.head.removeChild(script); // 初始化回调函数 resCallback = null </script>
- document.domain + iframe跨域
-
必要条件:主域名必须相同。比如很多域名是二级域名:www.xxx.com 和 yyy.xxx.com就是两个主域相同的域名。
-
原理:通过设置两个页面的document.domain为主域,比如document.domain=xxx.com。以此创建同域来实现资源共享
-
原生js代码示例:
// 主页面 <iframe id="iframe" src="http://www.xxx.com/child.html"</script> <script> document.domain = 'xxx.com' var msg = '来获取我吧' </script> // iframe页面 <script> document.domain = 'xxx.com' console.log(‘获取到父窗口的页面信息msg是’+ window.parent.msg) </script>
-
- location.hash + iframe
- 原理:主页面的域与iframe的域不同,主页面可以通过给iframe的src上添加#加一些想传递的信息。但是只能单线传递。此时,如果通过iframe中再加一个iframe,地址是与主页面同域的页面。这样就可以通过parent.parent访问主页面的所有对象。
- 示例
// 主页面 http://www.xxxx.com/a.html <iframe id=""iframe src="http://www.yyyy.com/b.html"></iframe> <script> var iframe = document.getElementByid('iframe') iframe.src = iframe.src + '#msg=我是主页面,现在与你进行单方面通信' </script> // 子页面 <iframe id="iframeb" src="http://www.xxx.com/c.html"></iframe> <script> window.onhashchange = function () { iframe.src = iframe.src + location.hash } </script> // 孙子页面 <script> window.onhashchange = function() { window.parent.parent.callback('我:'+location.hash+'转了一圈有又回来了。') } </script> - 由以上示例可以看出,通过location.hash可以实现主页面向子页面的单方面通信。如果子页面想向父页面通讯,则需要与父页面同域的孙子页面来进行代理转发。
- window.name + iframe
- 原理:window.name属性在不同的页面,甚至不同域名加载后依旧存在,并且可以支持非常长的name值,大小为2MB。
- postMessage
- 原理:postMessage是HTML5 XMLHttpRequest level2中规定的可以跨域操作的window属性之一。
- 场景:
- 页面和其他打开的新窗口的数据传递 (跨域+非跨域)
- 多窗口之间消息传递 (跨域+非跨域)
- 页面与嵌套的iframe消息传递 (跨域+非跨域)
- 用法:postMessage接受两个参数:
- data: 数据对象。受部分浏览器的兼容性影响,最好序列化成字符串再进行传递
- origin:
- 协议+ip或者域名+端口号:精准发送信息,指定可以接受消息的域
-
- :向所有窗口发送信息
- / :向本窗口内的页面发送消息
- 监听:
- 通过监听message事件,来获取通过postMessage发送过来的消息
总结
- 每种方法都有利弊与其局限性,没有最优,自己有最适合。
- 关于iframe的用法其实现在很大程度上已经不使用了。因为iframe表现出来的缺点越来越突出,除极个别场景,是不会使用到的。不过也不失为一种解决方案,可以先学着,万一用上了呢对不对。
- 前后端分离的开发模式,导致部分情况下会将页面和后端分成两个服务器,这势必会造成跨域的情况。这种情况下什么jsonp,document.domain+iframe, location.hash+iframe, window.name+iframe, postMessage统统不适用。只有websocket,cors, node中间层,nginx反向代理,后端设置请求白名单是比较合适的方式。其中nginx反向代理和后端设置请求白名单是最便捷的方式。
- 针对跨域问题,之前有看到很多后端同学发表看法说跨域问题是前端的问题就该前端自己解决。我个人认为针对不同的情况理应采取不同的方案,不应该这样一概而论。你们觉得呢家人们?