合格前端系列第一彈-跨域 頂 原

一、什麼是跨域

由於瀏覽器對安全訪問因素的考慮,是不允許js跨域調用其他頁面的,這裏的域我們把它想象成域名,如,一個域名爲https://www.oschina.net,另外一個域名爲https://www.zhihu.com,這兩者屬於不同的域名,它們之間的頁面也是不能相互調用的,它屬於同源策略所定義限制中的一種。同源策略具體分爲以下幾類:

  • 不同域名
  • 相同域名不同端口號,如https://www.oschina.net:8000和https://www.oschina.net:8001
  • 同一個域名不同協議,如http://www.oschina.net和https://www.oschina.net
  • 域名和域名對應的的IP,如http://b.qq.com和 http://10.198.7.85
  • 主域和子域,如http://www.oschina.net和https://test.oschina.net
  • 子域和子域,如https://test1.oschina.net和https://test2.oschina.net


以上情況只要出現了,那麼就會產生跨域問題。那麼如果解決跨域問題呢,下面的小節會總結一些解決跨域常用的方法。

二、跨域解決方案

1、JSONP

對於JSONP,有個通俗易懂的解釋-JSONPJSON with Padding)是數據格式JSON的一種“使用模式”,可以讓網頁從別的網域要數據。
由於同源策略,如上所述。但是(中國人講話是很有文化的),HTML的 <script>元素是一個例外,它並不遵循同源策略,利用 <script>元素的這個開放策略,網頁可以得到從其他來源動態產生的JSON數據,而這種使用模式就是所謂的 JSONP。用JSONP抓到的數據並不是JSON,而是任意的JavaScript,用 JavaScript解釋器運行而不是用JSON解析器解析。來來來,我來舉個栗子🌰吧
前端瀏覽器頁面

<script>
function jsonpCallBack (res, req) {
  console.log(res, req);
}
</script>
<script type="text/JavaScript" src="http://localhost/test/jsonp.php?callback=jsonpCallBack&data=getJsonpData"></script>

另一個域名服務器請求接口

<?php
  /*後端獲取請求字段數據,並生成返回內容*/
  $data = $_GET["data"];
  $callback = $_GET["callback"];
  echo $callback."('success', '".$data."')";
?>

測試結果如下


這種方案需要注意的是他支持GET這一種HTTP請求類型,還有尤爲重要的就是其他域要有一定可靠性,不然你的網站會GG的。當然有時我們還可以通過一個方法來動態生成需要的JSONP

2、跨域資源共享(CORS-Cross Origin Resource Sharing)

CORS,它是JSONP模式的現代升級版,與JSONP不同的是,CORS除了GET要求方法以外也支持其他的 HTTP要求。瀏覽器CORS請求分成兩種
a、簡單請求
b、協商模型/預檢請求(Preflighted Request),即非簡單請求
如何區分請求具體屬於哪一種呢,下面我總結了幾點:
1) 請求方式

  • GET
  • HEAD
  • POST

2)HTTP的頭信息子段

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限於三個值application/x-www-form-urlencoded、multipart/form-data、text/plain,其中'text/plain'默認支持,其他兩種則需要預檢請求和服務器協商。

滿足以上兩大點的即爲簡單請求,否則爲非簡單請求。具體請求處理的不同,大家可以去查閱下MDN https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS ,那裏有詳細的解析及用法。

3、document.domain+iframe(適用於主域名相同的情況)

從上面的同源策略我們可以知道,瀏覽器這邊是認爲主域和子域、子域和子域,它們屬於不同的域,那麼我們如果需要讓主域和子域之間可以進行通信,需要做的就是通過修改document.domain,把它們改成相同的domain

在域名爲server.example.com中的a.html

document.domain = 'example.com';
var $iframe = document.createElement('iframe');
$iframe.src = 'server.child.example.com/b.html';
$iframe.style.display = 'none';
document.body.appendChild($iframe);
$iframe.onload = function(){
    var doc = $iframe.contentDocument || $iframe.contentWindow.document;
    //在這裏操作doc,也就是操作b.html
    $iframe.onload = null;
};

在域名爲server.child.example.com中的b.html

document.domain = 'example.com'

這種形式方便歸方便,但也有其方便帶來的隱患

  • 安全性,當一個站點被攻擊後,另一個站點會引起安全漏洞。
  • 若頁面中引入多個iframe,要想操作所有iframe,domain需要全部設置成一樣的。

4、window.name + iframe

window 對象的name屬性是一個很特別的屬性,它可以在不同頁面甚至不同域名加載後依舊存在。使用步驟如下:
step1 - 首先在頁面A中利用iframe加載其他域中的頁面B
step2 - 在頁面B中將需要傳遞的數據賦給window.name
step3 - iframe加載完成後,頁面A中修改iframe地址,將其變成同一個域下的地址,然後獲取iframe中頁面B的window.name 屬性

示例代碼如下:
首先我們在域名爲http://127.0.0.1下建立好B頁面,在B頁面的<script>標籤中將需要傳遞的數據賦給window.name

window.name = '頁面B中傳遞給頁面A的數據';

然後我們域名爲http://127.0.0.1:9000的A頁面,這裏我們需要做的一件事就是利用iframe加載頁面B,並將其域名進行修改,變成和頁面A一樣的域名。

