因為瀏覽器出于安全考慮,有同源策略。也就是說,如果協議、域名、端口有一個不同就是跨域,Ajax 請求會失敗。
我們可以通過以下幾種常用方法解決跨域的問題
JSONP
JSONP 的原理很簡單,就是利用 <script>
標簽沒有跨域限制的漏洞。通過 <script>
標簽指向一個需要訪問的地址并提供一個回調函數來接收數據
涉及到的端
JSONP 需要服務端和前端配合實現。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>function jsonp(data) {console.log(data)}
</script>
JSONP 使用簡單且兼容性不錯,但是只限于 get 請求
具體實現方式
在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,這時候就需要自己封裝一個 JSONP,以下是簡單實現
function jsonp(url, jsonpCallback, success) {let script = document.createElement("script");script.src = url;script.async = true;script.type = "text/javascript";window[jsonpCallback] = function(data) {success && success(data);};document.body.appendChild(script);
}
jsonp("http://xxx","callback",function(value) {console.log(value);}
);
CORS
CORS (Cross-Origin Resource Sharing,跨域資源共享) 是目前最為廣泛的解決跨域問題的方案。方案依賴服務端/后端在響應頭中添加 Access-Control-Allow-* 頭,告知瀏覽器端通過此請求
涉及到的端
CORS 只需要服務端/后端支持即可,不涉及前端改動
- CORS需要瀏覽器和后端同時支持。IE 8 和 9 需要通過 XDomainRequest 來實現。
- 瀏覽器會自動進行 CORS 通信,實現CORS通信的關鍵是后端。只要后端實現了 CORS,就實現了跨域。
- 服務端設置 Access-Control-Allow-Origin 就可以開啟 CORS。 該屬性表示哪些域名可以訪問資源,如果設置通配符則表示所有網站都可以訪問資源。
CORS 實現起來非常方便,只需要增加一些 HTTP 頭,讓服務器能聲明允許的訪問來源
只要后端實現了 CORS,就實現了跨域
以 koa框架舉例
添加中間件,直接設置Access-Control-Allow-Origin請求頭
app.use(async (ctx, next)=> {ctx.set('Access-Control-Allow-Origin', '*');ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');if (ctx.method == 'OPTIONS') {ctx.body = 200; } else {await next();}
})
具體實現方式
CORS 將請求分為簡單請求(Simple Requests)和需預檢請求(Preflighted requests),不同場景有不同的行為
簡單請求:不會觸發預檢請求的稱為簡單請求。當請求滿足以下條件時就是一個簡單請求:
- 請求方法:GET、HEAD、POST。
- 請求頭:Accept、Accept-Language、Content-Language、Content-Type。
- Content-Type 僅支持:application/x-www-form-urlencoded、multipart/form-data、text/plain
需預檢請求:當一個請求不滿足以上簡單請求的條件時,瀏覽器會自動向服務端發送一個 OPTIONS 請求,通過服務端返回的Access-Control-Allow-* 判定請求是否被允許
CORS 引入了以下幾個以 Access-Control-Allow-* 開頭:
- Access-Control-Allow-Origin 表示允許的來源
- Access-Control-Allow-Methods 表示允許的請求方法
- Access-Control-Allow-Headers 表示允許的請求頭
- Access-Control-Allow-Credentials 表示允許攜帶認證信息
當請求符合響應頭的這些條件時,瀏覽器才會發送并響應正式的請求
nginx反向代理
反向代理只需要服務端/后端支持,幾乎不涉及前端改動,只用切換接口即可
nginx 配置跨域,可以為全局配置和單個代理配置(兩者不能同時配置)
全局配置,在nginx.conf文件中的 http 節點加入跨域信息
http {# 跨域配置add_header 'Access-Control-Allow-Origin' '$http_origin' ;add_header 'Access-Control-Allow-Credentials' 'true' ;add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;
}
局部配置(單個代理配置跨域), 在路徑匹配符中加入跨域信息
server {listen 8080;server_name server_name;charset utf-8;location / {# 這里配置單個代理跨域,跨域配置add_header 'Access-Control-Allow-Origin' '$http_origin' ;add_header 'Access-Control-Allow-Credentials' 'true' ;add_header 'Access-Control-Allow-Methods' 'PUT,POST,GET,DELETE,OPTIONS' ;add_header 'Access-Control-Allow-Headers' 'Content-Type,Content-Length,Authorization,Accept,X-Requested-With' ;#配置代理 代理到本機服務端口proxy_pass http://127.0.0.1:9000;proxy_redirect off;proxy_set_header Host $host:$server_port;proxy_set_header X-Real-IP $remote_addr;proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;}
}
Node 中間層接口轉發
const router = require('koa-router')()
const rp = require('request-promise');// 通過node中間層轉發實現接口跨域
router.post('/github', async (ctx, next) => {let {category = 'trending',lang = 'javascript',limit,offset,period} = ctx.request.body lang = lang || 'javascript'limit = limit || 30offset = offset || 0period = period || 'week'let res = await rp({method: 'POST',// 跨域的接口uri: `https://e.juejin.cn/resources/github`,body: {category,lang,limit,offset,period},json: true})ctx.body = res
})module.exports = router
Proxy
如果是通過vue-cli腳手架工具搭建項目,我們可以通過webpack為我們起一個本地服務器作為請求的代理對象
通過該服務器轉發請求至目標服務器,得到結果再轉發給前端,但是最終發布上線時如果web應用和接口服務器不在一起仍會跨域
在vue.config.js文件,新增以下代碼
module.exports = {devServer: {host: '127.0.0.1',port: 8080,open: true,// vue項目啟動時自動打開瀏覽器proxy: {'/api': { // '/api'是代理標識,用于告訴node,url前面是/api的就是使用代理的target: "http://xxx.xxx.xx.xx:8080", //目標地址,一般是指后臺服務器地址changeOrigin: true, //是否跨域pathRewrite: { // pathRewrite 的作用是把實際Request Url中的'/api'用""代替'^/api': "" }}}}
}
通過axios發送請求中,配置請求的根路徑
axios.defaults.baseURL = '/api'
此外,還可通過服務端實現代理請求轉發,以express框架為例
var express = require('express');
const proxy = require('http-proxy-middleware')
const app = express()
app.use(express.static(__dirname + '/'))
app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false}));
module.exports = app
websocket
webSocket本身不存在跨域問題,所以我們可以利用webSocket來進行非同源之間的通信
原理:利用webSocket的API,可以直接new一個socket實例,然后通過open方法內send要傳輸到后臺的值,也可以利用message方法接收后臺傳來的數據。后臺是通過new WebSocket.Server({port:3000})實例,利用message接收數據,利用send向客戶端發送數據。具體看以下代碼:
function socketConnect(url) {// 客戶端與服務器進行連接let ws = new WebSocket(url); // 返回`WebSocket`對象,賦值給變量ws// 連接成功回調ws.onopen = e => {console.log('連接成功', e)ws.send('我發送消息給服務端'); // 客戶端與服務器端通信}// 監聽服務器端返回的信息ws.onmessage = e => {console.log('服務器端返回:', e.data)// do something}return ws; // 返回websocket對象
}
let wsValue = socketConnect('ws://121.40.165.18:8800'); // websocket對象
document.domain(不常用)
- 該方式只能用于二級域名相同的情況下,比如 a.test.com 和 b.test.com 適用于該方式。
- 只需要給頁面添加 document.domain = ‘test.com’ 表示二級域名都相同就可以實現跨域
- 自 Chrome 101 版本開始,document.domain 將變為可讀屬性,也就是意味著上述這種跨域的方式被禁用了
postMessage(不常用)
在兩個 origin 下分別部署一套頁面 A 與 B,A 頁面通過 iframe 加載 B 頁面并監聽消息,B 頁面發送消息
這種方式通常用于獲取嵌入頁面中的第三方頁面數據。一個頁面發送消息,另一個頁面判斷來源并接收消息
// 發送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {var origin = event.origin || event.originalEvent.origin;if (origin === 'http://test.com') {console.log('驗證通過')}
});
window.name(不常用)
主要是利用 window.name 頁面跳轉不改變的特性實現跨域,即 iframe 加載一個跨域頁面,設置 window.name,跳轉到同域頁面,可以通過 $(‘iframe’).contentWindow.name 拿到跨域頁面的數據
實例說明
比如有一個www.example.com/a.html頁面。需要通過a.html頁面里的js來獲取另一個位于不同域上的頁面www.test.com/data.html中的數據。
data.html頁面中設置一個window.name即可,代碼如下
<script>window.name = "我是data.html中設置的a頁面想要的數據";
</script>
- 那么接下來問題來了,我們怎么把data.html頁面載入進來呢,顯然我們不能直接在a.html頁面中通過改變window.location來載入data.html頁面(因為我們現在需要實現的是a.html頁面不跳轉,但是也能夠獲取到data.html中的數據)
- 具體的實現其實就是在a.html頁面中使用一個隱藏的iframe來充當一個中間角色,由iframe去獲取data.html的數據,然后a.html再去得到iframe獲取到的數據。
- 充當中間人的iframe想要獲取到data.html中通過window.name設置的數據,只要要把這個iframe的src設置為www.test.com/data.html即可,然后a.html想要得到iframe所獲取到的數據,也就是想要得到iframe的widnow.name的值,還必須把這個iframe的src設置成跟a.html頁面同一個域才行,不然根據同源策略,a.html是不能訪問到iframe中的window.name屬性的
<!-- a.html中的代碼 -->
<iframe id="proxy" src="http://www.test.com/data.html" style="display: none;" onload = "getData()"> <script>function getData(){var iframe = document.getElementById('proxy);iframe.onload = function(){var data = iframe.contentWindow.name;//上述即為獲取iframe里的window.name也就是data.html頁面中所設置的數據;}iframe.src = 'b.html'; //這里的b為隨便的一個頁面,只有與a.html同源就行,目的讓a.html等訪問到iframe里的東西,設置成about:blank也行}
</script>
上面的代碼只是最簡單的原理演示代碼,你可以對使用js封裝上面的過程,比如動態的創建iframe,動態的注冊各種事件等等,當然為了安全,獲取完數據后,還可以銷毀作為代理的iframe
補充
跨域與監控
前端項目在統計前端報錯監控時會遇到上報的內容只有 Script Error 的問題。這個問題也是由同源策略引起。在 <script>
標簽上添加 crossorigin=“anonymous” 并且返回的 JS 文件響應頭加上 Access-Control-Allow-Origin: * 即可捕捉到完整的錯誤堆棧
跨域與圖片
前端項目在圖片處理時可能會遇到圖片繪制到 Canvas 上之后卻不能讀取像素或導出 base64 的問題。這個問題也是由同源策略引起。解決方式和上文相同,給圖片添加 crossorigin=“anonymous” 并在返回的圖片文件響應頭加上 Access-Control-Allow-Origin: * 即可解決