做前端的同学基本都会遇到跨域问题,本文主要是想总结一下目前前端跨域的解决方案。供以后参考。
在此之前大家有没有想过,前端为什么会发生跨域问题?
同源策略
之所以会发生跨域,是因为同源策略的限制。 下表给出了相对http://127.0.0.1:3000同源检测的示例:
| URL | 结果 | 原因 |
| http://127.0.0.1:3000/index.html | 同源 | |
| https://127.0.0.1:3000 | 跨源 | 协议不同 |
| http://127.0.0.2:3000 | 跨源 | 域名不同 |
| http://127.0.0.1:3001 | 跨源 | 端口不同 |
跨域有什么影响呢?主要以下三点:
- 存储空间不共享,一个源中的Javascript脚本不能对属于其它源的数据进行读写操作。如Cookie、LocalStorage、IndexedDB。
- 不能获取 dom 节点
- 不能进行 Ajax 通信
基于以上几点原因,所以出现了几种规避同源策略的解决方案。
document.domain
这种方案有个限制,即当主域名,协议和端口相同,只有子域名不同时才能使用。
例如:https://tieba.baidu.com下页面的脚本和http://news.baidu.com下页面的脚本可以设置他们的document.domain属性为baidu.com,从而让这两个站点下面的文档看起来像在同源下,然后就可以让每个文档读取另一个文档的属性。
所以这种方案实际应用的并不多。
JSONP
提到 JSONP 跨域,不得不先提到 script 标签,和 img、iframe 标签类似,这些标签是不受同源策略限制的,JSONP 的核心就是通过动态加载 script 标签来完成对目标 url 的请求。
前端代码示例:(源为 http://127.0.0.1:3001)
function handleResponse(res) {
console.log(res) // {text: "jsonp"}
}
const script = document.createElement('script')
script.src = 'http://127.0.0.1:3000?callback=handleResponse'
document.head.appendChild(script)
注意URL后面的?callback=handleResponse,这个 callback 后面跟着的 handleResponse 即回调函数名(可以任意取)。 当服务端接收到这个参数后会拼接成形如 handleResponse(JSON) 的形式返还给前端,故称之为JSONP (JSON with Padding)。 这时候浏览器就会自动调用我们事先定义好的 handleResponse 函数。
缺点:
- 它没有关于JSONP调用的错误处理,一旦回调函数调用失败,浏览器会以静默失败的方式处理。
- 它只支持GET请求,这是由于该技术本身的特性所决定的。
因此,对于一些需要对安全性有要求的跨域请求,JSONP的使用需要谨慎一点了。
CORS 跨域
CORS(Cross-Origin Resource Sharing) 可以理解为加强版的 Ajax。它的核心思想即前端与后端进行 Ajax 通信时,通过自定义 HTTP 头部设置从而决定请求或响应是否生效。
比如前端代码(url 为 http://127.0.0.1:3001)写了段 Ajax,代码如下:
$.ajax({
type: "GET",
url: "http://127.0.0.1:3000",
success: function(data) {
console.log("ajax success.", data);
},
error: function(error) {
console.log("ajax error.", error);
}
});
这个时候控制台中会报以下错误:
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,它的作用就是设置允许来自什么源的请求。 如果值设置为 *,表明允许来自任意源的请求。服务端代码示例如下:
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001') // 设置允许来自 http://127.0.0.1:3001 源的请求
})
简单请求以及非简单请求
CORS 分为简单请求以及非简单请求。可以这么区分,如果请求方法为 POST、GET、HEAD 时为简单请求,
其它方法如 PUT、DELETE 等为非简单请求,如果是非简单请求的话,
可以在 chrome 的 Network 中看到多了一次 Request Method 为 OPTIONS 的请求。如下图:

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。 只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。
服务端代码示例如下:
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001')
res.setHeader('Access-Control-Allow-Methods', 'http://127.0.0.1:3001')
})
相较于JSONP只支持GET请求,CORS支持所有类型的HTTP请求。所以CORS也是目前主流的跨域解决方案。
hash + iframe
在文章最开始提到过 iframe 标签也是不受同源策略限制的标签之一,hash + iframe 的跨域核心思想就是, 在 A 源中通过动态改变 iframe 标签的 src 的哈希值, 在 B 源中通过 window.onhashchange 来捕获到相应的哈希值。思路不难直接上代码:
A 页面代码示例(源为 http://127.0.0.1:3000)
<body>
<iframe src="http://127.0.0.1:3001"></iframe>
<script>
const iframe = document.getElementsByTagName('iframe')[0]
iframe.setAttribute('style', 'display: none')
const obj = {
data: 'hash'
}
iframe.src = iframe.src + '#' + JSON.stringify(obj) // ① 关键语句
</script>
</body>
B 页面代码示例(源为 http://127.0.0.1:3001)
window.onhashchange = function() { // ① 关键语句
console.log('来自 page2 的代码 ' + window.location.hash) // 来自 page2 的代码 #{"data":"hash"}
}
刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。
来自 page2 的代码 #{"data":"hash"}
这种方式进行跨域优点是支持页面和页面间的通信,缺点也是只支持 GET 方法和单向的跨域通信。
postMessage
为了实现跨文档传送(cross-document messaging),简称 XDM。 HTML5 给出了一个 api —— postMessage。 postMessage() 方法接收两个参数:发送消息以及消息接收方所在域的字符串。代码示例如下:
A 页面代码示例(源为 http://127.0.0.1:3000)
<body>
<iframe src="http://127.0.0.1:3001"></iframe>
<script>
const iframe = document.getElementsByTagName('iframe')[0]
iframe.setAttribute('style', 'display: none')
iframe.onload = function() { // 此处要等 iframe 加载完毕,后续代码才会生效
iframe.contentWindow.postMessage('a secret', 'http://127.0.0.1:3001')
}
</script>
</body>
B 页面代码示例(源为 http://127.0.0.1:3001)
window.addEventListener('message', function(event) {
console.log('From page1 ' + event.data)
console.log('From page1 ' + event.origin)
}, false)
刷新 A 页面,可以发现在控制台打印了如下字段,至此实现了跨域。
From page1 a secret
From page1 http://127.0.0.1:3000
这种跨域方式优点是是支持页面和页面间的双向通信,缺点也是只能支持 GET 方法调用。
WebSockets
WebSockets 属于 HTML5 的协议,它的目的是在一个持久连接上建立全双工通信。 由于 WebSockets 采用了自定义协议,所以优点是客户端和服务端发送数据量少,缺点是要额外的服务器。
基础的使用方法如下:
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = function() {
// 连接成功建立
}
ws.onmessage = function(event) {
// 处理数据
}
ws.onerror = function() {
// 发生错误时触发,连接中断
}
ws.onclose = function() {
// 连接关闭时触发
}
当然一般我们会使用封装好 WebSockets 的第三方库 socket.io,这里具体就不展开了。
nginx反向代理
前端示例代码(源为 http://127.0.0.1:3001):
$.ajax({
type: "GET",
url: "http://127.0.0.1:3000/v1/test",
success: function(res){..},
....
})
由于端口号不同,请求发出后肯定会发生跨域问题。
这时可以通过配置Nginx代理服务进行转发。找到 nginx.conf 文件,在编辑器中打开,配置如下:
http {
include mime.types;
default_type application/octet-stream;
server {
...
# 添加如下代码
# 捕获URL中含有"/api/ms"字符串的请求,拦截后进行转发处理
location /api/ms
{
rewrite /api/ms/(.*) /$1 break;
proxy_pass "http://127.0.0.1:3000";
}
}
}
前端部分对应的发送请求的代码如下:
$.ajax({
type: "GET",
url: "api/ms/v1/test",
success: function(res){..},
....
})
这种方式应用也比较广泛,但是需要额外起一个Nginx服务。