跨域,你需要知道的全在這里


原文地址

跨域,你需要知道的全在這里

這篇文章是之前寫的,我重新整理了下,閱讀本文前,希望你有一定的 JS/Node 基礎,這里不另外介紹如何使用 Ajax 做異步請求,如果不了解,可以先看:

Ajax知識體系大梳理

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

  1. 介紹常見的跨域的解決方案以及其優缺點
  2. 模擬實際的跨域場景,在每種方案后給出一個簡單的實例,能夠讓讀者和我一起敲代碼,直觀地理解這些跨域技巧

如果覺得本文有幫助,可以點 star 鼓勵下,本文所有代碼都可以從 github 倉庫下載,讀者可以按照下述打開:

git clone https://github.com/happylindz/blog.git
cd blog/code/crossOrigin/
yarn 

建議你 clone 下來,方便你閱讀代碼,跟我一起測試。

同源策略

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

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

跨域方法

今天一共介紹七種常用的跨域技巧,關於跨域技巧大致可以分為 iframe 跨域和 API 跨域請求。

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

1. JSONP:

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

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

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

jsonp 之所以能夠跨域的關鍵在於頁面調用 JS 腳本是不受同源策略的影響,相當於向后端發起一條 http 請求,跟后端約定好函數名,后端拿到函數名,動態計算出返回結果並返回給前端執行 JS 腳本,相當於是一種 "動態 JS 腳本"

接下來我們通過一個實例來嘗試:

后端邏輯:

// jsonp/server.js const url = require('url'); require('http').createServer((req, res) => { const data = { x: 10 }; // 拿到回調函數名 const callback = url.parse(req.url, true).query.callback; console.log(callback); res.writeHead(200); res.end(`${callback}(${JSON.stringify(data)})`); }).listen(3000, '127.0.0.1'); console.log('啟動服務,監聽 127.0.0.1:3000');

前端邏輯:

// jsonp/index.html
<script>  function jsonpCallback(data) {  alert('獲得 X 數據:' + data.x);  } </script> <script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>

然后在終端開啟服務:

之所以能用腳本指令,是因為我在 package.json 里面設置好了腳本命令:

{
  // 輸入 yarn jsonp 等於 "node ./jsonp/server.js & http-server ./jsonp"
  "scripts": {
    "jsonp": "node ./jsonp/server.js & http-server ./jsonp",
    "cors": "node ./cors/server.js & http-server ./cors",
    "proxy": "node ./serverProxy/server.js",
    "hash": "http-server ./hash/client/ -p 8080 & http-server ./hash/server/ -p 8081",
    "name": "http-server ./name/client/ -p 8080 & http-server ./name/server/ -p 8081",
    "postMessage": "http-server ./postMessage/client/ -p 8080 & http-server ./postMessage/server/ -p 8081",
    "domain": "http-server ./domain/client/ -p 8080 & http-server ./domain/server/ -p 8081"
  },
  // ...
}
yarn jsonp
// 因為端口 3000 和 8080 分別屬於不同域名下
// 在 localhost:3000 查看效果,即可收到后台返回的數據 10

打開瀏覽器訪問 localhost:8080 即可看到獲取到的數據。

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

優點:

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

缺點:

  1. 它支持 GET 請求而不支持 POST 等其它類行的 HTTP 請求。
  2. 它只支持跨域 HTTP 請求這種情況,不能解決不同域的兩個頁面或 iframe 之間進行數據通信的問題
  3. 無法捕獲 Jsonp 請求時的連接異常,只能通過超時進行處理

CORS:

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

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

因此,實現 CORS 通信的關鍵是服務器。只要服務器實現了 CORS 接口,就可以跨域通信。

前端邏輯很簡單,只要正常發起 ajax 請求即可:

// cors/index.html
<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>

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

// cors/server.js require('http').createServer((req, res) => { res.writeHead(200, { 'Access-Control-Allow-Origin': 'http://localhost:8080', 'Content-Type': 'text/html;charset=utf-8', }); res.end('這是你要的數據:1111'); }).listen(3000, '127.0.0.1'); console.log('啟動服務,監聽 127.0.0.1:3000');

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

然后我們執行命令:yarn cors 打開瀏覽器訪問 localhost:3000 即可看到效果:

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

CORS 的優缺點:

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

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

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

3. 服務端代理:

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

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

代碼如下:

// serverProxy/server.js 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');

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

