[禿破前端面試] —— 跨域實踐總結

前言

年前年後跳槽季,準備從面試內容入手看看前端相關知識點,旨在探究一個系列知識點,能力範圍以內的深刻探究一下。重在實踐,針對初級前端和準備面試的同窗,爭取附上實際的代碼例子以及相關試題~系列名字就用【禿破前端面試】—— 由於圈內你們共識,技術與髮量成正比。😄但願你們早日 禿 破瓶頸~html

關於面試題或者某個知識點的文章太多了,這裏筆者只是想把我的的總結用代碼倉庫的形式記錄下來並輸出文章,畢竟理論不等於實踐,知其然也要知其因此然,實踐用過才能真正理解~前端

相關係列同類型文章:node

其餘類型:jquery

什麼是跨域

今天這篇咱們來好好講講跨域實踐~爲何要加上實踐,由於跨域這東西,相信你們理論上看得足夠多了,若是做爲面試來講,可能說出來幾個方案就夠了,面試官也不會讓實際寫代碼,可是你真的使用過嗎?你真的瞭解其中的實現原理嗎?基於此觀點,寫了以下這篇實踐爲主的跨域文章。git

具體來說,看上面的文章《Web安全相關》應該大致瞭解了。若是沒了解,在這裏就再簡要概述一下。github

在前端頁面請求 url 地址的時候,該 url 與瀏覽器上的 url 地址必須處於同域上,也就是域名、端口以及協議三者相同。若是其中任何一個不一樣,就屬於跨域範疇。面試

直接代碼截圖來看更爲直觀:ajax

// express 起了一個小型服務,而且寫了一個接口 /list
app.get('/list', (req, res) => {
  const list = [
    {
      name: 'luffy',
      age: 20,
      email: 'luffy@163.com' 
    }, {
      name: 'naruto',
      age: 24,
      email: 'naruto@qq.com'
    }
  ]
  res.status(200).json(list);
});
複製代碼

瀏覽器訪問一下:express

再寫一個html頁面調用這個接口:npm

<script>
  window.onload = function() {
    const xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:3000/list');
    xhr.onreadystatechange = function () { 
      if (xhr.readyState === 4 && xhr.status === 200) {
        const resData = JSON.parse(xhr.responseText);
        console.log(resData);
      }
    };
    xhr.send();
  }
</script>
複製代碼

能夠看到,這就是跨域,相信剛學前端又不太懂後臺的小夥伴常常會見到。

跨域:簡而言之,咱們一般所說的跨域就是指在瀏覽器同源策略的限制下,瀏覽器不容許執行其餘網站的腳本。

解決跨域的方式

解決跨域的方式多種多樣,不過其實說白了,咱們平時用到的也就那麼兩三種,可是既然是總結,咱們就把各類奇淫技巧都整理一下~

我的以爲,在團隊項目開發過程當中,前端並非很適合作跨域處理,大部分場景跨域都應該是由後端處理的,因此這裏也只是簡單討論這個跨域方案的發展歷程。

最流行的跨域解決方案 —— CORS

當下項目中若是涉及到跨域,實際上都應該是後端經過設置 CORS 來解決的。CORS 是目前最主流的跨域解決方案,跨域資源共享(CORS) 是一種機制,它使用額外的 HTTP 頭來告訴瀏覽器 讓運行在一個 origin (domain) 上的Web應用被准許訪問來自不一樣源服務器上的指定的資源。

// 在node端設置一下請求頭,容許接收全部源地址的請求
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', '*');
複製代碼

重啓服務,刷新一下頁面:

能夠看到,獲取到了數據,跨域解決完成~

通常來講簡單點,Node.js 能夠直接使用社區成熟的 cors 方案

最經典的跨域解決方案 —— JSONP

接下來要說的就是 JSONP 解決方案,說它最經典一點都不爲過,雖然如今大部分項目並不會使用它來解決跨域,可是隻要是面試,涉及到跨域,基本都會問到這個知識點。

原理,非同源限制的標籤

同源限制是跨域的本質,也就是沒有同源限制這麼個東西,那麼也就不存在跨域了。事實上,存在一些標籤沒有同源限制 —— <script>/<link>/<img>。JSONP 利用的原理就是這些標籤來解決跨域的問題。

第一步:假設後臺有一個接口 /jsonp/list

// 按鈕獲取數據
<button onclick="loadJsonpData()">JSONP獲取數據</button>

<script>
  function loadJsonpData() {
    const script = document.createElement('script');
    script.src='http://localhost:3000/jsonp/list';
    document.body.appendChild(script);
  }
</script>
複製代碼

點擊按鈕,就會向<body>標籤內部插入一個<script>標籤,瀏覽器遇到<script>就會執行裏面的內容。關鍵點,瀏覽器會執行腳本里面的內容。

第二步:先後端約定好執行函數的名稱

第一步提到過了,把指定的 url 經過<script>標籤加載到頁面裏,會執行腳本里面的內容。那麼想一下咱們跨域請求的目的 —— 獲取數據。也就是說,裏面的內容應該是個可執行函數,而且把咱們想要的數據傳遞過來。所以,如今先後端須要約定一個執行函數的名稱!

假設這邊約定該函數名稱爲callbackData

第三步:前端定義callbackData

約定好了執行函數名稱,前端就得定義它,由於後臺返回的是一段可執行代碼,若是前端沒定義,就會報callbackData undefined的錯誤。

// 定義 callback 函數,獲取後臺的 data
function callbackData(data) {
    console.log(data, 98989);
}
複製代碼

第四步:後臺返回攜帶數據的可執行代碼

先後端約定好了名稱,而且前端定義好了函數,參數是想要拿到的數據,後臺只須要把數據包在執行函數裏響應回去就能夠了。

注意,JSONP 的接口不一樣於正常接口,它返回的不是 json 格式的數據,而是一段可執行字符串,這個字符串會被前端執行。

app.get('/jsonp/list', (req, res) => {
  const list = [
    {
      name: 'luffy',
      age: 20,
      email: 'luffy@163.com' 
    }, {
      name: 'naruto',
      age: 24,
      email: 'naruto@qq.com'
    }
  ]
  // 把數據塞進執行函數裏面
  const resData = `callbackData(${JSON.stringify(list)})`;
  res.send(resData); // 這裏不能使用res.json而是res.send
});
複製代碼

咱們來執行一下看看:

能夠看到,點擊按鈕,瀏覽器 Network JS 會請求新插入的<script>的地址,該地址響應的內容是事先定義好的callbackData(resData)

而在前端,由於定義了callbackData(data),因此控制檯能夠看到,打印了後臺響應過來的內容。

上面就是 jsonp 的基本過程,不知道給你們解釋沒解釋清楚,其實真的很簡單,只不過之前在看的時候感受全部人講的都很官方,並無實際操做,讓不少人會誤解或者看不懂,這裏我就經過實際代碼來說解,相信會很容易理解~

事實上,jsonp 還能夠進行封裝,而後能夠實現的很漂亮~哈哈。好比jquery就內置支持 jsonp。

// jquery jsonp
$.ajax({
	url: "http://cross-domain/get_data",
	dataType: "jsonp", // 指定服務器返回的數據類型
	jsonp: "callback", // 指定參數名稱
	jsonpCallback: "callbackName" // 指定回調函數
}).done(function(resp_data) {
	console.log("Ajax Done!");
});
複製代碼

這裏我就不封裝了,由於懂得原理就好了,如今的前端應該不多使用了,只用來面試了。

另外,雖然我常用的是<script>標籤,可是其實<img>標籤也是能夠的。

最簡單的跨域解決方案 —— NGINX

這個就很少作介紹了,說實話,並不算先後端跨域解決方案,而是屬於運維層級的,而且若是面試被問到跨域相關,面試官應想獲得的應該也不是這個答案。

一個簡易版 NGINX 解決跨域配置大概以下:

