1. 程式人生 > >別慌,不就是跨域麼!

別慌,不就是跨域麼!

(點選上方公眾號,可快速關注)

作者:Neal_yang

github.com/Nealyang/YOU-SHOULD-KNOW-JS/blob/master/doc/basic_js/JavaScript中的跨域總結.md

前端開發中,跨域使我們經常遇到的一個問題,也是面試中經常被問到的一些問題,所以,這裡,我們做個總結。小小問題,不足擔心

什麼是跨域

跨域,是指瀏覽器不能執行其他網站的指令碼。它是由瀏覽器的同源策略造成的,是瀏覽器對JavaScript實施的安全限制。

同源策略限制了一下行為:

  • Cookie、LocalStorage 和 IndexDB 無法讀取

  • DOM 和 JS 物件無法獲取

  • Ajax請求傳送不出去

常見的跨域場景

所謂的同源是指,域名、協議、埠均為相同。

http://www.nealyang.cn/index.html 呼叫 http://www.nealyang.cn/server.php 非跨域

http://www.nealyang.cn/index.html 呼叫 http://www.neal.cn/server.php 跨域,主域不同

http://abc.nealyang.cn/index.html 呼叫 http://def.neal.cn/server.php 跨域,子域名不同

http://www.nealyang.cn:8080/index.html 呼叫 http://www.nealyang.cn/server.php 跨域,埠不同

https://

www.nealyang.cn/index.html 呼叫 http://www.nealyang.cn/server.php 跨域,協議不同

localhost 呼叫 127.0.0.1 跨域

跨域的解決辦法

jsonp跨域

jsonp跨域其實也是JavaScript設計模式中的一種代理模式。在html頁面中通過相應的標籤從不同域名下載入靜態資原始檔是被瀏覽器允許的,所以我們可以通過這個“犯罪漏洞”來進行跨域。一般,我們可以動態的建立script標籤,再去請求一個帶參網址來實現跨域通訊

//原生的實現方式

let script = document.createElement('script');

script.src =

 'http://www.nealyang.cn/login?username=Nealyang&callback=callback';

document.body.appendChild(script);

function callback(res) {

  console.log(res);

}

當然,jquery也支援jsonp的實現方式

$.ajax({

    url:'http://www.nealyang.cn/login',

    type:'GET',

    dataType:'jsonp',//請求方式為jsonp

    jsonpCallback:'callback',

    data:{

        "username":"Nealyang"

    }

})

雖然這種方式非常好用,但是一個最大的缺陷是,只能夠實現get請求

document.domain + iframe 跨域

這種跨域的方式最主要的是要求主域名相同。什麼是主域名相同呢? www.nealyang.cn aaa.nealyang.cn ba.ad.nealyang.cn 這三個主域名都是nealyang.cn,而主域名不同的就不能用此方法。

假設目前a.nealyang.cn 和 b.nealyang.cn 分別對應指向不同ip的伺服器。

a.nealyang.cn 下有一個test.html檔案

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <title>html</title>

    <script type="text/javascript" src = "jquery-1.12.1.js"></script>

</head>

<body>

    <div>A頁面</div>

    <iframe

    style = "display : none"

    name = "iframe1"

    id = "iframe"

    src="http://b.nealyang.cn/1.html" frameborder="0"></iframe>

    <script type="text/javascript">

        $(function(){

            try{

                document.domain = "nealyang.cn"

            }catch(e){}

            $("#iframe").load(function(){

                var jq = document.getElementById('iframe').contentWindow.$

                jq.get("http://nealyang.cn/test.json",function(data){

                    console.log(data);

                });

            })

        })

    </script>

</body>

</html>

利用 iframe 載入 其他域下的檔案(nealyang.cn/1.html), 同時 document.domain 設定成 nealyang.cn ,當 iframe 載入完畢後就可以獲取 nealyang.cn 域下的全域性物件, 此時嘗試著去請求 nealyang.cn 域名下的 test.json (此時可以請求介面),就會發現資料請求失敗了~~ 驚不驚喜,意不意外!!!!!!!

資料請求失敗,目的沒有達到,自然是還少一步:

<!DOCTYPE html>

<html lang="en">

<head>

    <meta charset="UTF-8">

    <title>html</title>

    <script type="text/javascript" src = "jquery-1.12.1.js"></script>

    <script type="text/javascript">

        $(function(){

            try{

                document.domain = "nealyang.com"

            }catch(e){}

        })

    </script>

</head>

<body>

    <div id = "div1">B頁面</div>

</body>

</html>

此時在進行重新整理瀏覽器,就會發現資料這次真的是成功了

window.name + iframe 跨域

window.name屬性可設定或者返回存放視窗名稱的一個字串。他的神器之處在於name值在不同頁面或者不同域下載入後依舊存在,沒有修改就不會發生變化,並且可以儲存非常長的name(2MB)

假設index頁面請求遠端伺服器上的資料,我們在該頁面下建立iframe標籤,該iframe的src指向伺服器檔案的地址(iframe標籤src可以跨域),伺服器檔案裡設定好window.name的值,然後再在index.html裡面讀取改iframe中的window.name的值。完美~

<body>

  <script type="text/javascript">

    iframe = document.createElement('iframe'),

    iframe.src = 'http://localhost:8080/data.php';

    document.body.appendChild(iframe);

    iframe.onload = function() {

      console.log(iframe.contentWindow.name)

    };

  </script>