啟動服務 yarn proxy 並訪問 http://localhost:3000/topics 即可看到效果:

跨域請求成功。純粹的獲取跨域獲取后端數據的請求的方式已經介紹完了,另外介紹四種通過 iframe 跨域與其它頁面通信的方式。

location.hash:

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

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

// hash/client/index.html 對應 localhost:8080/index.html
<script>  let ifr = document.createElement('iframe');  ifr.style.display = 'none';  ifr.src = "http://localhost:8081/data.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>

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

// hash/server/data.html 對應 localhost:8081/data.html
<script>  switch(location.hash) {  case "#data":  callback();  break;  }  function callback() {  const data = "data.html 的數據"  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/proxy.html#' + data; // 該文件在 client 域名的域下  document.body.appendChild(ifrproxy);  }  } </script>

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

// hash/client/proxy.html 對應 localhost:8080/proxy.html
<script>  parent.parent.location.hash = self.location.hash.substring(1); </script>

之后啟動服務 yarn hash,即可在 localhost:8080 下觀察到:

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

  1. 數據直接暴露在了 url 中
  2. 數據容量和類型都有限等等

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,跨域解決方案似乎可以呼之欲出了:

前端邏輯:

// name/client/index.html 對應 localhost:8080/index.html 
<script>  let data = '';  const ifr = document.createElement('iframe');  ifr.src = "http://localhost:8081/data.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/proxy.html";  } </script>

數據頁面:

// name/server/data.html 對應 localhost:8081/data.html
<script>  window.name = "data.html 的數據!"; </script>

localhost:8080index.html 在請求數據端 localhost:8081/data.html 時,我們可以在該頁面新建一個 iframe,該 iframe 的 src 指向數據端地址(利用 iframe 標簽的跨域能力),數據端文件設置好 window.name 的值。

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

之后運行 yarn name 即可看到效果:

6.postMessage

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

前端邏輯:

// postMessage/client/index.html 對應 localhost:8080/index.html
<iframe src="http://localhost:8081/data.html" style='display: none;'></iframe> <script>  window.onload = function() {  let targetOrigin = 'http://localhost:8081';  window.frames[0].postMessage('index.html 的 data!', targetOrigin);  }  window.addEventListener('message', function(e) {  console.log('index.html 接收到的消息:', e.data);  }); </script>

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

數據端邏輯:

// postMessage/server/data.html 對應 localhost:8081/data.html
<script>  window.addEventListener('message', function(e) {  if(e.source != window.parent) {  return;  }  let data = e.data;  console.log('data.html 接收到的消息:', data);  parent.postMessage('data.html 的 data!', e.origin);  }); </script>

啟動服務:yarn postMessage 並打開瀏覽器訪問:

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

PostMessage_百度百科
Window.postMessage()

7.document.domain

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

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

前端邏輯:

// domain/client/index.html 對應 sub1.example.com/index.html
<script>  document.domain = 'example.com';  let ifr = document.createElement('iframe');  ifr.src = 'http://sub2.example.com/data.html';  ifr.style.display = 'none';  document.body.append(ifr);  ifr.onload = function() {  let win = ifr.contentWindow;  alert(win.data);  } </script>

數據端邏輯:

// domain/server/data 對應 sub2.example.com/data.html
<script>  document.domain = 'example.com';  window.data = 'data.html 的數據!'; </script>

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

127.0.0.1 sub1.example.com
127.0.0.1 sub2.example.com

之后打開 nginx 的配置文件:/usr/local/etc/nginx/nginx.conf,並在 http 模塊里添加,記得輸入 nginx 啟動 nginx 服務:

/usr/local/etc/nginx/nginx.conf
http {
    // ...
    server {
        listen 80;
        server_name sub1.example.com;
        location / {
            proxy_pass http://127.0.0.1:8080/;
        }
    }
    server {
        listen 80;
        server_name sub2.example.com;
        location / {
            proxy_pass http://127.0.0.1:8081/;
        }
    }
    // ...
}

相當於是講 sub1.example.com 和 sub2.example.com 這些域名地址指向本地 127.0.0.1:80,然后用 nginx 做反向代理分別映射到 8080 和 8081 端口。

這樣訪問 sub1(2).example.com 等於訪問 127.0.0.1:8080(1)

啟動服務 yarn domain 訪問瀏覽器即可看到效果:

總結:

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM