文章目錄
- 瀏覽器上下文通信
- 同源通信
- 同源通信流程
- 同一瀏覽器上下文通信
- 不同瀏覽器上下文通信
- 跨域通信
- 前端和前端跨域
- 前端和后端跨域
瀏覽器上下文通信
瀏覽器上下文通信分為兩種:同源和跨源。同樣的同源通信也分為同一瀏覽器上下文和不同的瀏覽器上下文。
同源通信
同源通信方式多種多樣,最常用的應該就是localStorage, sessionStorage
,也有一個全局對象或者模塊導出的對象,還有cookie, caches(cacheStorage)
等,以及通過location
對象,history.state
, Event API
, Messaging API
, SharedWorker API
等方式傳遞消息。
同源通信流程
通信流程大體分為兩步:第一步是傳遞數據,第二步是通知目標。
同一瀏覽器上下文通信
第一步很容易,上面的方法都可以。但是第二步就有點不同的。有些方法提供了官方的事件監聽,比如storage, hashchange, popstate
等,但有些是沒有這些原生事件的,那就需要一種方式通知。
很容易想到的就是自定義事件(CustomEvent)。
el.addEventListener('cookiechange', (e) => {console.log(e.detail);// ...
});const event = new CustomEvent('cookiechange', {detail: {getRandom(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;}},bubbles: true, // 可以調用 stopPropagationcancelable: true, // 可以調用 preventDefault
});// 更新操作后通知對應元素或其子元素(要設置冒泡)
el.dispatchEvent(event);
除了這種方式,還有一種常見的就是發布訂閱。
class PubSub {constructor() {this.subs = new Map() // 訂閱者}// 訂閱on(type, fn) {if (!this.subs.has(type)) {this.subs.set(type, new Set())}this.subs.get(type).add(fn)}// 發布emit(type, ...args) {if (this.subs.has(type)) {for (const fn of this.subs.get(type)) {fn(...args)}}}
}
最后一個就是window.postMessage
,可以先監聽message
事件,然后在需要時通知。
window.addEventListener('message', (e) => {console.log(e);
});// 在需要時調用
window.postMessage({msg: '這是發給自己的消息',type: 'postMessage'},'*');
不同瀏覽器上下文通信
上面的方式都要求同一瀏覽器上下文,也就是處于同一個window
下的文檔,這屬于內部通信。如果是不同的上下文,比如使用iframe
或兩個標簽頁,雖然是同源的但這樣就無法通知到,這就需要跨瀏覽器上下文通信。
postMessage,Broadcast Channel API,Channel Messaging API, SharedWorker API都可以實現。
最簡單的就是使用Broadcast
和SharedWorker(需要瀏覽器支持)
,其它兩種更常用于跨域通信。可以點擊上面的鏈接查看用法。
跨域通信
跨域通信也有很多類型,前端和前端跨域,前端和后端跨域,后端和后端跨域。后端和后端那就是純后端的處理,和瀏覽器沒什么關系了,除非使用了類似web socket
這種長連接。
前端和前端跨域
想要實現兩個跨域的前端頁面通信,一方必須知道另一方的瀏覽器上下文或者消息句柄,也就是兩個瀏覽器上下文必須要存在聯系。比如使用<iframe>
包含另一個上下文或者使用window.open
打開另一個上下文。
需要注意的是,這兩種方式都需要等待新的上下文加載完成之后才能通信的。所以<iframe>
需要添加load
事件監聽,而window.open
可以讓對方先發送消息建立連接,類似TCP三次握手之后才正式發送消息。
// localhost:3000/a.html 向 localhost:4000/b.html 通信
// localhost:3000/a.html
const messageCallback = (e) => {if (e.origin === 'http://localhost:4000') {//接受返回數據console.log(e.data)}
}
let active = null;
const origin = 'http://localhost:4000';
frame.addEventListener('load', () => {window.addEventListener('message', messageCallback)active = frame.contentWindow;active.postMessage({type: 'init',msg: 'hello'},origin) //發送數據
})
frame.addEventListener('beforeunload', () => {window.removeEventListener('message', messageCallback)
})
// 發送消息函數
const postMsg = (payload) => {if (active && !active.closed) {active.postMessage(payload, origin);}
}// localhost:4000/b.html
let active = null;
let origin = '';
window.postMessage('message', (e) => {if (e.data && e.data.type === 'init') {active = e.source;origin = e.origin;return;}// 處理其它消息
});
const postMsg = (payload) => {// ...
}// 如果使用的是 window.open
let active = null;
let origin = '';
window.addEventListener('message', (e) => {// 處理初始化消息if (e.data === 'B_LOADED') {active = e.source;origin = e.origin;active.postMessage('A_ACCEPTED', origin); // 第三步,完成之后可以正常通信了return;}// 處理其它消息
})
xxx.addEventListener('click', () => {// 默認創建標簽頁,如果傳遞第三個參數或`target: '_blank'`會創建新的窗口window.open('http://localhost:4000/b.html', 'b') // 第一步// 這里創建之后可能頁面還沒加載完成,所以調用 postMessage 可能沒有響應。// 要么使用定時器不斷輪詢,要么等待對方加載完成之后再建立連接。
})
const postMsg = (payload) => {// ...
}// localhost:4000/b.html
let active = null;
let origin = 'http://localhost:3000';
window.addEventListener('message', (e) => {if (e.data && e.data === 'A_ACCEPTED') {// 建立連接成功return;}// 處理其它消息
})
const postMsg = (payload) => {// ...
}
if (window.opener && !window.opener.closed) {active = window.opener;postMsg('B_LOADED'); // 第二步
}
這里使用的一般是上面的postMessage
和Channel Messaging API
,可以點擊上面的鏈接查看使用示例。
前端和后端跨域
前后端跨域是非常常見的,通用的解決方案一般使用使用JSONP
和new Image
需要使用callback
作為參數的查詢字符串,缺點就是只能發起GET
請求。
另一種需要滿足CORS
的請求規范,也就是前后端同時遵守一套跨域規則,滿足之后就允許跨域請求了。
一些普通的GET
請求可以直接請求,但其它方法的請求需要先發起預檢請求約定一些規則。
// server.js
import express from 'express'
const app = express()const whiteList = ['http://localhost:3000'] //設置白名單
// 設置跨域插件
app.use(function (req, res, next) {const origin = req.headers.originif (whiteList.includes(origin)) {// 設置哪個源可以訪問我res.setHeader('Access-Control-Allow-Origin', origin)// 允許攜帶哪個頭訪問我res.setHeader('Access-Control-Allow-Headers', 'Access-Token')// 允許哪個方法訪問我res.setHeader('Access-Control-Allow-Methods', 'POST,PUT,DELETE')// 允許攜帶cookieres.setHeader('Access-Control-Allow-Credentials', true)// 預檢的存活時間res.setHeader('Access-Control-Max-Age', 24 * 60 * 60 * 1000)// 允許返回的頭res.setHeader('Access-Control-Expose-Headers', 'latest-version')}next()
})
app.put('/getData', function (req, res) {console.log(req.headers)res.setHeader('latest-version', '3.2.1') // 返回一個響應頭res.end('over');
})
app.listen(4000);
這個后端要求跨域請求的源必須是白名單里的,同時規定了請求需要使用的方法,跨域請求需要傳遞的請求頭以及一些其它配置。只要用戶滿足請求方法和請求頭的要求就可以請求http://localhost:4000/getData
,這個方法設置了響應頭latest-version
和響應數據over
。
前端需要根據需求發起跨域請求。
// XMLHttpRequest
const xhr = new XMLHttpRequest()
xhr.withCredentials = true // 前端設置是否帶cookie
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('Access-Token', '123456')
xhr.onreadystatechange = function () {if (xhr.readyState === 4) {if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {//得到響應頭console.log(xhr.getResponseHeader('latest-version')) // 3.2.1console.log(xhr.response) // over}}
}
xhr.send();// fetch
fetch('http://localhost:4000/getData', {mode: 'cors', // 跨域credentials: 'include', // 攜帶cookieheaders: {'Access-Token': '123456' // 這個自定義請求頭是后臺要求的},method: 'PUT' // 請求方式
}).then((res) => {for (const [key, value] of res.headers.entries()) {if (key === 'latest-version') {console.log(value) // 3.2.1}}return res.text()}).then((data) => {console.log(data) // over})
這樣前端和后端的跨域請求就完成了,只要過期時間沒到,再次跨域請求就不需要發起預檢請求了。