前端跨域方法論

惆悵客發表於2018-09-07

前言

本著學習和總結的態度寫的技術輸出,文中有任何錯誤和問題,請大家指出。更多的技術輸出可以檢視我的 github部落格

整理了一些前端的學習資源,希望能夠幫助到有需要的人,地址: 學習資源彙總

跨域

跨域指的是協議(protocol ),域名(host),埠號(post)都不相同的資源之間嘗試著進行互動通訊,而由於受瀏覽器同源策略的限制,無法正常進行互動通訊。

最常見的實際場景就是在專案開發過程中,會存在請求第三方其他域下的資源,例如:使用地圖 API 的時候,設定金鑰的時候需要設定白名單才能正常使用地圖 API。

image

使用 AJAX 請求第三方不同域下的資料資源的時候,如果不處理跨域問題,便不能成功傳送 HTTP 請求,且瀏覽器會發出錯誤警告。

前端跨域方法論

同源策略

MDN 解釋: 同源策略限制了從同一個源載入的文件或指令碼如何與來自另一個源的資源進行互動。這是一個用於隔離潛在惡意檔案的重要安全機制。

瀏覽器的同源策略的目的就是為了防止 XSS,CSRF 等惡意攻擊。

同源策略的互動方式有三種:

  • 通常允許跨域寫操作,例如連結,重定向等。
  • 通常允許跨域巢狀資源,例如 img,script 標籤等。
  • 通常不允許跨域讀操作。

跨域場景

只有資源之間的協議,域名和埠號都相同,才是同一個源。

下面是關於同源以及不同源之間的跨域描述。

URL 說明 是否允許通訊
www.demo.com/a.html
www.demo.com/b.html
www.demo.com/c.html
同一域名 允許
www.demo.com/news/a.html
www.demo.com/center/b.ht…
www.demo.com/server/c.ht…
同一域名下的不同資料夾 允許
www.demo.com/a.html
www.demo.com:80/b.html
不同埠號 不允許
www.demo.com/a.html
www.demo.com/b.html
不同協議 不允許
www.demo.com/a.html
www.test.com/b.html
不同域名 不允許
www.demo.com/a.html
test.demo.com/b.html
主域相同,子域不同 不允許

跨域解決方案

1. JSONP

由於瀏覽器同源策略是允許 script 標籤這樣的跨域資源巢狀的,所以 script 標籤的資源不受同源策略的限制。

JSONP 的解決方案就是通過 script 標籤進行跨域請求。

  • 前端設定好回撥函式,並把回撥函式當做請求 url 攜帶的引數。
  • 後端接受到請求之後,返回回撥函式名和需要的資料。
  • 後端響應並返回資料後,返回的資料傳入到回撥函式中並執行。
<!-- 通過原生使用 script 標籤 -->
<script>
    function jsonpCallback(data) {
        alert('獲取到的資料了,開啟控制檯瞧瞧');
        console.log(data);
    }
</script>
<script src="http://127.0.0.1:3000?callback=jsonpCallback"></script>
複製程式碼

也可以使用 AJAX GET 請求方式來跨域請求(axios GET 方式跨域同理)。

<!-- AJAX GET 請求 -->
<script>
    function jsonpCallback(data) {
        alert('獲取到的資料了,開啟控制檯瞧瞧');
        console.log(data);
    }
    $.ajax({
        type: 'GET', // 必須是 GET 請求
        url: 'http://127.0.0.1:3000',
        dataType: 'jsonp', // 設定為 jsonp 型別
        jsonpCallback: 'jsonpCallback' // 設定回撥函式
    })
</script>
複製程式碼

優缺點:

  • 相容性好,低版本的 IE 也支援這種方式。

  • 只能支援 GET 方式的 HTTP 請求。

  • 只支援前後端資料通訊這樣的 HTTP 請求,並不能解決不同域下的頁面之間的資料互動通訊問題。

2. CORS

CORS 跨域資源共享允許在服務端進行相關設定後,可以進行跨域通訊。

服務端未設定 CORS 跨域欄位,服務端會拒絕請求並提示錯誤警告。

前端跨域方法論

服務端設定 Access-Control-Allow-Origin 欄位,值可以是具體的域名或者 '*' 萬用字元,配置好後就可以允許跨域請求資料。

<script>
    $.ajax({
    type: 'post',
    url: 'http://127.0.0.1:3000',
    success: function(res) {
        alert('獲取到的資料了,開啟控制檯瞧瞧');
        console.log(res);
    }
})
</script>
複製程式碼

服務端如何設定跨域欄位? 後端語言設定跨域的方式都不一致,具體可參考後端語言本身的 API。

Node 端設定

res.writeHead(200, {
    'Access-Control-Allow-Origin': '*'
});

// 或者使用了 Express 這樣的框架
res.header("Access-Control-Allow-Origin", "*");
複製程式碼

關於 CORS 的詳細,可以參考這篇筆記,CORS跨域資源共享

