內嵌iframe_基於iframe,前端和前端聯調也是很絲滑
技術標籤:內嵌iframe
平時做的需求,都是前後端聯調,可能有時候多一個客戶端聯調。但還有一些需求,需要前端與前端聯調——iframe內嵌,一些很複雜的頁面可能會選擇直接內嵌、還有現在很火的微前端其中一種實現方式也是iframe,最後頁面也基本少不了兩個前端頁面的通訊了。前端和前端聯調的時候,比起和後端聯調的時候,需要做的更多。因為前端和前端聯調不僅是資料層面上,還有頁面狀態的資訊傳遞。下面我們來探討一套前端聯調通訊的方案
技術選擇
1. hashchange事件
頁面監聽hashchange事件,然後父頁面改變雜湊,子頁面讀取雜湊來實現通訊。但是這有一個問題,如果傳遞的資訊過多,那就會導致url很長,而且維護起來也麻煩。更嚴重的問題是,如果頁面本身有利用雜湊的邏輯,將會無解
2. storage
雖然可以解決,但導致storage資料冗餘,而且還需要及時清除多餘資料。一般情況下不用,更適合多個tab通訊
3. postmessage
這個應該是最穩定的方案,也不會帶來額外的副作用,也不用擔心資料量多少。加上一些鑑權校驗邏輯,就比較完善了
設計思路
我們選擇postmessage方案,那麼需要考慮的設計思路有:
1. 需要鑑權,否則有安全性問題(host校驗、data 傳入一些flag來校驗)
2. 使用的時候,像request http請求一樣的無差別體驗,只是底層換成前端通訊
3. 支援promise的呼叫方式
4. 支援引數和資料的預處理、後處理
5. 容易擴充套件
實現細節
發&收
假設當前在子頁面,發出請求的時候:
window.parent && window.parent.postMessage({ api: 'getUserInfo', payload: { id: 1 }}, '*');
收請求的處理:
window.IFRAME_APIS = { getUserInfo({ id }) { // 通過id拉使用者資訊,返回 // 怎麼返回呢,在子頁面再定義一個handleGetUserInfoSucc方法 iframeElement.postMessage({ api: 'handleGetUserInfoSucc', payload: { name: 'lhyt', age: 23 } }) }}window.addEventListener('message', ({ data }) => { try { console.log('recive data', data); window.IFRAME_APIS[data.api](data.payload); } catch (e) { console.error(e); }});
子頁面請求父頁面,獲取資料後,父頁面再調一下子頁面的處理成功的方法。當然,子頁面的addEventListener也是一模一樣的程式碼,而且IFRAME_APIS裡面要提前準備好handleGetUserInfoSucc
的方法
鑑權
addEventListener需要一些鑑權,否則有安全風險。最簡單有效的方法,加一個准入名單校驗即可
const FR_ALLOW_LIST = ['sourceA', 'sourceB']window.addEventListener('message', ({ data }) => { if (!data || typeof data !== 'object') { return; } if (FR_ALLOW_LIST.includes(data.fr)) { try { console.log('recive data', data); window.IFRAME_APIS[data.api](data.payload); } catch (e) { console.error(e); } } else { throw Error('unknown fr!') }});
後續我們可以和其他前端約定一些來源值fr來校驗是否可以訪問這些api
支援promise的方式
我們也看見了,子頁面發請求的時候,父頁面返回成功還要子頁面提前再準備一個方法,這樣子很麻煩。很明顯是需要一個promise的then處理,就像平時使用request/axios/fetch
一樣。需要解決的問題:
postMessage只能傳可被結構化克隆演算法序列化的資料,其中就不包含函式
promise的resolve和reject函式不能直接傳過去,需要用另一種方式來間接呼叫
//子頁面// 存放resolve、rejectconst resolvers = {};const rejecters = {};window.IFRAME_APIS = {// 準備好處理promise的函式 resolvePromise({ payload, resolve }) { if (resolvers[resolve]) { resolvers[resolve](payload || {}); } delete resolvers[resolve]; delete rejecters[resolve]; }, }// 子頁面請求父頁面function requestParent({ api, payload }) { return new Promise((resolve, reject) => { const rand = Math.random().toString(36).slice(2); window.parent.postMessage({ api, payload: { ...payload, resolve: rand, reject: rand, } }, '*'); resolvers[rand] = resolve; rejecters[rand] = reject; })}
父頁面要實現一個告訴子頁面執行resolve的函式
function sendResponse(payload) { iframe.contentWindow.postMessage( { payload: { resolve: payload.resolve, payload }, fr: 'sourceA', api: 'resolvePromise', }, '*' );}
這個過程就是,子頁面發請求給父頁面的時候,順便帶上key傳過去,自己維護key和resolve/reject
對映。父頁面呼叫子頁面的resolvePromise
來間接執行resolve/reject
。這樣子下來,所有的promise型別呼叫的請求都可以用這種方式來完成,舉個?
// 子頁面requestParent({ api: 'a', payload: { fr: 'sourceA', a: 1, b: '2' } }).then(console.log)// 父頁面window.IFRAME_APIS = {// 在裡面準備好處理promise的函式sendResponse a(payload) { sendResponse({ resolve: payload.resolve, msg: 'succ' }) }, }
預處理 & 後處理
有時候需要上游加上一些統一處理的邏輯,以免每一個請求的地方都做一次特殊處理。對於後處理也是,對格式進行一次全域性適配
const prefix = { a(params) { params.b = 2; return params }, b(params) { // loading的時候不請求 if (params.loading) { return false } return params }}const afterfix = { a(data) { return { ...data, msg: 'afterfix success' } }}function requestParent({ api, payload }) { // 預處理 if (prefix[api]) { payload = prefix[api](payload) } // 不請求 if (!payload) { return Promise.resolve({}) } return new Promise((resolve, reject) => { const rand = Math.random().toString(36).slice(2); window.parent.postMessage({ api, payload: { ...payload, resolve: rand, reject: rand, } }, '*'); resolvers[rand] = data => { // 後處理在這裡 if (afterfix[api]) { data = afterfix[api](data) } return resolve(data) }; rejecters[rand] = reject; })}
有一些不需要promise,是單向呼叫的,額外寫一個不是promise呼叫的函式即可,或者加一個引數來控制。還有promise呼叫方式可以加一個超時處理,改成正常請求和一個定時器來
Promise.race
。這些都是小問題,可酌情修改
可擴充套件
不一定所有的請求都要提前放IFRAME_APIS
裡面的,有一些有元件內建依賴的要在元件內部寫,還有一些是可能不需要這個請求了要刪掉。所以需要一個擴充套件iframe-api
的函式和一個刪除的函式,以及輔助資料的維護
const ext = {}function injectIframeApi(api, fn, injectExt) { function remove() { delete window.IFRAME_APIS[api]; } // 這個是擴充套件輔助資料,em,有時候的確是需要一些額外輔助資料 injectExt(ext); // 可以理解為,fn傳null就是僅僅更新ext if (fn === null) { return remove; } if (window.IFRAME_APIS[api]) { return remove; } window.IFRAME_APIS[api] = fn; return remove;}
加上了ext機制,請求的時候可能會用到,所以需要加上
function requestParent({ api, payload }) {
// 預處理
if (prefix[api]) {-- payload = prefix[api](payload)++ payload = prefix[api](payload, ext)
}
// 不請求
if (!payload) {
return Promise.resolve({})
}
return new Promise((resolve, reject) => {
const rand = Math.random().toString(36).slice(2);
window.parent.postMessage({
api, payload: {
...payload,
resolve: rand,
reject: rand,
}
}, '*');
resolvers[rand] = data => {
// 後處理在這裡
if (afterfix[api]) {-- data = afterfix[api](data)++ data = afterfix[api](data, ext)
}
return resolve(data)
};
rejecters[rand] = reject;
})
}
window.addEventListener('message', ({ data }) => {
try {
console.log('recive data', data);-- window.IFRAME_APIS[data.api](data.payload);++ window.IFRAME_APIS[data.api](data.payload, ext);
} catch (e) {
console.error(e);
}
});
使用的時候,比如在一個元件裡面:
window.IFRAME_APIS = { a(params, ext) { if (ext.loading) { return false } retuan params }}function C({ loading }) { useEffect(() => { // 請求a的時候,需要看看loading的值 injectIframeApi('a', null, ext => { ext.loading = loading }) }, [loading]) // 元件特有的請求函式,不用的時候就可以不要他了 useEffect(() => { const remove = injectIframeApi('someapi', data => { console.log(data, 'this is iframe api data') }) return remove }, []) return }
最後
這樣,就可以和普通request的使用方式一模一樣了,而且也支援各種處理和擴充套件,是一個和發起http請求的方式一模一樣的無差別體驗。當然,根據自己情況酌情修改更舒服哦,比如一些人喜歡node的error放第一個引數的callback風格、一些人喜歡axios風格的、一些人喜歡面向物件的風格,這些都可以圍著這個思路來酌情修改,最合適自己為好b站全灰了,但我一下把它弄回來了——css 濾鏡
1年多職業生涯中最玄乎的線上問題
前端與前端聯調的姿勢
[js演算法]手把手帶你從leetcode原題——【兩數相加】到大數相加
內功修煉之lodash——Object系列
那個前端寫的頁面好酷——大量的粒子(元素)的動效實現)
內功修煉之lodash—— clone&cloneDeep(一定有你遺漏的js基礎知識