跨域實踐二三事

文章首發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 跨域

提到 JSONP 跨域,不得不先提到 script 標籤,和 imgiframe 標籤相似,這些標籤是不受同源策略限制的,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 跨域

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 分爲簡單請求以及非簡單請求。能夠這麼區分,若是請求方法爲 POSTGETHEAD 時爲簡單請求,其它方法如 PUTDELETE 等爲非簡單請求,若是是非簡單請求的話,能夠在 chrome 的 Network 中看到多了一次 Request MethodOPTIONS 的請求。以下圖:

能夠把這個請求稱爲預請求,用白話文翻譯下,瀏覽器詢問服務器,'服務器大哥,我此次要進行 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。

hash + iframe

在文章最開始提到過 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 方法和單向的跨域通訊。

postMessage

爲了實現跨文檔傳送(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

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 搭建。

固然跨域方式還有一些其餘方式的實現,後續酌情慢慢填坑~

相關文章
相關標籤/搜索