關於跨域,你想知道的全在這裡

想學習更多前端或編程知識,歡迎關注專欄:敲代碼,學編程 - 知乎專欄

閱讀本文前,希望你有一定的 JS/Node 基礎,這裡不另外介紹如何使用 ajax 做非同步請求,如果不了解,可以先看:

Ajax 知識體系大梳理 - 掘金

最近在面試的時候常被問到如何解決跨域的問題,看了網上的一些文章後,發現許多文章都沒有寫清楚明白,使讀者(我)感到困惑,所以今天我整理了一下常用跨域的技巧,寫這篇關於跨域文章的目的在於:

  1. 介紹常見的跨域的解決方法以及其優缺點
  2. 模擬實際的跨域場景,在每種方式後都會給出一個簡單實例,你只要跟我做一起敲代碼,就更加直觀地理解這些跨域的技巧

這篇文章的所有代碼我放在了 happylindz/blog Github 上,建議你 clone 下來,方便你閱讀代碼,跟我一起測試。

後面代碼的測試環境:不考慮跨域的兼容性問題,旨在理解其思想

  • Mac os
  • Chrome 54+
  • Node 6.8.1+

同源策略:

使用過 Ajax 的同學都知道其便利性,可以在不向伺服器提交完整的頁面的情況下,實現局部更新頁面。但是瀏覽器處於對安全方面的考慮,不允許跨域調用其他頁面的對象,這對於我們在注入 iframe 或是 ajax 應用上帶來不少麻煩。

簡單說來,只有當協議,域名,埠相同的時候才算是同一個域名,否則均認為需要做跨域的處理。

跨域方法:

今天一共介紹七種常用跨域的方式,關於跨域大概可以分為 iframe 的跨域和純粹的跨全域請求。

下面就先介紹三種跨全域的方法:

1. JSONP:

只要說到跨域,就必須聊到 JSONP,JSONP全稱為:JSON with Padding,可用於解決主流瀏覽器的跨域數據訪問的問題。

Web 頁面上調用 js 文件不受瀏覽器同源策略的影響,所以通過 Script 便簽可以進行跨域的請求:

  1. 首先前端先設置好回調函數,並將其作為 url 的參數。
  2. 服務端接收到請求後,通過該參數獲得回調函數名,並將數據放在參數中將其返回
  3. 收到結果後因為是 script 標籤,所以瀏覽器會當做是腳本進行運行,從而達到跨域獲取數據的目的。

實例:

後端邏輯:

//server.jsconst url = require("url"); require("http").createServer((req, res) => { const data = { x: 10 }; const callback = url.parse(req.url, true).query.callback; res.writeHead(200); res.end(`${callback}(${JSON.stringify(data)})`);}).listen(3000, "127.0.0.1");console.log("啟動服務,監聽 127.0.0.1:3000");

通過 node server.js 啟動服務,監聽埠 3000,這樣服務端就建立起來了

前端頁面:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>index.html</title></head><body> <script> function jsonpCallback(data) { alert("獲得 X 數據:" + data.x); } </script> <script src="http://127.0.0.1:3000?callback=jsonpCallback"></script></body></html>

邏輯已經寫好了,那如何來模擬一個跨域的場景呢?

這裡我們通過埠號的不同來模擬跨域的場景,通過 127.0.0.1:8080 埠來訪問頁面。先通過 npm 下載 http-server 模塊:

nom install -g http-server

並且在頁面同目錄下輸入:

http-server

這樣就可以通過埠 8080 訪問 index.html 剛才那個頁面了,相當於是開啟兩個監聽不同埠的 http 伺服器,通過頁面中的請求來模擬跨域的場景。打開瀏覽器,訪問 127.0.0.1:8080 就可以看到從 127.0.0.1:3000 獲取到的數據了。

至此,通過 JSONP 跨域獲取數據已經成功了,但是通過這種事方式也存在著一定的優缺點:

優點:

  1. 它不像XMLHttpRequest 對象實現 Ajax 請求那樣受到同源策略的限制
  2. 兼容性很好,在古老的瀏覽器也能很好的運行
  3. 不需要 XMLHttpRequest 或 ActiveX 的支持;並且在請求完畢後可以通過調用 callback 的方式回傳結果。

