前端跨域的各類文章其實已經不少了,但大部分仍是不太符合我胃口的介紹跨域。看來看去,若是要讓本身理解印象深入,果真仍是得本身敲一敲,並總結概括整理一篇博客出來,以此記錄。javascript
跨域是爲了阻止用戶讀取到另外一個域名下的內容,Ajax 能夠獲取響應,瀏覽器認爲這不安全,因此攔截了響應。html
除非特別說明,不然下方標記的 html 文件默認都運行在 http://127.0.0.1:5500 服務下前端
CORS 便是指跨域資源共享。它容許瀏覽器向非同源服務器,發出 Ajax 請求,從而克服了 Ajax 只能同源使用的限制。這種方式的跨域主要是在後端進行設置。vue
這種方式的關鍵是後端進行設置,便是後端開啓 Access-Control-Allow-Origin 爲*
或對應的 origin
就能夠實現跨域。java
瀏覽器將 CORS 請求分紅兩類:簡單請求和非簡單請求。node
只要同時知足如下兩大條件,就屬於簡單請求。react
凡是不一樣時知足上面兩個條件,就屬於非簡單請求。webpack
簡單請求nginx
cors.htmlgit
let xhr = new XMLHttpRequest() xhr.open('GET', 'http://localhost:8002/request') xhr.send(null)
server.js
const express = require('express') const app = express() app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', 'http://127.0.0.1:5500') // 設置容許哪一個域訪問 next() }) app.get('/request', (req, res) => { res.end('server ok') }) app.listen(8002)
非簡單請求
上面的是簡單請求,若是咱們用非簡單請求的方式,好比請求方法是 PUT,也能夠經過設置實現跨域。
非簡單請求的 CORS 請求,會在正式通訊以前,增長一次 HTTP 查詢請求,稱爲"預檢"請求。
瀏覽器先詢問服務器,服務器收到"預檢"請求之後,檢查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段之後,確認容許跨源請求,瀏覽器纔會發出正式的 XMLHttpRequest 請求,不然就報錯。
let xhr = new XMLHttpRequest() xhr.open('PUT', 'http://localhost:8002/request') xhr.send(null)
server.js
const express = require('express') const app = express() let whileList = ['http://127.0.0.1:5500'] // 設置白名單 app.use((req, res, next) => { let origin = req.headers.origin console.log(whitList.includes(origin)) if (whitList.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin) // 設置容許哪一個域訪問 res.setHeader('Access-Control-Allow-Methods', 'PUT') // 設置容許哪一種請求方法訪問 } next() }) app.put('/request', (req, res) => { res.end('server ok') }) app.listen(8002)
整個過程發送了兩次請求,跨域成功。
固然,還能夠設置其餘參數:
該字段是一個逗號分隔的字符串,指定瀏覽器 CORS 請求會額外發送的頭信息字段
表示是否容許發送 Cookie。默認狀況下,Cookie 不包括在 CORS 請求之中。
CORS 請求時,XMLHttpRequest 對象的 getResponseHeader()方法只能拿到 6 個基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若是想拿到其餘字段,就必須在 Access-Control-Expose-Headers 裏面指定。
用來指定本次預檢請求的有效期,單位爲秒。有效期是 20 天(1728000 秒),即容許緩存該條迴應 1728000 秒(即 20 天),在此期間,不用發出另外一條預檢請求。
實現原理:同源策略是瀏覽器須要遵循的標準,而若是是服務器向服務器請求就沒有跨域一說。
代理服務器,須要作如下幾個步驟:
此次咱們使用 express 中間件 http-proxy-middleware 來代理跨域, 轉發請求和響應
案例三個文件都在同一級目錄下:
index.html
let xhr = new XMLHttpRequest() xhr.open('GET', '/api/request') xhr.onreadystatechange = () => { if (xhr.readyState === 4 && xhr.status === 200) { console.log('請求成功,結果是:', xhr.responseText) // request success } } xhr.send(null)
nodeMdServer.js
const express = require('express') const { createProxyMiddleware } = require('http-proxy-middleware') const app = express() // 設置靜態資源 app.use(express.static(__dirname)) // 使用代理 app.use( '/api', createProxyMiddleware({ target: 'http://localhost:8002', pathRewrite: { '^/api': '', // 重寫路徑 }, changeOrigin: true, }) ) app.listen(8001)
nodeServer.js
const express = require('express') const app = express() app.get('/request', (req, res) => { res.end('request success') }) app.listen(8002)
運行http://localhost:8001/index.html
,跨域成功
日常 vue/react 項目配置 webpack-dev-server 的時候也是經過 Node proxy 代理的方式來解決的。
實現原理相似於 Node 中間件代理,須要你搭建一箇中轉 nginx 服務器,用於轉發請求。
這種方式只需修改 Nginx 的配置便可解決跨域問題,前端除了接口換成對應形式,而後先後端不須要修改做其餘修改。
實現思路:經過 nginx 配置一個代理服務器(同域不一樣端口)作跳板機,反向代理要跨域的域名,這樣能夠修改 cookie 中 domain 信息,方便當前域 cookie 寫入,實現跨域登陸。
nginx 目錄下的 nginx.conf 修改以下:
// proxy服務器 server { listen 80; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; # 反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; # 修改cookie裏域名 index index.html index.htm; # 當用webpack-dev-server等中間件代理接口訪問nignx時,此時無瀏覽器參與,故沒有同源限制,下面的跨域配置可不啓用 add_header Access-Control-Allow-Origin http://www.domain1.com; # 當前端只跨域不帶cookie時,可爲* add_header Access-Control-Allow-Credentials true; } }
啓動 Nginx
index.html
var xhr = new XMLHttpRequest() // 前端開關:瀏覽器是否讀寫cookie xhr.withCredentials = true // 訪問nginx中的代理服務器 xhr.open('get', 'http://www.domain1.com:81/?user=admin', true) xhr.send()
server.js
var http = require('http') var server = http.createServer() var qs = require('querystring') server.on('request', function (req, res) { var params = qs.parse(req.url.substring(2)) // 向前臺寫cookie res.writeHead(200, { 'Set-Cookie': 'l=123456;Path=/;Domain=www.domain2.com;HttpOnly', // HttpOnly:腳本沒法讀取 }) res.write(JSON.stringify(params)) res.end() }) server.listen(8080)
原理:利用了 script 標籤可跨域的特性,在客戶端定義一個回調函數(全局函數),請求服務端返回該回調函數的調用,並將服務端的數據以該回調函數參數的形式傳遞過來,而後該函數就被執行了。該方法須要服務端配合完成。
實現步驟:
jsonp.html
function getInfo(data) { console.log(data) // 告訴你一聲, jsonp跨域成功 } let script = document.createElement('script') script.src = 'http://localhost:3000?callback=getInfo' // document.body.appendChild(script)
server.js
const express = require('express') const app = express() app.get('/', (req, res) => { let { callback } = req.query res.end(`${callback}('告訴你一聲, jsonp跨域成功')`) }) app.listen(3000)
jQuery 的 $.ajax() 方法當中集成了 JSONP 的實現,在此就不寫出來了。
在開發中可能會遇到多個 JSONP 請求的回調函數名是相同的,並且這種方式用起來也麻煩,故咱們本身封裝一個 jsonp 函數
function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { let script = document.createElement('script') // 定義全局回調函數 window[callback] = function (data) { resolve(data) document.body.removeChild(script) // 調用完畢即刪除 } params = { callback, ...params } // {callback: "getInfo", name: "jacky"} let paramsArr = [] for (const key in params) { paramsArr.push(`${key}=${params[key]}`) } script.src = `${url}?${paramsArr.join('&')}` // http://localhost:3000/?callback=getInfo&name=jacky document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000', params: { name: 'jacky', }, callback: 'getInfo', }).then(res => { console.log(res) // 告訴你一聲, jsonp跨域成功 })
服務端解構的時候能夠取出參數
app.get('/', (req, res) => { let { callback, name } = req.query res.end(`${callback}('告訴你一聲, jsonp跨域成功')`) })
優勢:兼容性好
缺點:因爲 script 自己的限制,該跨域方式僅支持 get 請求,且不安全可能遭受 XSS 攻擊
postMessage 是 HTML5 XMLHttpRequest Level 2 中的 API,且是爲數很少能夠跨域操做的 window 屬性之一,它可用於解決如下方面的問題:
總之,它能夠容許來自不一樣源的腳本採用異步方式進行有限的通訊,能夠實現跨文本檔、多窗口、跨域消息傳遞。
otherWindow.postMessage(message, targetOrigin, [transfer]);
此次咱們把兩個 html 文件掛到兩個 server 下,採起 fs 讀取的方式引入,運行兩個 js 文件
postMessage1.html
<body> <iframe src="http://localhost:8002" frameborder="0" id="frame" onLoad="load()"></iframe> <script> function load() { let frame = document.getElementById('frame') frame.contentWindow.postMessage('你好,我是postMessage1', 'http://localhost:8002') //發送數據 window.onmessage = function (e) { //接受返回數據 console.log(e.data) // 你好,我是postMessage2 } } </script> </body>
postMsgServer1.js
const express = require('express') const fs = require('fs') const app = express() app.get('/', (req, res) => { const html = fs.readFileSync('./postMessage1.html', 'utf8') res.end(html) }) app.listen(8001, (req, res) => { console.log('server listening on 8001') })
postMessage2.html
<body> <script> window.onmessage = function (e) { console.log(e.data) // 你好,我是postMessage1 e.source.postMessage('你好,我是postMessage2', e.origin) } </script> </body>
postMsgServer2.js
const express = require('express') const fs = require('fs') const app = express() app.get('/', (req, res) => { const html = fs.readFileSync('./postMessage2.html', 'utf8') res.end(html) }) app.listen(8002, (req, res) => { console.log('server listening on 8002') })
WebSocket 是一種網絡通訊協議。它實現了瀏覽器與服務器全雙工通訊,同時容許跨域通信,長鏈接方式不受跨域影響。因爲原生 WebSocket API 使用起來不太方便,咱們通常都會使用第三方庫如 ws。
Web 瀏覽器和服務器都必須實現 WebSockets 協議來創建和維護鏈接。因爲 WebSockets 鏈接長期存在,與典型的 HTTP 鏈接不一樣,對服務器有重要的影響。
socket.html(http://127.0.0.1:5500/socket.html
)
let socket = new WebSocket('ws://localhost:8001') socket.onopen = function () { socket.send('向服務端發送數據') } socket.onmessage = function (e) { console.log(e.data) // 服務端傳給你的數據 }
運行nodeServer.js
const express = require('express') const WebSocket = require('ws') const app = express() let wsServer = new WebSocket.Server({ port: 8001 }) wsServer.on('connection', function (ws) { ws.on('message', function (data) { console.log(data) // 向服務端發送數據 ws.send('服務端傳給你的數據') }) })
這種方式只能用於二級域名相同的狀況下。
好比 a.test.com 和 b.test.com 就屬於二級域名,它們都是 test.com 的子域
只須要給頁面添加 document.domain ='test.com' 表示二級域名都相同就能夠實現跨域。
好比:頁面 a.test.com:3000/test1.html 獲取頁面 b.test.com:3000/test2.html 中 a 的值
test1.html
<body> <iframe src="http://b.test.com:3000/test2.html" frameborder="0" onload="load()" id="iframe" ></iframe> <script> document.domain = 'test.com' function load() { console.log(iframe.contentWindow.a) } </script> </body>
test2.html
document.domain = 'test.com' var a = 10
瀏覽器具備這樣一個特性:同一個標籤頁或者同一個 iframe 框架加載過的頁面共享相同的 window.name 屬性值。在同個標籤頁裏,name 值在不一樣的頁面加載後也依舊存在,這些頁面上 window.name 屬性值都是相同的。利用這些特性,就能夠將這個屬性做爲在不一樣頁面之間傳遞數據的介質。
因爲安全緣由,瀏覽器始終會保持 window.name 是 string 類型。
打開http://localhost:8001/a.html
<body> <iframe src="http://localhost:8002/c.html" frameborder="0" onload="load()" id="iframe" ></iframe> <script> let first = true function load() { if (first) { // 第1次onload(跨域頁)成功後,切換到同域代理頁面 let iframe = document.getElementById('iframe') iframe.src = 'http://localhost:8001/b.html' first = false } else { // 第2次onload(同域b.html頁)成功後,讀取同域window.name中數據 console.log(iframe.contentWindow.name) // 我是c.html裏的數據 } } </script> </body>
<body> <script> window.name = '我是c.html裏的數據' </script> </body>
c 頁面給 window.name 設置了值, 即使 c 頁面銷燬,但 name 值不會被銷燬;a 頁面依舊可以獲得 window.name。
實現原理: a.html 欲與 c.html 跨域相互通訊,經過中間頁 b.html 來實現。 三個頁面,不一樣域之間利用 iframe 的 location.hash 傳值,相同域之間直接 js 訪問來通訊。
具體實現步驟:一開始 a.html 給 c.html 傳一個 hash 值,而後 c.html 收到 hash 值後,再把 hash 值傳遞給 b.html,最後 b.html 將結果放到 a.html 的 hash 值中。
一樣的,a.html 和 b.html 是同域的,都是http://localhost:8001,也就是說 b 的 hash 值能夠直接複製給 a 的 hash。c.html 爲http://localhost:8002下的
a.html
<body> <iframe src="http://localhost:8002/c.html#jackylin" style="display: none;"></iframe> <script> window.onhashchange = function () { // 檢測hash的變化 console.log(456, location.hash) // #monkey } </script> </body>
b.html
window.parent.parent.location.hash = location.hash // b.html將結果放到a.html的hash值中,b.html可經過parent.parent訪問a.html頁面
c.html
console.log(location.hash) // #jackylin let iframe = document.createElement('iframe') iframe.src = 'http://localhost:8001/b.html#monkey' document.body.appendChild(iframe)