跨域是日常开发中经常开发中经常会接触到的一个重难点知识,何不总结实践一番,从此心中对之了无牵挂。
同源策略
之所以会出现跨域解决方案,是因为同源策略的限制。同源策略规定了如果两个 url 的协议、域名、端口中有任何一个不等,就认定它们跨源了。比如下列表格列出和 http://127.0.0.1:3000
比较的同源检测的结果,
url | 结果 | 原因 |
---|---|---|
http://127.0.0.1:3000/index | 同源 | |
https://127.0.0.1:3000 | 跨源 | 协议不同 |
https://localhost:3000 | 跨源 | 域名不同 |
http://127.0.0.1:3001 | 跨源 | 端口不同 |
那跨源有什么后果呢?归纳有三:不能获取 Cookie、LocalStorage、IndexedDB;不能获取 dom 节点;不能进行一般的 Ajax 通信;跨域解决方案的出现就是为了解决以上痛处。
JSONP 跨域
提到 JSONP 跨域,不得不先提到 script
标签,和 img
、iframe
标签类似,这些标签是不受同源策略限制的,JSONP 的核心就是通过动态加载 script 标签来完成对目标 url 的请求。
先来看一段 JSONP 调用的 Headers
部分,字段如下:
1 | Request URL:http://127.0.0.1:3000/?callback=handleResponse |
可以很鲜明地发现在 Request URL
中有一句 ?callback=handleResponse
,这个 callback 后面跟着的 handleResponse 即回调函数名(可以任意取),服务端会接收到这个参数然后拼接成形如 handleResponse(JSON)
的形式返还给前端(这也是 JSONP == JSON with padding 的原因吧),如下图,这时候浏览器就会自动调用我们事先定义好的 handleResponse 函数。
前端代码示例:(源为 http://127.0.0.1:3001)
1 | function handleResponse(res) { |
服务端代码示例:(源为 http://127.0.0.1:3000)
1 | const server = http.createServer((req, res) => { |
可以看出 JSONP 具有直接访问响应文本的优点,但是要想确认 JSONP 是否请求失败并不容易,因为 script 标签的 onerror 事件还未得到浏览器广泛的支持,此外它仅能支持 GET 方式调用。
CORS 跨域
CORS(Cross-Origin Resource Sharing) 可以理解为加强版的 Ajax,也是目前主流的跨域解决方案。它的核心思想即前端与后端进行 Ajax 通信时,通过自定义 HTTP 头部设置从而决定请求或响应是否生效
。
比如前端代码(url 为 http://127.0.0.1:3001)写了段 Ajax,代码如下:
1 | const xhr = new XMLHttpRequest() |
因为端口不一致的关系这时候导致不同源了,这时候会在 Request Headers 中发现多了这么一行字段,
1 | Origin: http://127.0.0.1:3001 |
而且控制台中会报出如下错误:
1 | Failed to load http://127.0.0.1:3000/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:3001' is therefore not allowed access. |
这时候就需要在服务端设置字段 Access-Control-Allow-Origin
,它的作用就是设置允许来自什么源的请求,如果值设置为 *
,表明允许来自任意源的请求。服务端代码示例如下:
1 | http.createServer((req, res) => { |
CORS 分为简单请求以及非简单请求。可以这么区分,如果请求方法为 POST
、GET
、HEAD
时为简单请求,其它方法如 PUT
、DELETE
等为非简单请求,如果是非简单请求的话,可以在 chrome 的 Network 中看到多了一次 Request Method
为 OPTIONS
的请求。如下图:
可以把这个请求称为预请求,用白话文翻译下,浏览器询问服务器,’服务器大哥,我这次要进行 PUT 请求,你给我发张通行证呗’,服务器大哥见浏览器小弟这么殷勤,于是给了它发了张通行证,叫作 Access-Control-Allow-Methods:PUT
,接着浏览器就能愉快地进行 PUT 请求了。服务端代码示例如下:
1 | http.createServer((req, res) => { |
聊完简单请求和非简单请求的区别后,再来看看如何利用 CORS 实现 Cookie 的跨域传送,首先在服务器随意设置个 Cookie 值下发到浏览器,如果非跨域的情况下,浏览器再次请求服务器时就会带上服务器给的 Cookie,但是跨域的时候怎么办呢?不卖关子了,需在服务端设置 Access-Control-Allow-Credentials
字段以及在客户端设置 withCredentials
字段,两者缺一不可,代码如下:
前端代码示例:(源为 http://127.0.0.1:3001)
1 | const xhr = new XMLHttpRequest() |
服务端代码示例: (源为 http://127.0.0.1:3000)
1 | const server = http.createServer((req, res) => { |
至此介绍了几个比较关键 HTTP 头在 CORS 中的实践运用,更为详细的资料可以参阅 Cross-Origin Resource Sharing,最后概括下 CORS 的优缺点,优点是支持所有类型的 HTTP 方法,缺点是有些老的浏览器不支持 CORS。
hash + iframe
在文章最开始提到过 iframe 标签也是不受同源策略限制的标签之一,hash + iframe 的跨域核心思想就是,在 A 源中通过动态改变 iframe 标签的 src 的哈希值,在 B 源中通过 window.onhashchange
来捕获到相应的哈希值。思路不难直接上代码:
A 页面代码示例(源为 http://127.0.0.1:3000)
1 | <body> |
B 页面代码示例(源为 http://127.0.0.1:3001)
1 | window.onhashchange = function() { // ① 关键语句 |
刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。
1 | 来自 page2 的代码 #{"data":"hash"} |
这种方式进行跨域优点是支持页面和页面间的通信,缺点也是只支持 GET 方法和单向的跨域通信。
postMessage
为了实现跨文档传送(cross-document messaging),简称 XDM。HTML5 给出了一个 api —— postMessage,postMessage() 方法接收两个参数:发送消息
以及消息接收方所在域的字符串
。代码示例如下:
A 页面代码示例(源为 http://127.0.0.1:3000)
1 | <body> |
B 页面代码示例(源为 http://127.0.0.1:3001)
1 | window.addEventListener('message', function(event) { |
刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。
1 | From page1 a secret |
这种跨域方式优点是是支持页面和页面间的双向通信,缺点也是只能支持 GET 方法调用。
WebSockets
WebSockets 属于 HTML5 的协议,它的目的是在一个持久连接上建立全双工通信。由于 WebSockets 采用了自定义协议,所以优点是客户端和服务端发送数据量少,缺点是要额外的服务器。基础的使用方法如下:
1 | const ws = new WebSocket('ws://127.0.0.1:3000') |
当然一般我们会使用封装好 WebSockets 的第三方库 socket.io,这里具体就不展开了。
项目地址
前文所述五种跨域实践的 demo 已上传至 cross-domain,前端环境基于 create-react-app 搭建,后端环境用 node 搭建。
当然跨域方式还有一些其他方式的实现,后续酌情慢慢填坑~