server
{
    listen 3003;
    server_name localhost;
    location /ok {
        proxy_pass http://localhost:3000;

        # 指定容許跨域的方法,*表明全部
        add_header Access-Control-Allow-Methods *;

        # 預檢命令的緩存,若是不緩存每次會發送兩次請求
        add_header Access-Control-Max-Age 3600;
        # 帶cookie請求須要加上這個字段,並設置爲true
        add_header Access-Control-Allow-Credentials true;

        # 表示容許這個域跨域調用(客戶端發送請求的域名和端口) 
        # $http_origin動態獲取請求客戶端請求的域 不用*的緣由是帶cookie的請求不支持*號
        add_header Access-Control-Allow-Origin $http_origin;

        # 表示請求頭的字段 動態獲取
        add_header Access-Control-Allow-Headers 
        $http_access_control_request_headers;

        # OPTIONS預檢命令,預檢命令經過時才發送請求
        # 檢查請求的類型是否是預檢命令
        if ($request_method = OPTIONS){
            return 200;
        }
    }
}
複製代碼

理論上的跨域解決方案 —— window.name

說它是理論上的跨域解決方案也就是說它確實能實現跨域傳遞數據,可是卻不多被應用。 查閱了一下,MDN 是這麼說的,(如 SessionVars 和 Dojo's dojox.io.windowName ,該屬性也被用於做爲 JSONP 的一個更安全的備選,來提供跨域通訊(cross-domain messaging)。可是這倆框架我也確實孤陋寡聞了,仍是有應用的而且兼容性仍是很好的,除了萬年不變的 IE 不必定支持,其餘的瀏覽器都支持。

咱們先來簡單瞭解一下什麼是window.name

  • 每一個瀏覽器窗口(Tab頁)都有獨立的window.name與之對應
  • 在一個窗口(Tab頁)的從打開到關閉以前,窗口載入的全部頁面同時共享一個window.name,該窗口下每一個頁面對window.name都有讀寫的權限。
  • window.name是當前窗口(Tab頁)的屬性,並不會由於頁面跳轉而發生改變。
  • window.name容量大概是2MB,存儲格式爲字符串

提及來略顯蒼白,仍是來實際例子看看吧。

上圖,在http://127.0.0.1:3006設置了window.name = aaaaa,以後頁面跳轉到了http://127.0.0.1:3008,根據瀏覽器同源策略,這是跨域 場景,而也正確拿到了上一個頁面設置的window.name。上面提到了,每個窗口共享,那麼若是是不一樣端口呢?

// 代碼改爲新窗口打開
<a target='_blank' href='http://127.0.0.1:3008/'>跳轉到3008端口</a>
複製代碼

能夠看到,window.name確實是窗口(Tab頁)之間獨立的,新窗口打開,window.name初始化是空字符串。

上面這兩張圖則是window.name的存儲形式,設置了Array aObject b,可見window.name在存儲的時候會調用該對象自身的toString()以後再存儲。

固然,若是面試官真的問到這裏了,這麼回答其實也沒什麼大問題,可是仍是存在瑕疵的,由於存在特殊狀況,就是 ES6 新增的基本類型Symbol

實際使用window.name進行跨域數據獲取

上面說了那麼多,其實都是簡單的介紹window.name特性及使用方法,事實上,並無涉及到跨域獲取數據,好比說第一個 Demo,我在 A 設置 window.name 而後跳轉到 B 能拿到 A 設置的值,這叫跨域嗎?我跳轉的時候把值加在參數上豈不是更方便,因此並非實際場景。下面咱們就來一個實際場景:

【問題描述】: 存在兩個不一樣域頁面 A 和 B, 經過 window.name 實現 A 加載的時候獲取到 B 頁面設置的 window.name

先來思考一下,B 頁面作的事情無非就是把數據設置在window.name裏,那麼咱們加載 A 頁面的時候去 B 頁面獲取,這個時候又不能先跳轉到 B 而後再回到 A,由於獲取數據是一個異步不刷新頁面的場景。嗯,說到這差很少就知道了,確定是得經過 iframe 來實現了,也就是 A 頁面開一個同域的 iframe —— 假設是 proxy.html,咱們稱之爲中轉頁,中轉頁內咱們再打開 B 頁面,獲取 B 的window.name事先設置好的 data 便可。整個過程大體以下:

下面是代碼部分:

http://127.0.0.1:3006/a-data.html -> A 頁面
http://127.0.0.1:3006/a-data.html -> proxy 數據中轉頁面 -> 只是個空頁面
http://127.0.0.1:3008/b-data.html -> B 頁面
___________________________________________

// b-data.html
<script>
  const data = [
    {
      name: 'luffy',
      age: 20
    }, {
      name: 'naruto',
      age: 22
    }
  ]
  window.onload = function() {
    window.name = JSON.stringify(data);
  }
</script>

// a-data.html
<script>
  const currentDomain = 'http://127.0.0.1:3006'; // 當前域
  const corssDomain = 'http://127.0.0.1:3008'; // 跨域
  window.onload = function() {
    let flag = false; // 是否獲取數據
    iframe = document.createElement('iframe'),
    loadData = ()=> {
      if (flag) {
        // 讀取B的數據
        let data = iframe.contentWindow.name;    
        console.log(data, 66666);
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
      } else {
          flag = true;
          // 加載同域代理文件
          iframe.contentWindow.location = `${currentDomain}/proxy.html`; 
      }  
    };
    iframe.src = `${corssDomain}/b-data.html`;
    if (iframe.attachEvent) {
      iframe.attachEvent('onload', loadData);
    } else {
      iframe.onload  = loadData;
    }
    document.body.appendChild(iframe);
  }
</script>
複製代碼

從上圖能夠看到,A 獲取到了 B 頁面傳遞過來的數據,怎麼說呢,太複雜了,須要增長一個 iframe 而且還須要一箇中轉頁,因此 -> 綜上所述, 我的以爲,window.name確實是個理論解決方案,跨域必須依賴window.name的種種特性,可是必須是在瀏覽器同一窗口下進行,且須要配合iframe以及同域中轉頁。

傳說中的嫡系方案 HTML5 postMessage

這算是一個比較高級的方式了,爲何呢?由於前面的 JSONP 或者 window.name,是奇思妙想而來的,人家官方設計它並非拿來進行跨域的或者說是鑽了設計的漏洞,至於 CORS 和 NGINX 則是非前端範疇。因此 HTML5 出了這個 postMessage 專門用來正正經經作「安全」跨域的,親兒子和撿來的兒子的區別能同樣嗎😂?可是也不知道爲啥,我以爲可能這種方式使用的也比較少吧,或許我孤陋寡聞,可是確實沒怎麼看人用過。

原理otherWindow.postMessage(message, targetOrigin, [transfer]);

postMessage 的原理是依賴於一個其餘窗口的引用,而這個引用能夠是window.open()返回的,也能夠是一個 iframe 的 contentWindow,還能夠是命名事後有索引的window.frames,因此可能在其餘地方你也會看到 postMessage 和 iframe 在一塊兒使用。

postMessage使用起來也是比較簡單,而且官方文檔介紹的也是比較詳細,感興趣的能夠仔細閱讀閱讀,我這裏直接就實踐上代碼了:

// A頁面

let opener;
function openB () {
    opener = window.open('http://127.0.0.1:3008/index.html')
}

// 發送消息
function postMsg() {
    const msg = document.getElementById('chatB').value;
    opener.postMessage(msg, "http:/127.0.0.1:3008");
}
複製代碼

A 頁面的邏輯很簡單,就是咱們經過 A 頁面使用window.open()打開 B,而後使用獲取到的 targetWindow 進行兩個頁面間的通訊。

// B 頁面
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
    // Chrome瀏覽器兼容
    const origin = event.origin || event.originalEvent.origin; 
    if (origin !== "http://127.0.0.1:3006") {
      return;
    }
    const { data } = event; // 獲取到 A 的數據
    // 下面是你的邏輯
    ...
}
複製代碼

在 B 頁面,咱們監聽message事件,而後判斷是不是目標域,若是是目標域,獲取到數據進行操做。

這裏強調一下爲何說安全,由於 A 與 B 進行通訊以前,都會判斷是不是否是目標域,若是不是是不會進行操做的,是前端開發者可控的狀態。

咱們來看一下效果:

只能用一句哎呦,不錯呦~來形容了,既然已經完成了 A 跟 B 發消息,那麼就送佛送到西,咱把 B 到 A 的也寫完,也算是一個簡易版聊天系統了。

OK,看起來仍是很不錯的,畢竟官方方案,弄的很成熟,並且發送數據也能夠是多種多樣的格式,這應該是最早進的了。可是,原諒我,即便是寫完了這個小 Demo,我也仍是想象不到它的真實貼切的使用場景,局域網聊天?有可能,你若是跟我說實時通訊,那我確定是不信的,由於下面還會介紹更牛的大佬~若是有人用得多,或者真實場景使用過,能夠留言交流下,讓我漲漲見識😄

其餘跨域解決方案

document.domain

這個就更沒啥人用了,也只是存在於書本上或者文檔裏,由於場景比較侷限限制比較大。他的限制卻是很少,可是限制性很大。想要使用這種方式實現跨域,A與B必須知足以下條件:

A: http://aaa.xxx.com:port
B: http://bbb.xxx.com:port
複製代碼

也就是,A 與 B 的一級域名相同,二級域名不一樣,而且協議和端口號也必須相同。

在知足上述條件基礎之上,兩個頁面彼此設置window.domain = 'xxx.com',就能夠進行通訊了。。。原諒我窮B一個沒有域名給你們展現了。可是說實話也不必,這種方式何須呢。。。

WebScoket

好了,真正的大佬在這裏,來了,上面再講 postMessage 的時候,作了個 A 和 B 聊天的小 Demo,也說到了,若是真的是聊天通信場景,大佬級別的確定是 Webscoket 啊。

爲啥 Websocket 能處理跨域?

這個問題該怎麼說呢。首先,把 Websocket 放在這裏其實算是做弊了,爲何呢?由於咱們所說的跨域是指,瀏覽器和服務端基於 HTTP 協議進行通訊時出現同源限制了。而 Websocket 根本就不是基於 HTTP 協議的,它是位於 TCP/IP 上層,跟 HTTP 協議同層的瀏覽器通訊協議。

如今寫個文章太難了,還得會畫畫😂

它大概是上面這個樣子的,Webscoket 與 HTTP 是同層協議,因此 HTTP 的限制對於 Webscoket 來講,人家根本不鳥你,同級關係,你憑啥管我~可是呢,還有個小箭頭,是指 WebSocket 在創建握手鍊接時(TCP三次握手),數據是經過 HTTP 協議傳輸的,可是在創建鏈接以後,真正的數據傳輸階段是不須要 HTTP 協議參與的。關於 Websocket 的這裏就不涉及太多了,由於本文是說跨域的,既然上面寫了個通信,那麼就拿 Websoket 一樣寫一個來看看區別,先上效果圖(簡單的客戶端和服務端聊天):

能夠看到,Websocket 的重要之處其實並不在於解決跨域,事實上應該也沒人用它解決跨域。它的重要之處在於,它提供了客戶端主動與服務端主動推送消息的能力。若是是使用 HTTP,咱們通常都只是,客戶端發起一個請求,服務端響應這個請求,沒辦法作到彼此主動推送消息,所以,若是不使用 Websocket 的時候,通常都是經過一個 AJAX 長輪訓,設置定時器不斷的去發送請求更新數據,這樣作其實浪費性能而且也不是很優雅。因此 Websocket 歸納起來的優勢就是:

  • 沒有同源限制,不跨域
  • 全雙工通訊,雙端都能主動推送消息
  • 實時性更好,靈活性更高

Websocket 的適用場景也就是那些實時性很高的應用,好比通信類,股票基金類,基於位類等應用。

筆者瞭解的並非不少,更多相關信息請查閱官方文檔以及其餘相關文章。

相關題目

1. 什麼是跨域,爲何會跨域

2. 說說解決跨域的幾種方式

3. 說說 JSONP 的實現原理及過程

總結

雖然上面羅列了那麼多跨域解決方案,但實際上仍是 CORS 和 JSONP 這兩種是最經常使用的,而且面試中也常常被深問。

那麼對比一下 CORS 和 JSONP:

CORS JSONP
優勢 比較簡便,既支持 post 又支持 get 利用的原生標籤特性,兼容性特別好
缺點 低版本IE不兼容 只支持 get 方式,而且須要先後端約定

寫到這裏跨域相關的實踐總結基本上是寫完了,好累啊,由於除了原理還要想場景寫代碼,確實不容易,但願能對你們有所幫助吧~

代碼地址👇這裏

若是以爲還不錯,點個 star 和贊不勝感激。

相關文章
相關標籤/搜索