缺點:

  1. 它支持 GET 請求而不支持 POST 等其它類行的 HTTP 請求。
  2. 它只支持跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進行數據通信的問題

2. CORS:

CORS 是一個 W3C 標準,全稱是"跨域資源共享"(Cross-origin resource sharing)它允許瀏覽器向跨源伺服器,發出 XMLHttpRequest 請求,從而克服了 ajax 只能同源使用的限制。

CORS 需要瀏覽器和伺服器同時支持才可以生效,對於開發者來說,CORS 通信與同源的 ajax 通信沒有差別,代碼完全一樣。瀏覽器一旦發現 ajax 請求跨源,就會自動添加一些附加的頭信息,有時還會多出一次附加的請求,但用戶不會有感覺。

因此,實現 CORS 通信的關鍵是伺服器。只要伺服器實現了 CORS 介面,就可以跨源通信。

首先前端先創建一個 index.html 頁面:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>CORS</title></head><body> <script> const xhr = new XMLHttpRequest(); xhr.open("GET", "http://127.0.0.1:3000", true); xhr.onreadystatechange = function() { if(xhr.readyState === 4 && xhr.status === 200) { alert(xhr.responseText); } } xhr.send(null); </script></body></html>

這似乎跟一次正常的非同步 ajax 請求沒有什麼區別,關鍵是在服務端收到請求後的處理:

require("http").createServer((req, res) => { res.writeHead(200, { "Access-Control-Allow-Origin": "http://localhost:8080" }); res.end("這是你要的數據:1111");}).listen(3000, "127.0.0.1");console.log("啟動服務,監聽 127.0.0.1:3000");

關鍵是在於設置相應頭中的 Access-Control-Allow-Origin,該值要與請求頭中 Origin 一致才能生效,否則將跨域失敗。

接下來再次開啟兩個 http 伺服器進程:

打開瀏覽器訪問 localhost:8080 就可以看到:

成功的關鍵在於 Access-Control-Allow-Origin 是否包含請求頁面的域名,如果不包含的話,瀏覽器將認為這是一次失敗的非同步請求,將會調用 xhr.onerror 中的函數。

CORS 的優缺點:

  1. 使用簡單方便,更為安全
  2. 支持 POST 請求方式
  3. CORS 是一種新型的跨域問題的解決方案,存在兼容問題,僅支持 IE 10 以上

這裡只是對 CORS 做一個簡單的介紹,如果想更詳細地了解其原理的話,可以看看下面這篇文章:

跨域資源共享 CORS 詳解 - 阮一峰的網路日誌

3. Server Proxy:

伺服器代理,顧名思義,當你需要有跨域的請求操作時發送請求給後端,讓後端幫你代為請求,然後最後將獲取的結果發送給你。

假設有這樣的一個場景,你的頁面需要獲取 CNode:Node.js專業中文社區 論壇上一些數據,如通過 https://cnodejs.org/api/v1/topics,當時因為不同域,所以你可以將請求後端,讓其對該請求代為轉發。

代碼如下:

