JS跨域筆記

原文連接javascript

因項目本地開發時,調用API都是涉及到跨域的問題,而如今前端工程化,前端構建工具會集成跨域的功能,所以也沒有深刻地區探究跨域的問題,而本身自己也對跨域問題還有一些模糊之處,所以決定寫下這篇文章,督促本身瞭解的同時,也是作個記錄,方便之後回顧。html

本文目錄結構:

  1. 什麼是跨域
  2. 現階段跨域的解決方案及案例
  3. 最佳實踐

1、什麼是跨域

1. 跨域的兩個誤區
  1. 動態請求就會有跨域問題
  2. 跨域就是請求發不出去

對於誤區1,跨域僅僅存在於瀏覽器端,不存在於其餘環境;前端

對於誤區2,只要網絡沒有問題,全部跨域的請求都是能正常發送出去,而且服務端也能收到請求並正常返回結果,只是因爲跨域限制,被瀏覽器攔截了。java

這也是爲何咱們用postman等代理工具模擬請求時,能夠獲取到返回信息;node

若是是非簡單請求,(除GET,POST,HEAD以外,且http頭信息不超出一下字段:Accept、Accept-LanguageContent-LanguageLast-Event-IDContent-Type(限於三個值:application/x-www-form-urlencodedmultipart/form-datatext/plain)),都會先發出預請求(preflight),預請求詢問服務端該請求容許跨域否,接着服務端會返回只有headers不含body的信息,而後瀏覽器根據headers中的信息進行判斷,如果容許跨域,則再次發送請求,不然拋出跨域限制的錯誤。程序員

2. 爲何跨域僅僅限制讀取遠端的數據

若是限制寫入端(也就是發送請求端),那麼服務器的資源僅僅只能同源請求,沒法作到資源共享。web

3. 瀏覽器如何識別一個請求是否跨域

同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。json

若是兩個頁面的協議(如:http,https)、域名或ip(如:binnera.com.cn)、端口(通常web網站都是默認80端口)都相同,則兩個頁面具備相同的源,而只要其中任意一個不一樣,則瀏覽器則會將這兩個源之間的請求視爲跨域。前端工程化

咱們以http://www.binenar.com.cn爲例,進行具體說明跨域

連接 結果 緣由
http://www.binenar.com.cn/blog 同協議同域名同端口(默認80端口)
http://www.binenar.com.cn/blog 同協議同域名同端口
http://www.binenar.com.cn:81/blog 同協議同域名不一樣端口
https://www.binenar.com.cn 協議不一樣
http://binenar.com.cn/blog 域名不一樣(若是有作域名映射,那麼兩個域名能夠指向同一個ip)
http://b.binenar.com.cn/blog 域名不一樣
4. 瀏覽器跨域限制主要限制了什麼
  1. 不一樣的源沒法讀取對方的CookieLocalStorageIndexDB
  2. 沒法獲取DOM,BOM
  3. JS沒法獲取AJAX以及Fetch請求的結果。
5. 瀏覽器容許的跨域資源請求

瀏覽器容許嵌入跨域資源的請求

  • <script src="..."></script>標籤嵌入跨域腳本;
  • <link rel="stylesheet" href="..."> 標籤嵌入CSS,CSS的跨域須要一個設置正確的Content-Type 消息頭;
  • <img src="...">嵌入圖片;
  • <video><audio>嵌入多媒體資源;
  • @font-face 引入的字體;
  • <frame><iframe> 載入的任何資源,可經過設置X-Frame-Options消息頭來阻止iframe嵌入資源。

2、現階段跨域的解決方案及案例

1. 跨域資源共享(CORS)

若是是簡單請求,請求發送出去時,瀏覽器會在請求頭添加Origin字段:

Origin: http://binnear.com.cn
複製代碼

告訴服務端該請求是來自那個源。

接着服務端接受到請求後,並在響應頭加上以下字段:

Access-Control-Allow-Origin: http://binnear.com.cn
複製代碼

表明服務端容許的域,瀏覽器收到後,便會容許這次請求,以上字段若被設置爲*,則表示可接受任意的源訪問。

若是是非簡單請求:

const url = 'http://binnear.com.cn/data';
const xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
複製代碼

