筆者常常在前端開源羣答疑,加上以前的招聘面試經歷。發現許多新手前端在問起跨域問題的解決方案,一套一套的,但是實際遇到跨域問題了就不知道怎麼解決了。此次寫這篇文章從實踐角度聊一聊跨域問題。javascript
出於瀏覽器的同源策略限制,瀏覽器會拒絕跨域請求。 這就是跨域問題的產生緣由,同源策略是用於隔離潛在惡意文件的重要安全機制。css
這句話的三個關鍵字:html
那麼第一個問題來了,什麼算是同源。解決這個問題須要先了解一下URL的完整結構: 前端
API 接口地址 | 是否跨域 | 緣由 |
---|---|---|
www.domain.com/api/users/1 | 否 | 協議、主機、端口所有都相同 |
www.domain.com:80/api/users/1 | 是 | 端口不一樣 |
www.baidu.com/api/users/1 | 是 | 協議不一樣 |
api.domain.com/v1/users/1 | 是 | 主機不一樣 |
script
、img
、link
、video
等標籤加載資源的請求(HTTP GET請求)不作限制。具體的限制規則還有不少,這裏只說常見和本文用得上的。vue
那麼那些環境算是瀏覽器?java
那麼若是用戶發出的AJAX GET請求是一個跨域請求,那麼會在上圖中哪個階段被阻止? 可是第3階段,也就是說用戶發送的信息能夠到達服務端,服務器是可以接受處理並返回了。返回的瀏覽器發現這是一個跨域請求。就直接拒絕,同時把返回的信息替換爲報錯信息,返回給JavaScript程序。 對於更復雜的POST/PUT等請求, MDN CORS文檔裏面有更詳細的處理方法。這裏就不細說。react
這一點很重要,可是老是被新人忽略。因此重要的事情說三遍,webpack
反過來講,Nginx、Java/Nodejs等編程語言的HTTPClient以及手機App,他們發出的HTTP請求就徹底沒有跨域問題,由於他們不是瀏覽器,沒有實現W3C規範。ios
在瀏覽器中假設有如下一段代碼會執行結果會是什麼樣?nginx
<script> window.callback = function (data) { console.log(data); delete window.callback; } </script>
<script> callback({ "code": 1, "data": [1,2,3] }); </script>
複製代碼
毫無疑問,確定是在控制檯輸出了一個對象信息。 記得剛纔在介紹跨域基本概念的時候說個瀏覽器不限制script
標籤加載js文件。那麼把這兩者的特性相結合。第二個script
標籤改成從網絡加載. 就能夠實現跨域. 例如 一個跨域APIhttp://api.domain.com/v1/users/1
<script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script>
GET http://api.domain.com/v1/users/1?callback=callbackFun
的請求application/javascript
callbackFun({/*須要的數據*/});
複製代碼
以上步驟就是JSONP的思想。實現一個完善的JSNOP請求庫還有細節要處理,好比超時取消、回調函數防重名等。不少開源庫(jQuery, axios)都實現了JSNOP請求。想要代碼的去Github閱讀源碼,這裏就不給出代碼。
JSONP雖然是一種實現跨域訪問的方法,前端想要使用JSONP進行跨域訪問卻不容易。
GET http://api.domain.com/v1/users/1
返回ContentType: application/json
複製代碼
{
"code": 1,
"data" : {"userid": 1}
}
複製代碼
而GET http://api.domain.com/v1/users/1?callback=callbackFun
返回
ContentType: application/javascript
複製代碼
callbackFun({
"code": 1,
"data" : {"userid": 1}
});
複製代碼
既然能夠和後端商量配合你改造接口,那還有更好的方案能夠解決。何須用這種方案。
JSONP有一個有點就是兼容性好,IE678統統兼容,因此通常JSNOP是後端同窗若是主動須要開放API給他人使用,同時有須要極高的兼容的一個妥協方案。通常狀況下不推薦這個方案。
真實經歷。以前開發項目須要調用另外一個項目組的接口。 跨域形成接口掉不通,而後找Z君溝通, Z君說:"你用JSONP來掉接口就行了。這都不知道...." 而後我還在想大神這麼NB的麼,JSONP兼容都提早作完了。我試了JSONP。坑爹呀,你後端根本就沒兼容JSONP,我怎麼調用,呵呵... 呵呵呵呵....
JSONP方案不推薦,那麼又須要訪問跨域接口,怎麼辦呢? 重要事情不在意再多說一遍拒絕跨域請求是瀏覽器。 那麼若是有一個非W3C標準的HttpClient幫助咱們轉發請求,不就能夠了實現跨域訪問了。
一般App對於webview都有很強的控制權,能夠在Webview的JS環境中注入一些方法。 那麼移動端程序員能夠在Webview中注入一個接口,運行在裏面的js代碼能夠經過這個方法把本身的請求地址、請求參數、請求體等數據交給App Native端,讓App Native代爲收發請求。App Native不是瀏覽器,不受跨域限制。
Web端必然運行在瀏覽器環境中,那麼沒有App Native。還有服務器上能夠作反向代理。 所謂的反向代理,原理和App Native請求代理的原理差很少,就是咱們請求非跨域下的反向代理服務,反向代理服務會把你的請求轉發給目標服務器。 反向代理服務能夠是Nginx也能夠是java/Nodejs程序等等。這些程序也不受跨域限制,能夠接受目標服務器的請求,並返回給咱們。
React/Vue 這種SPA開發施行的徹底的先後端分離的模式,開發階段必然是須要跨域訪問接口的。 Vue開發能夠這樣配置:
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: '<url>',
ws: true,
changeOrigin: true
},
'/foo': {
target: '<other_url>'
}
}
}
}
複製代碼
詳情見Vue CLI文檔。React也有相似的配置,詳情見 create-react-app文檔
那麼他們具體是怎麼實現的呢? 本地起一個服務端程序提供反向代理的能力。而React/Vue本地啓動的這個服務端程序就是Webpack-dev-server。來探索Webpack-dev-server源碼,源碼中啓動server的關鍵代碼在lib/Server.js中,挑重點
/* 此處省略許多行代碼 */
// 27行 引入express 做爲服務端框架
const express = require('express');
/* 此處省略許多行代碼 */
// 31行 引入 http-proxy-middleware 提供反向代理的能力
const httpProxyMiddleware = require('http-proxy-middleware');
/* 此處省略許多行代碼 */
// 328行 獲取 proxyMiddleware 並加載到爲express的中間件Middleware
app.use((req, res, next) = > {
if (typeof proxyConfigOrCallback === 'function') {
const newProxyConfig = proxyConfigOrCallback();
if (newProxyConfig !== proxyConfig) {
proxyConfig = newProxyConfig;
// 334行 根據 proxyConfig 獲取 處理proxy請求的中間件proxyMiddleware
proxyMiddleware = getProxyMiddleware(proxyConfig);
}
}
const bypass = typeof proxyConfig.bypass === 'function';
const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false;
if (bypassUrl) {
req.url = bypassUrl;
next();
} else if (proxyMiddleware) {
// 347行 最最關鍵一行 通過屢次斷定某個請求是須要代理轉發的請求,那麼把它交給proxyMiddleware進行處理, proxyMiddleware
return proxyMiddleware(req, res, next);
} else {
next();
}
});
複製代碼
以上代碼有點NodeJS服務端開發的同窗基本能看明白,看不明白也不要緊。你知道React/Vue能夠經過相應的配置項得到接口跨域訪問的能力就能夠了。其中最核心的就是依靠Express的網絡請求能力充當反向代理服務器。
開發階段還能夠經過本地啓動一個Express服務器做爲代理,幫助咱們處理跨域問題,問題是生產環境是不推薦這麼作的。React/Vue 項目一般在build之後會生成如下文件:
對於index.html的部署,Vue-Router文檔寫的很清楚。推薦經過nginx try-file命令來進行部署。同時nginx又是一個反向代理服務器。假設 網頁須要在host http://www.domain.com/
下, 真實API服務部署在http://api.domain.com/api
。那麼經過反向代理把接口代理到 http://www.domain.com/api
下。那麼跨域訪問就變成了同域名訪問。 那麼nginx的配置文件能夠這樣寫
server {
listen 80;
server_name www.domain.com ;
root www; # 存放html文件的文件夾
location ^/api { # 接口代理到 8080
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_pass http://api.domain.com/api;
}
location / { # 其餘請求返回index.html
try_files $uri $uri/ /index.html;
}
}
複製代碼
這樣作完, 訪問API就會被代理轉發,訪問其餘路徑就返回html。以下圖所示:
線上部署的方式可能根據系統架構選型而多種多樣。這只是其中一種比較通用且爲官方推薦的方式。僅作參考。相似ngixn的服務端軟件仍是Caddy、Envoy
這種方案的優勢是不須要後端同窗改動接口,只須要運維小哥幫助配置一下nginx便可完成兼容。缺點是多一次轉發可能帶來性能損失。
實際狀況多種多樣,有些時候沒辦法使用JSONP,也經過nginx轉發又會產生性能損失。那麼還有一個終極大招———— CORS.
W3C的同源策略出來之後形成了不少不便,沒法應對某些跨域訪問的強需求。爲此W3C增長了CORS相關的規範, 文檔以前也說起過:MDN CORS文檔。
重要的事情再重複一遍:拒絕跨域請求是瀏覽器,那麼CORS的原理就是CORS相關的規範中制定了一些響應頭(Response Header),這些響應頭以Access-Control-Request-
開頭。簡單枚舉幾個,具體這些頭的含義和用法見MDN CORS文檔.
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Allow-Origin: http://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
複製代碼
瀏覽器在接收到設置過CORS響應頭的返回之後,會根據CORS規範檢查合法性,檢查經過則再也不阻止,放行經過。
簡而言之就是CORS響應頭就是用來告訴瀏覽器:"我是雖然是跨域請求,可是我是合法的,請不要拒絕我"。
CORS方案的優勢是支持各類方法 GET、POST、PUT、DELTE等等。並且改動量比較小。能夠在服務端程序好比Java或者NodeJS上作,也可經過前置代理服務器nginx完成。
缺點就是
其餘還有用與父頁面與子頁面(iframe)之間的通訊的跨域問題,window.name、postMessage等方法。這裏就不詳細說了。平常用的確實很少,有須要再查把。
生產環境中建議選擇順序是 反向代理 > CORS >> JSONP。 由於反向代理兼容性最好,程序改動少。 CORS適用於沒法容忍反向代理的性能損失和第三方OpenAPI訪問。 JSONP 只有當後端須要兼容性高,沒辦法部署反向代理服務器的狀況以及前端訪問第三方提供的JSONP接口。其餘任何狀況下不推薦。