1. 程式人生 > 其它 >內嵌iframe_基於iframe,前端和前端聯調也是很絲滑

內嵌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 }

最後

f9226eae28dd6e21f8a1de756098a710.png這樣,就可以和普通request的使用方式一模一樣了,而且也支援各種處理和擴充套件,是一個和發起http請求的方式一模一樣的無差別體驗。當然,根據自己情況酌情修改更舒服哦,比如一些人喜歡node的error放第一個引數的callback風格、一些人喜歡axios風格的、一些人喜歡面向物件的風格,這些都可以圍著這個思路來酌情修改,最合適自己為好 f9364d28be73fae8bfed1ffc6ced2e40.png fcb96900cc3f1e99c671760ce5caab3d.gif

b站全灰了,但我一下把它弄回來了——css 濾鏡

1年多職業生涯中最玄乎的線上問題

前端與前端聯調的姿勢

[js演算法]手把手帶你從leetcode原題——【兩數相加】到大數相加

內功修煉之lodash——Object系列

那個前端寫的頁面好酷——大量的粒子(元素)的動效實現)

內功修煉之lodash—— clone&cloneDeep(一定有你遺漏的js基礎知識