iframe实现微前端系统

448 阅读2分钟

主要记录使用iframe标签实现简易的微前端系统的过程。 先用vue脚手架搭建一个工程,该工程为主应用。里面包含头部,左侧菜单,右侧内容三大块。

主应用的页面

<div id="container">
  <div class="header">
  </div>
  <div class="menu">
  </div>
  <div class="body">
    <iframe ref="frameRef" :src="frame.src" @load="frame.frameLoad"></iframe>
  </div>
</div>
const frame = reactive({
      src: '',
      frameLoad: function () {
        const tag = frameRef.value?.src.match(/[^\/]+(?!.*\/)/)?.[0]?.split('?')[0];
        // 对url后的参数进行拼接 ,queryParam是进入页面后存下的url后的参数,可以借助
        // sessionStorage进行存储,并在此进行提取
        const query = Object.entries(queryParam)
          .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
          .join('&');
        history.replaceState(null, '', `${window.location.pathname.replace(/[^\/]+(?!.*\/)/, tag)}?${query}`);
      },
});

这是基本内容,可以使用grid或者flex进行布局调整。

首先浏览器中输入网址,开始访问主应用。主应用开始加载,把url中?以后的参数提取出来,存下来以备后续使用。此时左侧菜单组件需要调用获取菜单接口,渲染完左侧菜单后,需根据当前的url最后的一位标识,使iframe加载正确的子应用的url

根据url中的标识,反显页面代码,所有类型暂用any代替:

// 递归查找和当前Url相对应的菜单项,并加载该菜单下的子应用
function initMenu(tree: any[]): void {
      const target = window.location.pathname.match(/[^\/]+(?!.*\/)/)?.[0];
      if (!target) {
        return;
      }
      const checkTarget = (list:any[]): void => {
        list.some((item) => {
          if (item.tag === target) {
            loadFrame(item.path);
            return true;
          }
          if (item.children?.length !== 0) {
            checkTarget(item.children);
          }
        });
      };
      checkTarget(tree);
    }

iframe加载对应的url函数:

function loadFrame(path: string): Promise<void> {
    if (!path?.match(/[^\/]+(?!.*\/)/)?.[0]) {
      return;
    }
    // 组装参数项,params为存下的参数项
    const query = Object.entries(params)
      .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
      .join('&');
    // 更改iframe的src,frame为上面的与iframe有关的对象。
    // 这种获取子应用的路径,前提是子应用和主应用部署到一个服务器下
    frame.src = `${window.location.origin}${path}?${query}`
 }

实现的主要两点:

  1. 首次加载时根据浏览器url的path值,iframe加载完整的带有ip地址的路径。
  2. 点击菜单按钮加载对应的子应用,加载完成后,根据加载路径或者按钮对象带有的标识位,手动修改浏览器中的url

整合以上代码,可以写成服务类的形式,以下是伪代码:

class FrameService {
  frameSrc = ref('');
  function onframeLoad () {
    const tag = this.frameSrc.value.match(/[^\/]+(?!.*\/)/)?.[0]?.split('?')[0];
    // 对url后的参数进行拼接 ,queryParam是进入页面后存下的url后的参数,可以借助
    // sessionStorage进行存储,并在此进行提取
    const query = Object.entries(queryParam)
      .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
      .join('&');
    history.replaceState(null, '', `${window.location.pathname.replace(/[^\/]+(?!.*\/)/, tag)}?${query}`);
  },
  function loadFrame(path: string, flag: any): Promise<void> {
    if (!path?.match(/[^\/]+(?!.*\/)/)?.[0]) {
      return;
    }
    // 组装参数项,params为存下的参数项
    const query = Object.entries(params)
      .map(([k, v]) => `${k}=${encodeURIComponent(v)}`)
      .join('&');
    // 更改iframe的src,frame为上面的与iframe有关的对象。
    // 这种获取子应用的路径,前提是子应用和主应用部署到一个服务器下
    this.frameSrc.value = `${window.location.origin}${path}?${query}`
 }
}

nginx中主应用和子应用的部署

为方便主应用和子应用的维护,共享数据等,建议把主应用和子应用部署在同一服务器。主应用在获取用户信息后,可以把信息存入cookie等,方便子应用获取使用。

所有应用静态资源的访问前缀,和接口请求前缀,可以进行统一,方便应用的管理,以及nginx的配置,统一的前缀可以让nginx使用正则匹配。

所有子应用的接口报错弹窗行为,建议统一在主应用中处理。使用postMessage,在合适时机向主窗口发起事件,主应用接收事件完成相应处理即可。使用postMessage要注意两点:

  1. 子应用向主应用发送数据时,要带上targetOrigin,具体可查看MDN。
  2. 主应用在监听message事件时,要根据信息来源判断是否执行相应操作。