function proxy (url, callback) {
    var flag = true,
        $iframe = document.createElement('iframe'),
        loadCallBack = function () {
            if (flag) {
                // 這裏我們還得在域名爲 http://127.0.0.1:9000 建立一個tmp.html文件當做緩存界面
                $iframe.contentWindow.location = 'http://127.0.0.1:9000/tmp.html';
                flag = false;
            }
            // 修改localtion後,每次觸發onload事件會重置src,相當於重新載入頁面,然後繼續觸發onload。
            // 這裏是針對該問題做的處理
            else {
                callback($iframe.contentWindow.name);
                $iframe.contentWindow.close();
                document.body.removeChild($iframe);
                $iframe.src = '';
                $iframe = null;
            }
        };

    $iframe.src = url;
    $iframe.style.display = 'none';
    // 事件綁定兼容簡單處理
    // IE 支持iframe的onload事件,不過是隱形的,需要通過attachEvent來註冊
    if ($iframe.attachEvent) {
        $iframe.attachEvent('onload', loadCallBack);
    }
    else {
        $iframe.onload = loadCallBack;
    }

    document.body.appendChild($iframe);
}
proxy('http://127.0.0.1/bop/test.html', function(data){
    console.log(data);
});

測試結果如圖

5、HTML5中的postMessage(適用於兩個iframe或兩個頁面之間)

postMessage隸屬於html5,但是它支持IE8+和其他瀏覽器,可以實現同域傳遞,也能實現跨域傳遞。它包括髮送消息postMessage和接收消息message功能。

postMessage調用語法如下

otherWindow.postMessage(message, targetOrigin, [transfer]);
  • otherWindow : 其他窗口的一個引用,比如iframe的contentWindow屬性、執行window.open返回的窗口對象、或者是命名過或數值索引的window.frames。
  • message : 將要發送到其他 window的數據,類型爲string或者object。
  • targetOrigin : 通過窗口的origin屬性來指定哪些窗口能接收到消息事件,其值可以是字符串"*"(表示無限制)或者一個URI。
  • transfer (可選) : 一串和message 同時傳遞的 Transferable 對象。

接收消息message 的屬性有:

  • data :從其他 window 中傳遞過來的數據。
  • origin :調用 postMessage  時消息發送方窗口的 origin 。
  • source :對發送消息的窗口對象的引用。

示例如下:域名http://127.0.0.1:9000頁面A通過iframe嵌入了http://127.0.0.1頁面B,接下來頁面A將通過postMessage對頁面B進行數據傳遞,頁面B將通過message屬性接收頁面A的數據

頁面A發送消息代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>頁面A</title>
</head>
<body>
    <h1>hello jsonp</h1>
    <iframe src="http://127.0.0.1/b.html" id="iframe"></iframe>
</body>
</html>
<script>
window.onload = function() {  
    var $iframe = document.getElementById('iframe');  
    var targetOrigin = "http://127.0.0.1";  
    $iframe.contentWindow.postMessage('postMessage發送消息', targetOrigin);  
}; 
</script>

頁面B接收消息代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>頁面B</title>
</head>
<body>
    <h1>hello jsonp</h1>
</body>
</html>
<script>
var onmessage = function (event) {
  var data = event.data;     //消息
  var origin = event.origin; //消息來源地址
  var source = event.source; //源Window對象
  if(origin === "http://127.0.0.1:9000"){
    console.log(data, origin, source);
  }
};
// 事件兼容簡單處理
if (window.addEventListener) {
  window.addEventListener('message', onmessage, false);
}
else if (window.attachEvent) {
  window.attachEvent('onmessage', onmessage);
}
else {
  window.onmessage = onmessage;
}
</script>

運行結果如下

6、location.hash + iframe(適用於兩個iframe之間)

對於location.hash,我們先看一張圖先

相信看完圖,大家也大概清楚了location.hash到底是用來幹啥子的。沒錯,它可以用來獲取或設置頁面的標籤值 如http://127.0.0.1:9000/#hello ,它的location.hash值則爲'#hello'。它一般用於瀏覽器錨點定位,HTTP請求過程中卻不會攜帶hash,所以這部分的修改不會產生HTTP請求,但是會產生瀏覽器歷史記錄,這對我們進行跨域通信給予了幫助。我們可以通過修改URL的hash部分來進行雙向通信。

示例如下:域名http://127.0.0.1:9000頁面A通過iframe嵌入了http://127.0.0.1頁面B,接下來頁面A和頁面B將通過location.hash進行雙向通信。

頁面A代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>頁面A</title>
</head>
<body>
    <iframe src="http://127.0.0.1/bop/test.html#locationHash" id="ifr"></iframe>
</body>
</html>

頁面B代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>頁面B</title>
</head>
<body>
    <h1>hello localtionHash</h2>
</body>
</html>
<script>
try {
    parent.location.hash = 'data';
} catch (e) {
    // ie、chrome的安全機制無法修改parent.location.hash,所以要藉助於父窗口域名下的一個代理iframe
    var $ifrproxy = document.createElement('iframe');
    $ifrproxy.style.display = 'none';
    // 注意proxy.html必須是域名爲 http://127.0.0.1:9000 下的頁面
    $ifrproxy.src = "http://127.0.0.1:9000/proxy.html#locationHashChange";
    document.body.appendChild($ifrproxy);
}
</script>

代理頁面代碼

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Proxy頁面</title>
</head>
<body>
    
</body>
<script>
// 因爲parent.parent和自身屬於同一個域,所以可以改變其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
</script>
</html>

運行結果如圖

以上內容便是我本博客的所有內容了,希望多多少少可以幫助小夥伴們去更好的理解跨域,當然,不對的地方,還請小夥伴們輕噴哦 ^_^

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章