文章首發javascript
跨域是平常開發中常常開發中常常會接觸到的一個重難點知識,何不總結實踐一番,今後心中對之了無牽掛。html
之因此會出現跨域解決方案,是由於同源策略的限制。同源策略規定了若是兩個 url 的協議、域名、端口中有任何一個不等,就認定它們跨源了。好比下列表格列出和 http://127.0.0.1:3000
比較的同源檢測的結果,前端
url | 結果 | 緣由 |
---|---|---|
http://127.0.0.1:3000/index | 同源 | |
https://127.0.0.1:3000 | 跨源 | 協議不一樣 |
https://localhost:3000 | 跨源 | 域名不一樣 |
http://127.0.0.1:3001 | 跨源 | 端口不一樣 |
那跨源有什麼後果呢?概括有三:不能獲取 Cookie、LocalStorage、IndexedDB;不能獲取 dom 節點;不能進行通常的 Ajax 通訊;跨域解決方案的出現就是爲了解決以上痛處。java
提到 JSONP 跨域,不得不先提到 script
標籤,和 img
、iframe
標籤相似,這些標籤是不受同源策略限制的,JSONP 的核心就是經過動態加載 script 標籤來完成對目標 url 的請求。node
先來看一段 JSONP 調用的 Headers
部分,字段以下:react
Request URL:http://127.0.0.1:3000/?callback=handleResponse
Request Method:GET
Status Code:200 OK
Remote Address:127.0.0.1:3000
複製代碼
能夠很鮮明地發如今 Request URL
中有一句 ?callback=handleResponse
,這個 callback 後面跟着的 handleResponse 即回調函數名(能夠任意取),服務端會接收到這個參數而後拼接成形如 handleResponse(JSON)
的形式返還給前端(這也是 JSONP == JSON with padding 的緣由吧),以下圖,這時候瀏覽器就會自動調用咱們事先定義好的 handleResponse 函數。git
前端代碼示例:(源爲 http://127.0.0.1:3001)github
function handleResponse(res) {
console.log(res) // {text: "jsonp"}
}
const script = document.createElement('script')
script.src = 'http://127.0.0.1:3000?callback=handleResponse'
document.head.appendChild(script)
複製代碼
服務端代碼示例:(源爲 http://127.0.0.1:3000)chrome
const server = http.createServer((req, res) => {
if (~req.url.indexOf('?callback')) { // 簡單處理 JSONP 跨域的時候
const obj = {
"text": 'jsonp',
}
const callback = req.url.split('callback=')[1]
const json = JSON.stringify(obj)
const build = callback + `(${json})`
res.end(build) // 這裏返還給前端的是拼接好的 JSON 對象
}
});
複製代碼
能夠看出 JSONP 具備直接訪問響應文本的優勢,可是要想確認 JSONP 是否請求失敗並不容易,由於 script 標籤的 onerror 事件還未獲得瀏覽器普遍的支持,此外它僅能支持 GET 方式調用。json
CORS(Cross-Origin Resource Sharing) 能夠理解爲增強版的 Ajax,也是目前主流的跨域解決方案。它的核心思想即前端與後端進行 Ajax 通訊時,經過自定義 HTTP 頭部設置從而決定請求或響應是否生效
。
好比前端代碼(url 爲 http://127.0.0.1:3001)寫了段 Ajax,代碼以下:
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
console.log('responseTesx:' + xhr.responseText)
}
}
}
xhr.open('get', 'http://127.0.0.1:3000', true)
xhr.send()
複製代碼
由於端口不一致的關係這時候致使不一樣源了,這時候會在 Request Headers 中發現多了這麼一行字段,
Origin: http://127.0.0.1:3001
複製代碼
並且控制檯中會報出以下錯誤:
Failed to load http://127.0.0.1:3000/: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://127.0.0.1:3001' is therefore not allowed access.
複製代碼
這時候就須要在服務端設置字段 Access-Control-Allow-Origin
,它的做用就是設置容許來自什麼源的請求,若是值設置爲 *
,代表容許來自任意源的請求。服務端代碼示例以下:
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001') // 設置容許來自 http://127.0.0.1:3001 源的請求
})
複製代碼
CORS 分爲簡單請求以及非簡單請求。能夠這麼區分,若是請求方法爲 POST
、GET
、HEAD
時爲簡單請求,其它方法如 PUT
、DELETE
等爲非簡單請求,若是是非簡單請求的話,能夠在 chrome 的 Network 中看到多了一次 Request Method
爲 OPTIONS
的請求。以下圖:
能夠把這個請求稱爲預請求,用白話文翻譯下,瀏覽器詢問服務器,'服務器大哥,我此次要進行 PUT 請求,你給我發張通行證唄',服務器大哥見瀏覽器小弟這麼殷勤,因而給了它發了張通行證,叫做 Access-Control-Allow-Methods:PUT
,接着瀏覽器就能愉快地進行 PUT 請求了。服務端代碼示例以下:
http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001')
res.setHeader('Access-Control-Allow-Methods', 'http://127.0.0.1:3001')
})
複製代碼
聊完簡單請求和非簡單請求的區別後,再來看看如何利用 CORS 實現 Cookie 的跨域傳送,首先在服務器隨意設置個 Cookie 值下發到瀏覽器,若是非跨域的狀況下,瀏覽器再次請求服務器時就會帶上服務器給的 Cookie,可是跨域的時候怎麼辦呢?不賣關子了,需在服務端設置 Access-Control-Allow-Credentials
字段以及在客戶端設置 withCredentials
字段,二者缺一不可,代碼以下:
前端代碼示例:(源爲 http://127.0.0.1:3001)
const xhr = new XMLHttpRequest()
...
xhr.withCredentials = true // 傳 cookie 的時候前端要作的
xhr.open('get', 'http://127.0.0.1:3000', true)
xhr.send()
複製代碼
服務端代碼示例: (源爲 http://127.0.0.1:3000)
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:3001') // 必填:接受域的請求
res.setHeader('Set-Cookie', ['type=muyy']) // 下發 cookie
res.setHeader('Access-Control-Allow-Credentials', true) // ② 選填:是否容許瀏覽器傳 cookie 到服務端,只能設置爲 true
res.end('date from cors')
})
複製代碼
至此介紹了幾個比較關鍵 HTTP 頭在 CORS 中的實踐運用,更爲詳細的資料能夠參閱 Cross-Origin Resource Sharing,最後歸納下 CORS 的優缺點,優勢是支持全部類型的 HTTP 方法,缺點是有些老的瀏覽器不支持 CORS。
在文章最開始提到過 iframe 標籤也是不受同源策略限制的標籤之一,hash + iframe 的跨域核心思想就是,在 A 源中經過動態改變 iframe 標籤的 src 的哈希值,在 B 源中經過 window.onhashchange
來捕獲到相應的哈希值。思路不難直接上代碼:
A 頁面代碼示例(源爲 http://127.0.0.1:3000)
<body>
<iframe src="http://127.0.0.1:3001"></iframe>
<script> const iframe = document.getElementsByTagName('iframe')[0] iframe.setAttribute('style', 'display: none') const obj = { data: 'hash' } iframe.src = iframe.src + '#' + JSON.stringify(obj) // ① 關鍵語句 </script>
</body>
複製代碼
B 頁面代碼示例(源爲 http://127.0.0.1:3001)
window.onhashchange = function() { // ① 關鍵語句
console.log('來自 page2 的代碼 ' + window.location.hash) // 來自 page2 的代碼 #{"data":"hash"}
}
複製代碼
刷新 A 頁面,能夠發如今控制檯打印了以下字段,至此實現了跨域。
來自 page2 的代碼 #{"data":"hash"}
複製代碼
這種方式進行跨域優勢是支持頁面和頁面間的通訊,缺點也是隻支持 GET 方法和單向的跨域通訊。
爲了實現跨文檔傳送(cross-document messaging),簡稱 XDM。HTML5 給出了一個 api —— postMessage,postMessage() 方法接收兩個參數:發送消息
以及消息接收方所在域的字符串
。代碼示例以下:
A 頁面代碼示例(源爲 http://127.0.0.1:3000)
<body>
<iframe src="http://127.0.0.1:3001"></iframe>
<script> const iframe = document.getElementsByTagName('iframe')[0] iframe.setAttribute('style', 'display: none') iframe.onload = function() { // 此處要等 iframe 加載完畢,後續代碼纔會生效 iframe.contentWindow.postMessage('a secret', 'http://127.0.0.1:3001') } </script>
</body>
複製代碼
B 頁面代碼示例(源爲 http://127.0.0.1:3001)
window.addEventListener('message', function(event) {
console.log('From page1 ' + event.data)
console.log('From page1 ' + event.origin)
}, false)
複製代碼
刷新 A 頁面,能夠發如今控制檯打印了以下字段,至此實現了跨域。
From page1 a secret
From page1 http://127.0.0.1:3000
複製代碼
這種跨域方式優勢是是支持頁面和頁面間的雙向通訊,缺點也是隻能支持 GET 方法調用。
WebSockets 屬於 HTML5 的協議,它的目的是在一個持久鏈接上創建全雙工通訊。因爲 WebSockets 採用了自定義協議,因此優勢是客戶端和服務端發送數據量少,缺點是要額外的服務器。基礎的使用方法以下:
const ws = new WebSocket('ws://127.0.0.1:3000')
ws.onopen = function() {
// 鏈接成功創建
}
ws.onmessage = function(event) {
// 處理數據
}
ws.onerror = function() {
// 發生錯誤時觸發,鏈接中斷
}
ws.onclose = function() {
// 鏈接關閉時觸發
}
複製代碼
固然通常咱們會使用封裝好 WebSockets 的第三方庫 socket.io,這裏具體就不展開了。
前文所述五種跨域實踐的 demo 已上傳至 cross-domain,前端環境基於 create-react-app 搭建,後端環境用 node 搭建。
固然跨域方式還有一些其餘方式的實現,後續酌情慢慢填坑~