const url = require("url");const http = require("http");const https = require("https");const server = http.createServer((req, res) => { const path = url.parse(req.url).path.slice(1); if(path === "topics") { https.get("https://cnodejs.org/api/v1/topics", (resp) => { let data = ""; resp.on("data", chunk => { data += chunk; }); resp.on("end", () => { res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" }); res.end(data); }); }) }}).listen(3000, "127.0.0.1");console.log("啟動服務,監聽 127.0.0.1:3000");

通過代碼你可以看出,當你訪問 127.0.0.1:3000 的時候,伺服器收到請求,會代你發送請求 https://cnodejs.org/api/v1/topics 最後將獲取到的數據發送給瀏覽器。

同樣地開啟服務:

打開瀏覽器訪問 localhost:3000/topics,就可以看到

跨域請求成功。

純粹的跨全域請求的方式已經介紹完了,另外介紹四種通過 iframe 跨域與其它頁面通信的方式。

4. location.hash:

在 url 中,baidu.com# 的 "#helloworld" 就是 location.hash,改變 hash 值不會導致頁面刷新,所以可以利用 hash 值來進行數據的傳遞,當然數據量是有限的。

假設 localhost:8080 下有文件 cs1.html 要和 localhost:8081 下的 cs2.html 傳遞消息,cs1.html 首先創建一個隱藏的 iframe,iframe 的 src 指向 localhost:8081/cs2.html,這時的 hash 值就可以做參數傳遞。

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>CS1</title></head><body> <script> // http://localhost:8080/cs1.html let ifr = document.createElement("iframe"); ifr.style.display = "none"; ifr.src = "http://localhost:8081/cs2.html#data"; document.body.appendChild(ifr); function checkHash() { try { let data = location.hash ? location.hash.substring(1) : ""; console.log("獲得到的數據是:", data); }catch(e) { } } window.addEventListener("hashchange", function(e) { console.log("獲得的數據是:", location.hash.substring(1)); }); </script></body></html>

cs2.html 收到消息後通過 parent.location.hash 值來修改 cs1.html 的 hash 值,從而達到數據傳遞。

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>CS2</title></head><body> <script> // http://locahost:8081/cs2.html switch(location.hash) { case "#data": callback(); break; } function callback() { const data = "some number: 1111" try { parent.location.hash = data; }catch(e) { // ie, chrome 下的安全機制無法修改 parent.location.hash // 所以要利用一個中間的代理 iframe var ifrproxy = document.createElement("iframe"); ifrproxy.style.display = "none"; ifrproxy.src = "http://localhost:8080/cs3.html#" + data; // 該文件在請求域名的域下 document.body.appendChild(ifrproxy); } } </script></body></html>

由於兩個頁面不在同一個域下IE、Chrome不允許修改parent.location.hash的值,所以要藉助於 localhost:8080 域名下的一個代理 iframe 的 cs3.html 頁面

<script> parent.parent.location.hash = self.location.hash.substring(1);</script>

之後老規矩,開啟兩個 http 伺服器:

這裡為了圖方便,將 cs1,2,3 都放在同個文件夾下,實際情況的話 cs1.html 和 cs3.html 要與 cs2.html 分別放在不同的伺服器才對。

之後打開瀏覽器訪問 localhost:8080/cs1.html,注意不是 8081,就可以看到獲取到的數據了,此時頁面的 hash 值也已經改變。

當然這種方法存在著諸多的缺點:

  1. 數據直接暴露在了 url 中

  2. 數據容量和類型都有限等等

5. window.name:

window.name(一般在 js 代碼里出現)的值不是一個普通的全局變數,而是當前窗口的名字,這裡要注意的是每個 iframe 都有包裹它的 window,而這個 window 是top window 的子窗口,而它自然也有 window.name 的屬性,window.name 屬性的神奇之處在於 name 值在不同的頁面(甚至不同域名)載入後依舊存在(如果沒修改則值不會變化),並且可以支持非常長的 name 值(2MB)。

舉個簡單的例子:

你在某個頁面的控制台輸入:

window.name = "Hello World";window.location = "http://www.baidu.com";

頁面跳轉到了百度首頁,但是 window.name 卻被保存了下來,還是 Hello World,跨域解決方案似乎可以呼之欲出了:

首先創建 a.html 文件:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>a.html</title></head><body> <script> let data = ""; const ifr = document.createElement("iframe"); ifr.src = "http://localhost:8081/b.html"; ifr.style.display = "none"; document.body.appendChild(ifr); ifr.onload = function() { ifr.onload = function() { data = ifr.contentWindow.name; console.log("收到數據:", data); } ifr.src = "http://localhost:8080/c.html"; } </script></body></html>

之後在創建 b.html 文件:

<script> window.name = "你想要的數據!";</script>

localhost:8080/a.html 在請求遠端伺服器 localhost:8081/b.html 的數據,我們可以在該頁面下新建一個 iframe,該 iframe 的 src 屬性指向伺服器地址,(利用 iframe 標籤的跨域能力),伺服器文件 b.html 設置好 window.name 的值。

但是由於 a.html 頁面和該頁面 iframe 的 src 如果不同源的話,則無法操作 iframe 里的任何東西,所以就取不到 iframe 的 name 值,所以我們需要在 b.html 載入完後重新換個 src 去指向一個同源的 html 文件,或者設置成 "about:blank;" 都行,這時候我只要在 a.html 相同目錄下新建一個 c.html 的空頁面即可。如果不重新指向 src 的話直接獲取的 window.name 的話會報錯:

老規矩,打開兩個 http 伺服器:

打開瀏覽器就可以看到結果:

6. postMessage:

postMessage 是 HTML5 新增加的一項功能,跨文檔消息傳輸(Cross Document Messaging),目前:Chrome 2.0+、Internet Explorer 8.0+, Firefox 3.0+, Opera 9.6+, 和 Safari 4.0+ 都支持這項功能,使用起來也特別簡單。

首先創建 a.html 文件:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>a.html</title></head><body> <iframe src="http://localhost:8081/b.html" style="display: none;"></iframe> <script> window.onload = function() { let targetOrigin = "http://localhost:8081"; window.frames[0].postMessage("我要給你發消息了!", targetOrigin); } window.addEventListener("message", function(e) { console.log("a.html 接收到的消息:", e.data); }); </script></body></html>

創建一個 iframe,使用 iframe 的一個方法 postMessage 可以想 localhost:8081/b.html 發送消息,然後監聽 message,可以獲得其他文檔發來的消息。

同樣的 b.html 文件:

<script> window.addEventListener("message", function(e) { if(e.source != window.parent) { return; } let data = e.data; console.log("b.html 接收到的消息:", data); parent.postMessage("我已經接收到消息了!", e.origin); });</script>

同樣的開啟 http 伺服器:

打開瀏覽器同樣可以看到:

對 postMessage 感興趣的詳細內容可以看看教程:

PostMessage_百度百科

Window.postMessage()

7. document.domain:

對於主域相同而子域不同的情況下,可以通過設置 document.domain 的辦法來解決,具體做法是可以在 example.com/a.htmlsub.example.com/b.html 兩個文件分別加上 document.domain = "a.com";然後通過 a.html 文件創建一個 iframe,去控制 iframe 的 window,從而進行交互,當然這種方法只能解決主域相同而二級域名不同的情況,如果你異想天開的把 script.example.com 的 domain 設為 qq.com 顯然是沒用的,那麼如何測試呢?

測試的方式稍微複雜點,需要安裝 nginx 做域名映射,如果你電腦沒有安裝 nginx,請先去安裝一下: nginx news

先創建一個 a.html 文件:

<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>a.html</title></head><body> <script> document.domain = "example.com"; let ifr = document.createElement("iframe"); ifr.src = "http://sub.example.com/b.html"; ifr.style.display = "none"; document.body.append(ifr); ifr.onload = function() { let win = ifr.contentWindow; alert(win.data); } </script></body></html>

在創建一個 b.html 文件:

<script> document.domain = "example.com"; window.data = "傳送的數據:1111";</script>

之後打開 http 伺服器:

這時候只是開啟了兩個 http 伺服器,還需要通過 nginx 做域名映射,將 Example Domain 映射到 localhost:8080,sub.example.com 映射到 localhost:8081 上

打開操作系統下的 hosts 文件:mac 是位於 /etc/hosts 文件,並添加:

127.0.0.1 www.example.com127.0.0.1 sub.example.com

這樣在瀏覽器打開這兩個網址後就會訪問本地的伺服器。

之後打開 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf,並在 http 模塊里添加:

上面代碼的意思是:如果訪問本地的域名是 Example Domain,就由 localhost:8080 代理該請求。

所以我們這時候在打開瀏覽器訪問 Example Domain 的時候其實訪問的就是本地伺服器 localhost:8080。

最後打開瀏覽器訪問 example.com/a.html 就可以看到結果:

8. flash:

這種方式我沒有嘗試過,不好往下定論,感興趣的話可以上網搜看看教程。

總結:

前面八種跨域方式我已經全部講完,其實講道理,常用的也就是前三種方式,後面四種更多時候是一些小技巧,雖然在工作中不一定會用到,但是如果你在面試過程中能夠提到這些跨域的技巧,無疑在面試官的心中是一個加分項。

上面闡述方法的時候可能有些講的不明白,希望在閱讀的過程中建議你跟著我敲代碼,當你打開瀏覽器看到結果的時候,你也就能掌握到這種方法。


推薦閱讀:

談談 HTTP 緩存
APP精細化HTTP分析(二):響應性能分析與優化
請正確使用http狀態碼,謝謝!
HTTP伺服器的本質:tinyhttpd源碼分析及拓展

TAG:HTTP | 前端开发 | 跨域 |