Usually, the requirements are front and rear end joint debugging, and sometimes there may be one more client-side joint debugging. However, there are still some requirements. The front-end and front-end need to be jointly debugged - iframe embedding. Some very complex pages may choose to be directly embedded, and the popular micro front-end is also iframe. Finally, the communication between the two front-end pages is basically indispensable for the page. The front-end and front-end joint debugging need to be done more than the back-end joint debugging. Because the front-end and front-end joint debugging is not only the data level, but also the information transmission of page status. Let's discuss a set of front-end joint debugging communication scheme
Technology selection
- hashchange event
The page listens for hashchange events, then the parent page changes the hash, and the child page reads the hash to realize communication. But there is a problem. If too much information is passed, the url will be very long and troublesome to maintain. The more serious problem is that if the page itself has the logic of using hash, there will be no solution
- storage
Although it can be solved, it leads to redundant storage data, and redundant data needs to be removed in time. Generally, it is not used and is more suitable for multi tab communication
- postmessage
This should be the most stable solution, and will not bring additional side effects, and there is no need to worry about the amount of data. With some authentication and verification logic, it is more perfect
Design ideas
If we choose the postmessage scheme, we need to consider:
- Authentication is required, otherwise there is a security problem (host verification, data passing in some flag s for verification)
- When using, the same indifference experience as request http request, but the bottom layer is replaced by front-end communication
- Support the call mode of promise
- Support pre-processing and post-processing of parameters and data
- Easy to expand
Implementation details
Send & receive
Assume that the current sub page, when making a request:
window.parent && window.parent.postMessage({ api: 'getUserInfo', payload: { id: 1 } }, '*');
Processing of receipt request:
window.IFRAME_APIS = { getUserInfo({ id }) { // Pull user information through id and return // How to return? Define a handleGetUserInfoSucc method on the sub page 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); } });
The sub page requests the parent page. After obtaining the data, the parent page adjusts the processing method of the sub page to succeed. Of course, as like as two peas, the addEventListener of the subpage is identical to the IFRAME_. The method of handleGetUserInfoSucc should be prepared in advance in APIs
authentication
addEventListener needs some authentication, otherwise it has security risks. The simplest and effective method is to add an access list for verification
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!') } });
Later, we can agree some source values fr with other front ends to verify whether we can access these APIs
Ways to support promise
We have also seen that when the child page sends a request, the parent page returns successfully, and the child page needs to prepare another method in advance, which is very troublesome. Obviously, a promise then processing is required, just like using request/axios/fetch in normal times. Problems to be solved:
- postMessage can only transmit data that can be serialized by the structured cloning algorithm, which does not contain functions
- The resolve and reject functions of promise cannot be passed directly. They need to be called indirectly in another way
// Subpage // Store resolve and reject const resolvers = {}; const rejecters = {}; window.IFRAME_APIS = { // Prepare the function to handle promise resolvePromise({ payload, resolve }) { if (resolvers[resolve]) { resolvers[resolve](payload || {}); } delete resolvers[resolve]; delete rejecters[resolve]; }, } // Child page request parent page 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; }) }
The parent page should implement a function that tells the child page to execute resolve
function sendResponse(payload) { iframe.contentWindow.postMessage( { payload: { resolve: payload.resolve, payload }, fr: 'sourceA', api: 'resolvePromise', }, '*' ); }
In this process, when the child page sends a request to the parent page, take the key and pass it to the parent page, and maintain the key and resolve/reject mapping yourself. The parent page calls the resolvePromise of the child page to indirectly execute resolve/reject. In this way, all requests called by promise type can be completed in this way, for example 🌰
// Subpage requestParent({ api: 'a', payload: { fr: 'sourceA', a: 1, b: '2' } }) .then(console.log) // Parent page window.IFRAME_APIS = { // Prepare the function sendResponse to handle promise in it a(payload) { sendResponse({ resolve: payload.resolve, msg: 'succ' }) }, }
Pretreatment & post-treatment
Sometimes it is necessary to add some unified processing logic to the upstream to avoid a special processing for each request. For post-processing, it is also a global adaptation of the format
const prefix = { a(params) { params.b = 2; return params }, b(params) { // No request when loading if (params.loading) { return false } return params } } const afterfix = { a(data) { return { ...data, msg: 'afterfix success' } } } function requestParent({ api, payload }) { // Pretreatment if (prefix[api]) { payload = prefix[api](payload) } // No request 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 => { // Post processing is here if (afterfix[api]) { data = afterfix[api](data) } return resolve(data) }; rejecters[rand] = reject; }) }
Some do not need promise and are called one way. Just write an additional function that is not called by promise, or add a parameter to control it. In addition, the promise call mode can be added with a timeout processing, which can be changed into a normal request and a timer to promise race. These are minor issues that can be modified as appropriate
Extensible
Not all requests need to put iframe in advance_ In APIs, some built-in dependencies of components need to be written inside the component, and some may not need this request and need to be deleted. Therefore, we need a function to extend iframe API, a function to delete, and the maintenance of auxiliary data
const ext = {} function injectIframeApi(api, fn, injectExt) { function remove() { delete window.IFRAME_APIS[api]; } // This is the extended auxiliary data, em. sometimes some additional auxiliary data is needed injectExt(ext); // It can be understood that fn passing null means only updating ext if (fn === null) { return remove; } if (window.IFRAME_APIS[api]) { return remove; } window.IFRAME_APIS[api] = fn; return remove; }
The ext mechanism is added, which may be used when requesting, so it needs to be added
function requestParent({ api, payload }) { // Pretreatment if (prefix[api]) { -- payload = prefix[api](payload) ++ payload = prefix[api](payload, ext) } // No request 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 => { // Post processing is here 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); } });
When using a component, for example:
window.IFRAME_APIS = { a(params, ext) { if (ext.loading) { return false } retuan params } } function C({ loading }) { useEffect(() => { // When requesting a, you need to look at the value of loading injectIframeApi('a', null, ext => { ext.loading = loading }) }, [loading]) // The component specific request function can be removed when not in use useEffect(() => { const remove = injectIframeApi('someapi', data => { console.log(data, 'this is iframe api data') }) return remove }, []) return <section /> }
last
As like as two peas as like as two peas, the request is the same as the way that the common http is used, and it supports various processing and extensions. It is a same indifference experience as the way to initiate the request. Of course, it's more comfortable to modify it according to your own situation. For example, some people like node's error, the callback style with the first parameter, some people like axios style, and some people like object-oriented style. These can be modified according to this idea, which is best for you