瀏覽器則會發送預請求,在請求頭會添加如下字段:

OPTIONS /data HTTP/1.1
Origin: http://binnear.com.cn
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
複製代碼

OPTIONS是預請求的識別字段,Access-Control-Request-Method列出請求方法,Access-Control-Request-Headers指定發送的額外的頭信息。

服務端收到預請求後,檢查請求的字段後,確認容許跨域,便作出響應,

Access-Control-Allow-Origin: http://binnear.com.cn
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
複製代碼

響應頭中會包含以上三個字段,正好對應咱們請求頭中添加的字段,表示容許的域,容許的請求方法,以及額外的請求頭。瀏覽器接受到後,知道此次請求已被許可,接着發送真正的請求。

2. JSONP跨域

相信每個接觸過跨域的程序員都或多或少了解過JSONP跨域,它的原理也很簡單,就是利用上面咱們提過的嵌入跨域資源請求的方法。

<script type='text/javascript'>
    function localFn(data) {
        console.log('這是獲取到的遠程數據':data)
    }
    const script = document.createElement('script');
    script.src = 'http://binnear.com.cn?callback=localFn';
    document.body.appendChild(script);
</script>
複製代碼
  1. 首先咱們定義了一個全局函數localFn;
  2. 建立一個script標籤,並將src屬性指向咱們須要跨域請求的API;
  3. 將建立的script標籤添加到頁面

經過以上3步,咱們發送上面所示的API請求,而後服務端會返回一段可執行的JS代碼:

localFn({remark: '我是遠程數據對象裏面的屬性值'})
複製代碼

由於咱們以前定義了全局的localFn,因此這段代碼就會執行localFn這個函數,並將數據傳遞給形參data,在localFn內咱們就經過data獲取到服務端的數據了。

注意點:crc中的localFn能夠爲任意名,callback這個key是由接口提供者所定義。

3. 基於iframe的跨域
  1. 經過window.name傳輸跨域資源
  2. 經過window.postMessage傳輸跨域資源

講述以上方法以前咱們先本地配置一下本地跨域模擬環境

sever1.js配置以下:

const http = require('http');
const fs = require('fs');
const documentRoot = 'D:/code/sever1/';

