关于微前端框架wujie的一次企业级应用实践demo?
前言
本文将介绍我一种wujie的一次具体的应用,包括使用的场景、方式等等,完成一个具体的demo;
为什么要用微前端
事情是这样的,我们之前的业务有一个vue3+ts+vite的后台项目,后来公司决定新开发一个新的业务线,但是由于人力有限,如果重新搭建一个新的后台时间和人力成本较大都,尤其是其中的权限、登录功能的设计都比较复杂,所以我们综合考虑,有没有一种可以直接用旧后台的权限和登录功能,然后其它功能完全隔离的,且旧后台和新后台可用两个部门的人来开发,可以独立开发、测试、部署,甚至技术栈也可以不受影响呢?这里我们想到了微前端方案;
微前端方案选择
我们经过调研,目光逐步瞄向了两种微前端的方案:无界 和 乾坤;
对比我们的业务,经过调研发现无界相比于乾坤更有优势:
- 1、对旧后台项目影响较小,侵入程度低:只需要在旧有后台的项目上新起page页,以及新增一个路由即可;
- 2、可单独开发、部署:子应用可以单独开发、部署,也可以使用一个全新的技术栈,即使生产环境无界挂了,出现问题了,也可以直接访问子应用;
综上两种原因,我们决定使用无界的方案;
怎么用无界(demo演示)
我们的主应用是vue3,这里将子应用通过菜单栏的形式嵌入到父应用中间,点击菜单即可进入到子应用
登录场景,在子应用请求时,若发现登录失效,通过子组件通信window.$wujie.bus.$emit('notLogin')向父应用传递未登录消息,父应用执行后续逻辑
权限逻辑,天然就互通,当子应用的菜单权限在某些角色下不可见时,在父应用下直接隐藏掉菜单就行;如果是子应用下按钮权限等功能权限时,可在子应用单独再次调用权限接口,或通过父子应用通信方式获取权限信息
具体步骤
父应用改造
- 下载新依赖
- wujie相关文件
- 路由
下载相关依赖
pnpm install wujie-vue3
创建wujie文件
用于补充wujie的相关逻辑:
-
wujie的template及相关属性-
name: 子应用唯一标识 -
url: 子应用运行地址 -
props:向子应用传递的参数
-
- 父子应用通信
- 通知子应用路由发生改变
- 通知子应用其他数据
- 子应用告知父应用未登录
- 子应用告知父应用其他信息
<template>
<div class="main-app">
<h1>Vue3 主应用</h1>
<!-- 嵌入 React 子应用 -->
<WujieVue width="100%" height="600px" :url="subAppUrl" :name="subAppName" :props="subAppProps" />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useRouter } from "vue-router";
import { watch } from "vue";
import WujieVue from "wujie-vue3";
const { bus } = WujieVue;
// 子应用配置(React 子应用的运行地址,后续启动子应用后会用到)
const subAppName = ref("react-sub-app"); // 子应用唯一标识(必须唯一)
const subAppUrl = ref("http://localhost:1074/#/wujieDemo1"); // 子应用端口(后续配置 React 子应用为 3001)
// 主应用向子应用传递的 props(可选)
const subAppProps = ref({
mainAppName: "Vue3 主应用",
token: "main-app-token-123",
});
const router = useRouter();
/** 监听子应用的数据 */
bus.$on("subAppData", (data: { type: string, payload?: any }) => {
const { type } = data;
if (type == "noLogin") {
alert("未登录")
}
});
/** 监听子应用的数据 */
watch(
() => router.currentRoute.value.meta.subAppPath,
(newVal) => {
if (newVal === undefined) return;
bus.$emit("routeChange", newVal);
},
{
immediate: true,
}
);
</script>
创建wujie路由
这里新建了一个路由的文件wujieRouter.ts
通过监听subAppPath去判断跳转到子应用对应路由,且这里的subAppPath其实对应的是子应用的路由path
const routerName = "wujiePage";
const wujieRouters = [
{
path: `/${routerName}`,
name: `${routerName}`,
component: () => import("@/pages/wujie/index.vue"),
meta: {
title: '新项目-react', // 菜单显示文本
icon: 'CreditCard', // 菜单图标
hidden: false,
level: 0,
},
children: [
{
path: "wujieDemo1", // 子路由直接使用相对路径,不要包含父路由名称
name: `${routerName}wujieDemo1`, // 名称保持唯一,不要使用斜杠
component: () => import("@/pages/wujie/wujie.vue"),
meta: {
title: 'wujieDemo1', // 菜单显示文本
icon: 'Present', // 子菜单图标
hidden: false,
level: 1,
subAppPath: "/wujieDemo1",
},
},
{
path: "wujieDemo2", // 子路由直接使用相对路径,不要包含父路由名称
name: `${routerName}wujieDemo2`, // 名称保持唯一,不要使用斜杠
component: () => import("@/pages/wujie/wujie.vue"),
meta: {
title: 'wujieDemo2', // 菜单显示文本
icon: 'Present', // 子菜单图标
hidden: false,
level: 1,
subAppPath: "/wujieDemo2",
},
},
]
},
]
export default wujieRouters;
子应用改造
- 运行环境判断
- 路由通信
- 嵌入子页面
- 路由
- 接口响应拦截器
运行环境判断
这里我们在main.tsx文件通过判断window.$wujie属性是否存在,来判断当前的运行环境是独立运行还是微前端环境
原理:wujie会自动给子应用的window上挂载一个$wujie对象
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { HashRouter } from "react-router-dom";
import "./style/index.css";
import App from "./App";
import { Provider } from "react-redux";
import { store } from "./model/store";
// Wujie 子应用生命周期:挂载(主应用嵌入时调用)
const mount = (container: HTMLElement | ShadowRoot, props: any) => {
// 将主应用 props 存入 React 上下文(方便子应用内部使用)
createRoot(container).render(
<StrictMode>
<Provider store={store}>
<HashRouter>
<App {...props} />
</HashRouter>
</Provider>
</StrictMode>
);
};
// 判断是否在 Wujie 微前端环境中
if (window.$wujie) {
mount(document.getElementById("root")!, window.$wujie.props);
} else {
// 独立运行环境(正常启动)
mount(document.getElementById("root")!, {
mainAppName: "独立运行",
token: "local-token",
});
}
路由通信
在app.tsx文件中修改
子应用监听到父应用的路由发生了改变,立即进行路由跳转
import { router } from "./router/createRouteConfig";
import { useNavigate, useRoutes } from "react-router-dom";
import useLocationChange from "./router/useLocationChange";
import routerListener from "./router/routerListener";
import "./style/index.css";
import { useEffect } from "react";
const App = function (props: any) {
const elements = useRoutes(router);
const navigate = useNavigate();
useEffect(() => {
const wujieBus = window.$wujie?.bus;
const routeChangeHandler = (path: string) => {
navigate(path);
};
wujieBus?.$on("routeChange", routeChangeHandler);
return () => {
wujieBus?.$off("routeChange", routeChangeHandler);
};
}, [navigate]);
useLocationChange((to, from) => {
routerListener(navigate, to, from);
});
return elements;
};
export default App;
嵌入的子页面
新建立一个文件用于放嵌入的子页面,且在该子页面中还可以向父应用通信
const wujieDemo1 = () => {
return (
<div>
<h1>我是子应用(react)的wujieDemo1</h1>
<button onClick={() => window.$wujie?.bus.$emit("subAppData", "我是子应用数据")}>向主应用提交数据</button>
</div>
);
};
export default wujieDemo1;
路由
新建路由用于对应上面的子页面
其中需要注意的是,路由的path需要对应父应用路由上的subAppPath
......
{
name: "wujieDemo1",
path: "/wujieDemo1",
component: lazy(() => import("../page/wujiePage/wujieDemo1/index")),
isMenu: false,
},
......
接口响应拦截器
在响应拦截器中,主要是针对未登录的场景,在未登录时,告知父应用
这里也做了运行环境的判断,用于判断是进入子应用的登录页面还是父应用的登录页面
// 将方法封装成一个函数
const http = async (config: IAxiosParam): Promise<any> => {
return request(config)
.then((res: IResponse) => {
switch (res.code) {
case ResCode.notLogin:
// 未登录
if (window.$wujie) {
window.$wujie?.bus.$emit("subAppData", {
type: "noLogin"
})
} else {
window.location.href = "/login";
}
break;
}
if (res.code !== 0 && !config.noAlert) {
// 异常提示
alert(res.msg || "出现问题啦~");
return;
}
return config.needRes ? res : res.data;
})
.catch((res) => {
return Promise.reject(res);
});
};
总结
这里我完成了一个基础的demo,在时间的应用还有一些需要注意或优化的点:
- 子应用的运行地址可配置化
- 子应用的预加载与保活
- 多个子应用的配置
后续可根据自己的实际场景来配置