天天看點

JS跨域筆記

原文連結

因項目本地開發時,調用API都是涉及到跨域的問題,而現在前端工程化,前端建構工具會內建跨域的功能,是以也沒有深入地區探究跨域的問題,而自己本身也對跨域問題還有一些模糊之處,是以決定寫下這篇文章,督促自己了解的同時,也是做個記錄,友善以後回顧。

本文目錄結構:

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

一、什麼是跨域

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

對于誤區1,跨域僅僅存在于浏覽器端,不存在于其他環境;

對于誤區2,隻要網絡沒有問題,所有跨域的請求都是能正常發送出去,并且服務端也能收到請求并正常傳回結果,隻是由于跨域限制,被浏覽器攔截了。

這也是為什麼我們用

postman

等代理工具模拟請求時,可以擷取到傳回資訊;

如果是非簡單請求,(除

GET

,

POST

,

HEAD

之外,且http頭資訊不超出一下字段:

Accept、Accept-Language

Content-Language

Last-Event-ID

Content-Type

(限于三個值:

application/x-www-form-urlencoded

multipart/form-data

text/plain

)),都會先發出預請求(

preflight

),預請求詢問服務端該請求允許跨域否,接着服務端會傳回隻有

headers

不含

body

的資訊,然後浏覽器根據

headers

中的資訊進行判斷,若是允許跨域,則再次發送請求,否則抛出跨域限制的錯誤。

2. 為什麼跨域僅僅限制讀取遠端的資料

如果限制寫入端(也就是發送請求端),那麼伺服器的資源僅僅隻能同源請求,無法做到資源共享。

3. 浏覽器如何識别一個請求是否跨域
  • 浏覽器識别跨域是基于同源政策

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

如果兩個頁面的協定(如:

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. 不同的源無法讀取對方的

    Cookie

    LocalStorage

    IndexDB

  2. 無法擷取

    DOM

    ,

    BOM

  3. JS無法擷取

    AJAX

    以及Fetch請求的結果。
5. 浏覽器允許的跨域資源請求

浏覽器允許嵌入跨域資源的請求

  • <script src="..."></script>

    标簽嵌入跨域腳本;
  • <link rel="stylesheet" href="..." target="_blank" rel="external nofollow" >

    标簽嵌入CSS,CSS的跨域需要一個設定正确的Content-Type 消息頭;
  • <img src="...">

    嵌入圖檔;
  • <video>

    <audio>

    嵌入多媒體資源;
  • @font-face

    引入的字型;
  • <frame>

    <iframe>

    載入的任何資源,可通過設定X-Frame-Options消息頭來阻止iframe嵌入資源。

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

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(, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });
}).listen();

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(, {
                'content-type': 'text/html;charset="utf-8"'
            });
            res.write('<h1>404錯誤</h1><p>你要找的頁面不存在</p>');
            res.end();
        } else {
            res.write(data);
            res.end();
        }
    });

}).listen();

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 1

this 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

的資料我們确實擷取到了,但是控制卻在不停地輸出日志,仔細思考一下,發現是

iframe

scr

重新指向後,便觸發了

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

中,我們通過

iframe

domain1.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一種新的協定,它實作了浏覽器端與服務端的雙工通信,同時允許跨域通訊,這裡不做叙述,有興趣的可以去了解一下~