3. Server Proxy

通過服務端代理請求的方式也是解決瀏覽器跨域問題的方案。同源策略只是針對瀏覽器的安全策略,服務端並不受同源策略的限制,也就不存在跨域的問題。具體步驟如下:

  • 前端正常請求服務端提供的介面。比如請求介面:http://localhost:3000
  • 通過服務端設定代理髮送請求,請求到資料後再將需要的資料返回給前端。比如設定的代理請求介面是 cnodejs.org/api/v1/topi… ,服務端代理將資料請求回來之後再將資料 http://localhost:3000 介面返回給前端。
// 服務端代理請求程式碼
// 服務端只是簡單的通過正常的 HTTP 請求的方式來代理請求介面資料
// 或者也可以使用 proxy 模組來代理,至於怎麼使用 proxy 模組,待研究完善
var url = 'https://cnodejs.org/api/v1/topics';        
https.get(url, (resp) => {
    let data = "";
    resp.on('data', chunk => {
        data += chunk;
    });
    resp.on('end', () => {
        res.writeHead(200, {
            'Access-Control-Allow-Origin': '*',
            'Content-Type': 'application/json; charset=utf-8'
        });
        res.end(data);
    });
})
複製程式碼

4. location.hash + iframe

location.hash + iframe 跨域通訊的實現是這樣的:

  • 不同域的 a 頁面與 b 頁面進行通訊,在 a 頁面中通過 iframe 嵌入 b 頁面,並給 iframe 的 src 新增一個 hash 值。
  • b 頁面接收到了 hash 值後,確定 a 頁面在嘗試著與自己通訊,然後通過修改 parent.location.hash 的值,將要通訊的資料傳遞給 a 頁面的 hash 值。
  • 但由於在 IE 和 Chrmoe 下不允許子頁面直接修改父頁面的 hash 值,所以需要一個代理頁面,通過與 a 頁面同域的 c 頁面來傳遞資料。
  • 同樣的在 b 頁面中通過 iframe 嵌入 c 頁面,將要傳遞的資料通過 iframe 的 src 連結的 hash 值傳遞給 c 頁面,由於 a 頁面與 c 頁面同域,c 頁面可以直接修改 a 頁面的 hash 值或者呼叫 a 頁面中的全域性函式。

大致流程就是:

前端跨域方法論

a 頁面程式碼

<script>
    var iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = "http://localhost:8081/b.html#data";
    document.body.appendChild(iframe);

    function checkHash() {
        try {
            var data = location.hash ? location.hash.substring(1) : '';
            console.log('獲得到的資料是:', data);
        }catch(e) {}
    }
    window.addEventListener('hashchange', function(e) {
        console.log('監聽到hash的變化:', location.hash.substring(1));
    })
</script>
複製程式碼

b 頁面程式碼

<script>
     switch(location.hash) {
         case '#data':
         callback();
         break;
     }
    function callback() {
        var data = "testHash"
        try {
            parent.location.hash = data;
        }catch(e) {
            var ifrproxy = document.createElement('iframe');
            ifrproxy.style.display = 'none';
            ifrproxy.src = 'http://localhost:8080/c.html#' + data;
            document.body.appendChild(ifrproxy);
        }
    }
 </script>  
複製程式碼

c 頁面程式碼

<script>
    // 修改 a 頁面的 hash 值
    parent.parent.location.hash = self.location.hash.substring(1);
    // 呼叫 a 頁面的全域性函式
    parent.parent.checkHash();
</script>
複製程式碼

優缺點:

  • hash 傳遞的資料容量有限。
  • 資料直接暴露在 url 中。

5. document.domain + iframe

該方案只限於主域相同子域不同的資源跨域解決方案。

實際應用場景:

之前的專案開發中,經常碰到這樣的跨域問題,大致類似於在開發新產品的產品頁中,在沒有正式上線之前,一般都是上傳到內部的測試環境中,比如測試環境的域名是 test.admin.com/xxx/xxx,而專案… consumer-test.admin.com/xxx/xxx 這樣的,產品頁是單獨分離部署上線,再通過 iframe 巢狀到專案中。在內部測試過程中,由於產品頁測試環境和專案測試環境主域相同而子域不同,且產品頁中需要用到專案中定義的全域性公共資源,由於跨域問題,這些公共資源是獲取不到的。

這種場景的跨域解決方案就是利用 document.domain 設定。在產品頁和專案中將 document.domain 設定成相同域就可以實現跨域,巢狀的產品頁就可以訪問父頁面的公共資源了。需要注意的一點就是,document.domain 的設定是有限制的,只能設定成自身或者更高階的父域,且主域必須相同。

專案頁面

<iframe src="test.admin.com/xxx/xxx"></iframe>
<script>
    document.domain = 'admin.com';
</script>
複製程式碼

產品頁

<script>
    // 設定之後就可獲取專案頁面中定義的公共資源了
    document.domain = 'admin.com';
</script>
複製程式碼