</body>

當然,這樣還是不夠的。

因為規定如果index.html頁面和和該頁面裡的iframe框架的src如果不同源,則也無法操作框架裡的任何東西,所以就取不到iframe框架的name值了,告訴你我們不是一家的,你也休想得到我這裡的資料。 既然要同源,那就換個src去指,前面說了無論怎樣載入window.name值都不會變化,於是我們在index.html相同目錄下,新建了個proxy.html的空頁面,修改程式碼如下:

<body>

  <script type="text/javascript">

    iframe = document.createElement('iframe'),

    iframe.src = 'http://localhost:8080/data.php';

    document.body.appendChild(iframe);

    iframe.onload = function() {

      iframe.src = 'http://localhost:81/cross-domain/proxy.html';

      console.log(iframe.contentWindow.name)

    };

  </script>

</body>

理想似乎很美好,在iframe載入過程中,迅速重置iframe.src的指向,使之與index.html同源,那麼index頁面就能去獲取它的name值了!但是現實是殘酷的,iframe在現實中的表現是一直不停地重新整理, 也很好理解,每次觸發onload時間後,重置src,相當於重新載入頁面,又觸發onload事件,於是就不停地重新整理了(但是需要的資料還是能輸出的)。修改後程式碼如下:

<body>

  <script type="text/javascript">

    iframe = document.createElement('iframe');

    iframe.style.display = 'none';

    var state = 0;

    iframe.onload = function() {

      if(state === 1) {

          var data = JSON.parse(iframe.contentWindow.name);

          console.log(data);

          iframe.contentWindow.document.write('');

          iframe.contentWindow.close();

        document.body.removeChild(iframe);

      } else if(state === 0) {

          state = 1;

          iframe.contentWindow.location = 'http://localhost:81/cross-domain/proxy.html';

      }

    };

    iframe.src = 'http://localhost:8080/data.php';

    document.body.appendChild(iframe);

  </script>

</body>

所以如上,我們就拿到了伺服器返回的資料,但是有幾個條件是必不可少的:

  • iframe標籤的跨域能力

  • window.names屬性值在文件重新整理後依然存在的能力

location.hash + iframe 跨域

此跨域方法和上面介紹的比較類似,一樣是動態插入一個iframe然後設定其src為服務端地址,而服務端同樣輸出一端js程式碼,也同時通過與子視窗之間的通訊來完成資料的傳輸。

關於錨點相信大家都已經知道了,其實就是設定錨點,讓文件指定的相應的位置。錨點的設定用a標籤,然後href指向要跳轉到的id,當然,前提是你得有個滾動條,不然也不好滾動嘛是吧。

而location.hash其實就是url的錨點。比如http://www.nealyang.cn#Nealyang的網址開啟後,在控制檯輸入location.hash就會返回#Nealyang的欄位。

基礎知識補充完畢,下面我們來說下如何實現跨域

如果index頁面要獲取遠端伺服器的資料,動態的插入一個iframe,將iframe的src執行伺服器的地址,這時候的top window 和包裹這個iframe的子視窗是不能通訊的,因為同源策略,所以改變子視窗的路徑就可以了,將資料當做改變後的路徑的hash值載入路徑上,然後就可以通訊了。將資料加在index頁面地址的hash上, index頁面監聽hash的變化,h5的hashchange方法

<body>

  <script type="text/javascript">

    function getData(url, fn) {

      var iframe = document.createElement('iframe');

      iframe.style.display = 'none';

      iframe.src = url;

      iframe.onload = function() {

        fn(iframe.contentWindow.location.hash.substring(1));

        window.location.hash = '';

        document.body.removeChild(iframe);

      };

      document.body.appendChild(iframe);

    }

    // get data from server

    var url = 'http://localhost:8080/data.php';

    getData(url, function(data) {

      var jsondata = JSON.parse(data);

      console.log(jsondata.name + ' ' + jsondata.age);

    });

  </script>

</body>

補充說明:其實location.hash和window.name都是差不多的,都是利用全域性物件屬性的方法,然後這兩種方法和jsonp也是一樣的,就是隻能夠實現get請求

postMessage跨域

這是由H5提出來的一個炫酷的API,IE8+,chrome,ff都已經支援實現了這個功能。這個功能也是非常的簡單,其中包括接受資訊的Message時間,和傳送資訊的postMessage方法。

傳送資訊的postMessage方法是向外界視窗傳送資訊

otherWindow.postMessage(message,targetOrigin);

otherWindow指的是目標視窗,也就是要給哪一個window傳送訊息,是window.frames屬性的成員或者是window.open方法建立的視窗。 Message是要傳送的訊息,型別為String,Object(IE8、9不支援Obj),targetOrigin是限定訊息接受範圍,不限制就用星號 *

接受資訊的message事件

var onmessage = function(event) {

  var data = event.data;

  var origin = event.origin;

}

if(typeof window.addEventListener != 'undefined'){

    window.addEventListener('message',onmessage,false);

}else if(typeof window.attachEvent != 'undefined'){

    window.attachEvent('onmessage', onmessage);

}

舉個栗子

a.html(http://www.nealyang.cn/a.html)

<iframe id="iframe" src="http://www.neal.cn/b.html" style="display:none;"></iframe>

<script>

    var iframe = document.getElementById('iframe');

    iframe.onload = function() {

        var data = {

            name: 'aym'

        };