原文連結
因項目本地開發時,調用API都是涉及到跨域的問題,而現在前端工程化,前端建構工具會內建跨域的功能,是以也沒有深入地區探究跨域的問題,而自己本身也對跨域問題還有一些模糊之處,是以決定寫下這篇文章,督促自己了解的同時,也是做個記錄,友善以後回顧。
本文目錄結構:
- 什麼是跨域
- 現階段跨域的解決方案及案例
- 最佳實踐
一、什麼是跨域
1. 跨域的兩個誤區
- 動态請求就會有跨域問題
- 跨域就是請求發不出去
對于誤區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
為例,進行具體說明
連結 | 結果 | 原因 |
---|---|---|
/blog | 是 | 同協定同域名同端口(預設80端口) |
/blog | 是 | 同協定同域名同端口 |
/blog | 否 | 同協定同域名不同端口 |
| 否 | 協定不同 |
/blog | 否 | 域名不同(如果有做域名映射,那麼兩個域名可以指向同一個ip) |
/blog | 否 | 域名不同 |
4. 浏覽器跨域限制主要限制了什麼
- 不同的源無法讀取對方的
、Cookie
和LocalStorage
;IndexDB
- 無法擷取
,DOM
;BOM
- JS無法擷取
以及Fetch請求的結果。AJAX
5. 浏覽器允許的跨域資源請求
浏覽器允許嵌入跨域資源的請求
-
标簽嵌入跨域腳本;<script src="..."></script>
-
标簽嵌入CSS,CSS的跨域需要一個設定正确的Content-Type 消息頭;<link rel="stylesheet" href="..." target="_blank" rel="external nofollow" >
-
嵌入圖檔;<img src="...">
-
和<video>
嵌入多媒體資源;<audio>
-
引入的字型;@font-face
-
和<frame>
載入的任何資源,可通過設定X-Frame-Options消息頭來阻止iframe嵌入資源。<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>
複制代碼
- 首先我們定義了一個全局函數
;localFn
- 建立一個
标簽,并将script
屬性指向我們需要跨域請求的API;src
- 将建立的
标簽添加到頁面script
通過以上3步,我們發送上面所示的API請求,然後服務端會傳回一段可執行的JS代碼:
localFn({remark: '我是遠端資料對象裡面的屬性值'})
複制代碼
因為我們之前定義了全局的
localFn
,是以這段代碼就會執行
localFn
這個函數,并将資料傳遞給形參
data
,在
localFn
内我們就通過
data
擷取到服務端的資料了。
注意點:
crc
中的
localFn
可以為任意名,
callback
這個
key
是由接口提供者所定義。
3. 基于iframe的跨域
- 通過
傳輸跨域資源window.name
- 通過
傳輸跨域資源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
WebSocket protocol是HTML5一種新的協定,它實作了浏覽器端與服務端的雙工通信,同時允許跨域通訊,這裡不做叙述,有興趣的可以去了解一下~