主要记录使用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}`
}
实现的主要两点:
- 首次加载时根据浏览器
url的path值,iframe加载完整的带有ip地址的路径。 - 点击菜单按钮加载对应的子应用,加载完成后,根据加载路径或者按钮对象带有的标识位,手动修改浏览器中的
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要注意两点:
- 子应用向主应用发送数据时,要带上
targetOrigin,具体可查看MDN。 - 主应用在监听
message事件时,要根据信息来源判断是否执行相应操作。