const server = http.createServer(function (req, res) {
    const url = req.url;
    const file = documentRoot + url;
    fs.readFile(file, function (err, data) {
        if (err) {
            res.writeHeader(404, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });
}).listen(8888);

console.log('服務器開啓成功');
複製代碼

sever2.js的配置與sever1.js大體相同,只不過咱們將文件路徑更改成domain2的路徑,監聽的端口號改成了8889

const http = require('http');
const fs = require('fs');
const documentRoot = 'D:/code/sever2/';

const server = http.createServer(function (req, res) {
    const url = req.url;
    const file = documentRoot + url;
    fs.readFile(file, function (err, data) {
        if (err) {
            res.writeHeader(404, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });

}).listen(8889);

console.log('服務器開啓成功');
複製代碼

domain1.html配置

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>domain1</title>
</head>
<body>
  <div>this is domain 1</div>
</body>
</html>
複製代碼

domain2.html配置

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>domain1</title>
</head>
<body>
  <div>this is domain 2</div>
</body>
</html>
複製代碼

接着打開兩個命令行工具,分別進入sever1和sever2文件夾,執行如下命令:

node ./sever1.js
node ./sever2.js
複製代碼

接着進入到瀏覽器中,輸入以下連接

http://localhost:8888/domain1.html
http://localhost:8888/domain2.html
複製代碼

頁面輸出this is domain 1this is domain 2則啓動成功,到此咱們前期的準備已經完成,接下來咱們利用搭建好的環境來模擬window.name如何進行跨域傳輸數據。

window.name

在domain1中咱們添加如下代碼:

<script>
	window.name = JSON.stringify({info: 'this is domain1\'s name'})
</script>
複製代碼

咱們在domain1中,將一段json字符串賦值給了domain1中window的name屬性。

接着咱們在domain2中添加以下代碼

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);

    iframe.onload = () => {
      console.log(iframe.contentWindow.name)
    }
</script>
複製代碼

保存後,咱們進入http://localhost:8889/domain2.html這個頁面,而後刷新,打開控制檯:

咦,竟然被跨域限制了,不是說好window.name傳輸跨域資源的嗎?怎麼仍是被限制了呢?

不要急,猜測下,咱們是否是忽略了什麼事情,而後翻閱資源後,發現當前文件的所在的源與iframe的src指向的源不一樣,那麼就沒法操做iframe中的任何東西,天然window.name也就沒法讀取了。

原來是iframe的跨域限制了,那麼問題來了,既然這樣,window.name那不就是沒有辦法跨域傳輸數據了嗎?

對於上面問題,window.name自身提供了的一個神奇功能,給了咱們跨域傳輸的可能:那就是window.name的值在不一樣頁面或域下,加載後依然存在。

結合這個功能咱們再來優化一下咱們在domain2.html中新加的代碼:

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);

    iframe.onload = () => {
      iframe.src = 'about:blank' // 新增代碼
      console.log(iframe.contentWindow.name)
    }
</script>
複製代碼

咱們新增了一行代碼,就是在iframe加載完後,立馬將scr指向domain2的源,這個時候iframe就與domain2的源一致了,咱們就能讀取到了iframe下的window.name屬性了,並且由於window.name的神奇的功能,它的值依然是咱們在domain1中設置的值。很好,成功彷佛在向咱們招手了,保存文件,瀏覽器打開domain2的連接,刷新。

window.name的數據咱們確實獲取到了,可是控制卻在不停地輸出日誌,仔細思考一下,發現是iframescr從新指向後,便觸發了onload,致使進入死循環,再次優化,同時避免404的error,代碼在次優化以下:

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);
    let state = 0;
    iframe.onload = () => {
      if (state === 0) {
        state = 1
        iframe.src = ''
      }
      if (state === 1) {
        console.log(iframe.contentWindow.name)
        document.body.removeChild(iframe);
      }
    }
</script>
複製代碼

保存後在次刷新頁面,咱們終於如願以償地獲得了咱們但願的結果:

沒有了死循環,數據正常獲取,只是惟一不舒服的地方就是仍然有跨域限制的報錯,怎麼消除這個error,就留給愛探索的你了~~

小記:window.name能夠攜帶的信息限制爲2M。

window.postMessager

咱們依舊用sever1和sever2文件夾的內容,刪除關於window.name的相關代碼,接着咱們在domain1.html中添加以下代碼:

<script>
    window.addEventListener('message', function (e) {
      console.log('data from domain1 ---> ' + e.data);
      const data = { info: 'this is domain2' }
      window.parent.postMessage(JSON.stringify(data), 'http://localhost:8889');
    }, false);
</script>
複製代碼

domain2.html中添加以下代碼

<script>
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://localhost:8888/domain1.html';
    document.body.appendChild(iframe);
    iframe.onload = function () {
      const data = { info: 'this is domain 1' };
      iframe.contentWindow.postMessage(JSON.stringify(data), 'http://localhost:8888/');
    };
    window.addEventListener('message', function (e) {
      console.log('data from domain2 ---> ' + e.data);
    }, false);
</script>
複製代碼

domain2.html中,咱們經過iframedomain1.html引入進來,在iframe加載完成後,咱們經過iframe中的postMessage方法將data數據發送給domain1.html所在的域,同時監聽domain2.html中的message事件

接着咱們在domain1.html中監聽domain1.html中的message事件,獲取到domain2.html傳送過來的data,同時將零一份data經過domain2.html中的postMessage方法發送給domain2.html所在的域。

保存後,刷新domain2.html所在的頁面,在控制檯中咱們能夠看到以下信息。

這樣咱們就完成了在domain2.html中取domain1.html中的數據,並能夠作到二者之間的交互。

4. 服務端代理

前文咱們有說過,跨域僅僅瀏覽器端限制了咱們讀取遠程的數據,因此利用這一點,咱們能夠將跨域資源由服務端代理後,再將資源返回給咱們,

5. WebSocket協議跨域

WebSocket protocol是HTML5一種新的協議,它實現了瀏覽器端與服務端的雙工通訊,同時容許跨域通信,這裏不作敘述,有興趣的能夠去了解一下~

相關文章
相關標籤/搜索