原文連接javascript
因項目本地開發時,調用API都是涉及到跨域的問題,而如今前端工程化,前端構建工具會集成跨域的功能,所以也沒有深刻地區探究跨域的問題,而本身自己也對跨域問題還有一些模糊之處,所以決定寫下這篇文章,督促本身瞭解的同時,也是作個記錄,方便之後回顧。html
對於誤區1,跨域僅僅存在於瀏覽器端,不存在於其餘環境;前端
對於誤區2,只要網絡沒有問題,全部跨域的請求都是能正常發送出去,而且服務端也能收到請求並正常返回結果,只是因爲跨域限制,被瀏覽器攔截了。java
這也是爲何咱們用postman
等代理工具模擬請求時,能夠獲取到返回信息;node
若是是非簡單請求,(除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
中的信息進行判斷,如果容許跨域,則再次發送請求,不然拋出跨域限制的錯誤。程序員
若是限制寫入端(也就是發送請求端),那麼服務器的資源僅僅只能同源請求,沒法作到資源共享。web
同源策略限制了從同一個源加載的文檔或腳本如何與來自另外一個源的資源進行交互。這是一個用於隔離潛在惡意文件的重要安全機制。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 |
否 | 域名不一樣 |
Cookie
、LocalStorage
和 IndexDB
;DOM
,BOM
;AJAX
以及Fetch請求的結果。瀏覽器容許嵌入跨域資源的請求
<script src="..."></script>
標籤嵌入跨域腳本;<link rel="stylesheet" href="...">
標籤嵌入CSS,CSS的跨域須要一個設置正確的Content-Type 消息頭;<img src="...">
嵌入圖片;<video>
和 <audio>
嵌入多媒體資源;@font-face
引入的字體;<frame>
和 <iframe>
載入的任何資源,可經過設置X-Frame-Options消息頭來阻止iframe嵌入資源。若是是簡單請求,請求發送出去時,瀏覽器會在請求頭添加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
複製代碼
響應頭中會包含以上三個字段,正好對應咱們請求頭中添加的字段,表示容許的域,容許的請求方法,以及額外的請求頭。瀏覽器接受到後,知道此次請求已被許可,接着發送真正的請求。
相信每個接觸過跨域的程序員都或多或少了解過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
標籤,並將src
屬性指向咱們須要跨域請求的API;script
標籤添加到頁面經過以上3步,咱們發送上面所示的API請求,而後服務端會返回一段可執行的JS代碼:
localFn({remark: '我是遠程數據對象裏面的屬性值'})
複製代碼
由於咱們以前定義了全局的localFn
,因此這段代碼就會執行localFn
這個函數,並將數據傳遞給形參data
,在localFn
內咱們就經過data
獲取到服務端的數據了。
注意點:crc
中的localFn
能夠爲任意名,callback
這個key
是由接口提供者所定義。
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(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 1
和this is domain 2
則啓動成功,到此咱們前期的準備已經完成,接下來咱們利用搭建好的環境來模擬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。
咱們依舊用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
中的數據,並能夠作到二者之間的交互。
前文咱們有說過,跨域僅僅瀏覽器端限制了咱們讀取遠程的數據,因此利用這一點,咱們能夠將跨域資源由服務端代理後,再將資源返回給咱們,
WebSocket
協議跨域WebSocket protocol是HTML5一種新的協議,它實現了瀏覽器端與服務端的雙工通訊,同時容許跨域通信,這裏不作敘述,有興趣的能夠去了解一下~