前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于iframe,前端和前端联调也是很丝滑

基于iframe,前端和前端联调也是很丝滑

作者头像
lhyt
发布2022-03-08 11:07:53
7880
发布2022-03-08 11:07:53
举报
文章被收录于专栏:lhyt前端之路

平时做的需求,都是前后端联调,可能有时候多一个客户端联调。但还有一些需求,需要前端与前端联调——iframe内嵌,一些很复杂的页面可能会选择直接内嵌、还有现在很火的微前端其中一种实现方式也是iframe,最后页面也基本少不了两个前端页面的通信了。前端和前端联调的时候,比起和后端联调的时候,需要做的更多。因为前端和前端联调不仅是数据层面上,还有页面状态的信息传递。下面我们来探讨一套前端联调通信的方案

技术选择

  1. hashchange事件

页面监听hashchange事件,然后父页面改变哈希,子页面读取哈希来实现通信。但是这有一个问题,如果传递的信息过多,那就会导致url很长,而且维护起来也麻烦。更严重的问题是,如果页面本身有利用哈希的逻辑,将会无解

  1. storage

虽然可以解决,但导致storage数据冗余,而且还需要及时清除多余数据。一般情况下不用,更适合多个tab通信

  1. postmessage

这个应该是最稳定的方案,也不会带来额外的副作用,也不用担心数据量多少。加上一些鉴权校验逻辑,就比较完善了

设计思路

我们选择postmessage方案,那么需要考虑的有:

  1. 需要鉴权,否则有安全性问题(host校验、data 传入一些flag来校验)
  2. 使用的时候,像request http请求一样的无差别体验,只是底层换成前端通信
  3. 支持promise的调用方式
  4. 支持参数和数据的预处理、后处理
  5. 容易扩展

实现细节

发&收

假设当前在子页面,发出请求的时候:

代码语言:javascript
复制
window.parent && window.parent.postMessage({
  api: 'getUserInfo', payload: { id: 1 } 
}, '*');

收请求的处理:

代码语言:javascript
复制
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需要一些鉴权,否则有安全风险。最简单有效的方法,加一个准入名单校验即可

代码语言:javascript
复制
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函数不能直接传过去,需要用另一种方式来间接调用
代码语言:javascript
复制
// 子页面
// 存放resolve、reject
const 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的函数

代码语言:javascript
复制
function sendResponse(payload) {
  iframe.contentWindow.postMessage(
    {
      payload: { resolve: payload.resolve, payload },
      fr: 'sourceA',
      api: 'resolvePromise',
    },
    '*'
  );
}

这个过程就是,子页面发请求给父页面的时候,顺便带上key传过去,自己维护key和resolve/reject映射。父页面调用子页面的resolvePromise来间接执行resolve/reject。这样子下来,所有的promise类型调用的请求都可以用这种方式来完成,举个🌰

代码语言:javascript
复制
// 子页面
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' })
  },
 }

预处理 & 后处理

有时候需要上游加上一些统一处理的逻辑,以免每一个请求的地方都做一次特殊处理。对于后处理也是,对格式进行一次全局适配

代码语言:javascript
复制
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的函数和一个删除的函数,以及辅助数据的维护

代码语言:javascript
复制
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机制,请求的时候可能会用到,所以需要加上

代码语言:javascript
复制
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);
    }
});

使用的时候,比如在一个组件里面:

代码语言:javascript
复制
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 <section />
}

最后

这样,就可以和普通request的使用方式一模一样了,而且也支持各种处理和扩展,是一个和发起http请求的方式一模一样的无差别体验。当然,根据自己情况酌情修改更舒服哦,比如一些人喜欢node的error放第一个参数的callback风格、一些人喜欢axios风格的、一些人喜欢面向对象的风格,这些都可以围着这个思路来酌情修改,最合适自己为好

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/08/02 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术选择
  • 设计思路
  • 实现细节
    • 发&收
      • 鉴权
        • 支持promise的方式
          • 预处理 & 后处理
            • 可扩展
            • 最后
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档