Based on iframe, the front-end and front-end joint debugging are also very smooth

Posted by crinkle on Tue, 08 Mar 2022 05:15:01 +0100

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

  1. 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

  1. 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

  1. 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:

  1. Authentication is required, otherwise there is a security problem (host verification, data passing in some flag s for verification)
  2. When using, the same indifference experience as request http request, but the bottom layer is replaced by front-end communication
  3. Support the call mode of promise
  4. Support pre-processing and post-processing of parameters and data
  5. 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