6. window.name + iframe

window.name 指的是當前瀏覽器視窗的名稱,預設為空字串,每個視窗的 window.name 都是獨立的。iframe 巢狀的頁面中也有屬於自己的 window 物件,這個 window 是top window 的子視窗,也同樣擁有 window.name 的屬性。

window.name 的獨特之處在於當在頁面設定 window.name 的值,其實就是相當於給這個視窗設定了名稱,而後在這個視窗載入其他頁面(甚至不同域的頁面),window.name 的值依然存在(如果沒有重新設定那麼值不會變化),並且 window.name 的值支援比較大的儲存(2MB)。

例如: 隨便找個頁面開啟控制檯,給當前視窗設定名稱。

window.name = 'test-name';
複製程式碼

設定好之後可以在這個視窗下跳轉到其他頁面

window.location = 'https://www.baidu.com';
複製程式碼

頁面跳轉到了百度首頁,但是 window.name 的值依然是之前設定的值,因為是在一個視窗中跳轉的頁面,視窗名稱並不會被修改。

具體的跨域解決方式如下。

http://localhost:8080/a.htmlhttp://localhost:8081/b.html 跨域通訊,a 頁面通過 iframe 巢狀 b 頁面,b 頁面中設定好 window.name 的值,由於是不同域,a 頁面不能直接訪問到 b 頁面設定的 window.name 的值,需要一個與 a 頁面同域的中間頁來代理作為 a 頁面與 b 頁面通訊的橋樑。

a.html

<script>
	var data = null;
	var state = 0;
	var iframe = document.createElement('iframe');
	iframe.src = "http://localhost:8081/b.html";
	iframe.style.display = 'none';
	document.body.appendChild(iframe);
	
	// 第一次載入先載入 b.html,b.html 設定好了 window.name 的值
	// 而後載入 c.html,c.html 的 window.name 的值就是之前 b.html 設定的值
	// 同域的情況下,a.html 可以通過 iframe.contentWindow.name 獲取到 b.html 中 windoa.name 的值
	iframe.onload = function() {
    	if(state === 0) {
    	    iframe.src = "http://localhost:8080/c.html";
    	    state = 1;
    	}else if(state === 1) {
    	    data = iframe.contentWindow.name;
    	    console.log('收到資料:', data);
    	}
    }
</script>
複製程式碼

b.html

<script>
    window.name = '這是傳遞的資料';
</script>
複製程式碼

中間代理頁,只需要跟 a 頁面保持同域就可以了,例如: http://localhost:8080/c.html

7. window.postMessage

postMessage 是 HTML5 的新特性,用於頁面之間跨域通訊。

postMessage 方法接受兩個必要的引數:

  • message: 需要傳遞的資料。
  • targetOrigin: 資料傳遞的目標視窗域名,值可以是具體的域名或者 '*' 萬用字元。

a.html

<iframe src="http://localhost:8081/b.html" style='display: none;'></iframe>
<script>
	window.onload = function() {
	    var targetOrigin = 'http://localhost:8081';
	    var data = {
	    	name: '武林外傳',
	        time: 2005,
	        length: 81,
	        address: '同福客棧'
	    };
	    // 向 b.html 傳送訊息
	    window.frames[0].postMessage(data, targetOrigin);

	    // 接收 b.html 傳送的資料
	    window.addEventListener('message', function(e) {
	        console.log('b.html 傳送來的訊息:', e.data);
	    })
	}
</script>
複製程式碼

b.html

<script>
	var targetOrigin = 'http://localhost:8080';
	window.addEventListener('message', function(e) {
	    if(e.source != window.parent) {
	        return;
	    }
	    // 接收 a.html 傳送的資料
	    console.log('a.html 傳送來的訊息:', e.data);
	    // 向 a.html 傳送訊息
	    parent.postMessage('哈哈,我是b頁面,我收到你的訊息了', targetOrigin);
	})
</script>
複製程式碼

總結

  • 協議,域名,埠號不相同的資源之間相互通訊,就會產生跨域問題。

  • 處於安全考慮,瀏覽器的同源策略限制了不同域之間相互通訊。

  • JSONP,CORS,Server Proxy 跨域解決方式的應用場景都是用於前後端之間的資料通訊,其他跨域解決方案主要是解決視窗頁面之間的資料通訊。

  • JSONP 只支援 GET 方式的 HTTP 請求。

  • CORS 跨域資源請求需要後端支援。

  • Server Proxy 直接讓後端代理髮送請求。

後記

所有的跨域解決方案都有對應的 DEMO 例項,可在 DEMO 中檢視。想要看執行效果,可以全域性安裝 http-server 模組。

npm install -g http-server
複製程式碼

本著學習和總結的態度寫的技術輸出,文中有任何錯誤和問題,請大家指出。更多的技術輸出可以檢視我的 github部落格

整理了一些前端的學習資源,希望能夠幫助到有需要的人,地址: 學習資源彙總

參考

相關文章