今天咱們來聊一個老生常談的話題,跨域!html
又是跨域,煩不煩 ?網上跨域的文章那麼多,跨的我眼睛都疲勞了,不看了不看了 🤣 別走...我儘可能用最簡單的方式將常見的幾種跨域解決方案給你們闡釋清楚,相信認真看完本文,之後不論是做爲受試者仍是面試官,對於這塊的知識都可以遊刃有餘。webpack
不是講跨域嗎 ?怎麼又來個「跨源」 ?字都能打錯的 ?nginx
😄...稍安勿躁,其實咱們日常說的跨域是一種狹義的請求場景,簡單來講,就是「跨「過瀏覽器的同源策略去請求資「源」,因此咱們叫它「跨源」也沒啥問題。那麼,git
跨源,源是什麼?瀏覽器的同源策略github
什麼是同源?協議,域名,端口都相同就是同源web
乾巴巴的,能不能舉個栗子?栗子:),有的有的:面試
const url = 'https://www.google.com:3000'
複製代碼
好比上面的這個 URL,協議是:https,域名是 www.google.com,端口是 3000。json
不一樣源了會怎麼樣?會有不少限制,好比:設計模式
我就想請求個東西,至於嗎,爲何要搞個這麼個東西限制我?基於安全考慮,沒有它,你可能會遇到:跨域
因此,得有。正是由於瀏覽器同源策略的存在,你的 Ajax 請求有可能在發出去後就被攔截了,它還會給你報個錯:
✘ Access to XMLHttpRequest at 'xxx' from origin 'xxx' has been block by CORS,
policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
複製代碼
這種發出去拿不到響應的感覺,就像你在網上衝浪時,被一股神祕的東方力量限制了同樣:
很是難受,因此,咱們接下來就來看看怎麼用科學的方法上網( 啊呸,科學的方法解決跨域的問題)。這玩意兒就是利用了 <script>
標籤的 src 屬性沒有跨域限制的漏洞,讓咱們能夠獲得從其餘來源動態產生的 JSON 數據。
爲何叫 JSONP ?JSONP 是 JSON with Padding 的縮寫,額,至於爲何叫這個名字,我網上找了下也沒個標準的解釋,還望評論區的各位老哥知道的趕忙告訴我: )
怎麼實現 ?具體實現思路大體分爲如下步驟:
瀏覽器接受響應後就會去執行回調函數 callback,傳遞解析後的 JSON 對象做爲參數,這樣咱們就能夠在 callback 裏處理數據了。實際開發中,會遇到回調函數名相同的狀況,能夠簡單封裝一個 JSONP 函數:
function jsonp({ url, params, callback }) {
return new Promise((resolve, reject) => {
// 建立一個臨時的 script 標籤用於發起請求
const script = document.createElement('script');
// 將回調函數臨時綁定到 window 對象,回調函數執行完成後,移除 script 標籤
window[callback] = data => {
resolve(data);
document.body.removeChild(script);
};
// 構造 GET 請求參數,key=value&callback=callback
const formatParams = { ...params, callback };
const requestParams = Object.keys(formatParams)
.reduce((acc, cur) => {
return acc.concat([`${cur}=${formatParams[cur]}`]);
}, [])
.join('&');
// 構造 GET 請求的 url 地址
const src = `${url}?${requestParams}`;
script.setAttribute('src', src);
document.body.appendChild(script);
});
}
// 調用時
jsonp({
url: 'https://xxx.xxx',
params: {...},
callback: 'func',
})
複製代碼
咱們用 Promise 封裝了請求,使異步回調更加優雅,可是別看樓上的洋洋灑灑寫了一大段,其實本質上就是:
<script src='https://xxx.xxx.xx?key=value&callback=xxx'>複製代碼
想要看例子 ?戳這裏
JSONP 的優勢是簡單並且兼容性很好,可是缺點也很明顯,須要服務器支持並且只支持 GET 請求,下面咱們來看第二種方案,也是目前主流的跨域解決方案,劃重點!😁
CORS(Cross-Origin Resource Sharing)的全稱叫 跨域資源共享,名稱好高大上,別怕,這玩意兒其實就是一種機制。瀏覽器不是有同源策略吶,這東西好是好,可是對於開發人員來講就不怎麼友好了,由於咱們可能常常須要發起一個 跨域 HTTP 請求。咱們以前說過,跨域的請求實際上是發出去了的,只不過被瀏覽器給攔截了,由於不安全,說直白點兒就是,你想要從服務器哪兒拿個東西,可是沒有通過人家容許啊。因此怎麼樣才安全 ?服務器容許了不就安全了,這就是 CORS 實現的原理:使用額外的 HTTP 頭來告訴瀏覽器,讓運行在某一個 origin 上的 Web 應用容許訪問來自不一樣源服務器上的指定的資源。
目前,全部的主流瀏覽器都支持 CORS,其中,IE 瀏覽器的版本不能低於 10,IE 8 和 9 須要經過 XDomainRequest 來實現。
完整的兼容性狀況 ? 戳這裏
CORS 須要瀏覽器和服務器同時支持,整個 CORS 的通訊過程,都是瀏覽器自動完成。
怎麼個自動法 ?
簡單來講,瀏覽器一旦發現請求是一個跨域請求,首先會判斷請求的類型,
若是是簡單請求,會在請求頭中增長一個 Origin 字段,表示此次請求是來自哪個源。而服務器接受到請求後,會返回一個響應,響應頭中會包含一個叫 Access-Control-Allow-Origin 的字段,它的值要麼包含由 Origin 首部字段所指明的域名,要麼是一個 "*",表示接受任意域名的請求。若是響應頭中沒有這個字段,就說明當前源不在服務器的許可範圍內,瀏覽器就會報錯:
GET /cors HTTP/1.1
Origin: https://xxx.xx
Accept-Language: en-US
Connection: keep-alive
... ...
複製代碼
若是是非簡單請求,會在正式通訊以前,發送一個預檢請求(preflight),目的在於詢問服務器,當前網頁所在的域名是否在服務器的許可名單之中,以及可使用哪些 HTTP 動詞和頭信息字段,只有獲得確定答覆,瀏覽器纔會發出正式的請求,不然就報錯。你可能發現咱們在平常的開發中,會看到不少使用 OPTION 方法發起的請求,它其實就是一個預檢請求:
OPTIONS /cors HTTP/1.1
Origin: http://xxx.xx
Access-Control-Request-Method: PUT
Accept-Language: en-US
... ...
複製代碼
那麼到底哪些是簡單請求,哪些是非簡單請求 ?
不會觸發 CORS 預檢的,就是簡單請求。哪些請求不會觸發預檢 ?
使用如下方法之一:GET, HEAD, POST,
而且 Content-Type 的值僅限於下列三者之一:
相反,不符合上述條件的就是非簡單請求啦。
因此,實現 CORS 的關鍵是服務器,只要服務器實現了 CORS 的相關接口,就能夠實現跨域。CORS 與 JSONP相比,優點是支持全部的請求方法,缺點是兼容性上較 JSONP 差。除了 JSONP 和 CORS 外,還有一種經常使用的跨域解決方案:PostMessage,它更多地用於窗口間的消息傳遞。
PostMessage 是 Html5 XMLHttpRequest Level 2 中的 API,它能夠實現跨文檔通訊(Cross-document messaging)。兼容性上,IE8+,Chrome,Firfox 等主流瀏覽器都支持,能夠放心用😊,如何理解跨文檔通訊?
你能夠類比設計模式中的發佈-訂閱模式,在這裏,一個窗口發送消息,另外一個窗口接受消息,之因此說相似發佈-訂閱模式,而不是觀察者模式,是由於這裏兩個窗口間沒有直接通訊,而是經過瀏覽器這個第三方平臺。
window.postMessage(message, origin, [transfer])
複製代碼
postMessage 方法接收三個參數,要發送的消息,接收消息的源和一個可選的 Transferable 對象,如何接收消息 ?
window.addEventListener("message", function receiveMessage(event) {}, false); // 推薦,兼容性更好
window.onmessage = function receiveMessage(event) {} // 不推薦,這是一個實驗性的功能,兼容性不如上面的方法
複製代碼
接收到消息後,消息對象 event 中包含了三個屬性:source,origin,data,其中 data 就是咱們發送的 message。
此外,除了實現窗口通訊,postMessage 還能夠同 Web Worker 和 Service Work 進行通訊,有興趣的能夠 戳這裏。
Websocket 是 HTML5 的一個持久化的協議,它實現了瀏覽器與服務器的全雙工通訊,同時也是跨域的一種解決方案。什麼是全雙工通訊 ?簡單來講,就是在創建鏈接以後,server 與 client 都能主動向對方發送或接收數據。
原生的 WebSocket API 使用起來不太方便,咱們通常會選擇本身封裝一個 Websocket(嗯,咱們團隊也本身封了一個 : ))或者使用已有的第三方庫,咱們這裏以第三方庫 ws 爲例:
const WebSocket = require('ws');
const ws = new WebSocket('ws://www.host.com/path');
ws.on('open', function open() {
ws.send('something');
});
ws.on('message', function incoming(data) {
console.log(data);
});
... ...
複製代碼
須要注意的是,Websocket 屬於長鏈接,在一個頁面創建多個 Websocket 鏈接可能會致使性能問題。
咱們知道同源策略限制的是:瀏覽器向服務器發送跨域請求須要遵循的標準,那若是是服務器向服務器發送跨域請求呢?
答案固然是,不受瀏覽器的同源策略限制。
利用這個思路,咱們就能夠搭建一個代理服務器,接受客戶端請求,而後將請求轉發給服務器,拿到響應後,再將響應轉發給客戶端:
這就是 Nginx 反向代理的原理,只須要簡單配置就能夠實現跨域:
# nginx.config
# ...
server {
listen 80;
server_name www.domain1.com;
location / {
proxy_pass http://www.domain2.com:8080; #反向代理
proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie裏域名
index index.html index.htm;
# 當用 webpack-dev-server 等中間件代理接口訪問 nignx 時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啓用
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
# ...
}
}
複製代碼
實現的原理和咱們前文提到的代理服務器原理一模一樣,只不過這裏使用了 Node 中間件作爲代理。須要注意的是,瀏覽器向代理服務器請求時仍然遵循同源策略,別忘了在 Node 層經過 CORS 作跨域處理:
const https = require('https')
// 接受客戶端請求
const sever = https.createServer((req, res) => {
...
const { method, headers } = req
// 設置 CORS 容許跨域
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type',
...
})
// 請求服務器
const proxy = https.request({ host: 'xxx', method, headers, ...}, response => {
let body = ''
response.on('data', chunk => { body = body + chunk })
response.on('end', () => {
// 響應結果轉發給客戶端
res.end(body)
})
})
})
複製代碼
二級域名相同的狀況下,設置 document.domain 就能夠實現跨域。什麼是二級域名 ?
a.test.com 和 b.test.com 就屬於二級域名,它們都是 test.com 的子域。如何實現跨域 ?
document.domain = 'test.com' // 設置 domain 相同
// 經過 iframe 嵌入跨域的頁面
const iframe = document.createElement('iframe')
iframe.setAttribute('src', 'b.test.com/xxx.html')
iframe.onload = function() {
// 拿到 iframe 實例後就能夠直接訪問 iframe 中的數據
console.log(iframe.contentWindow.xxx)
}
document.appendChild(iframe)
複製代碼
固然,除了上述的方案外,比較 Hack 的還有:window.name, location.hash,可是這些跨域的方式如今咱們已經不推薦了,爲何 ?由於相比之下有更加安全和強大的 PostMessage 做爲替代。跨域的方案其實有不少,總結下來:
本文首發於個人 博客,才疏學淺,不免有錯誤,文章有誤之處還望不吝指正!
若是有疑問或者發現錯誤,能夠在評論區進行提問和勘誤,
若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。
(完)