普通视图

发现新文章,点击刷新页面。
昨天 — 2025年11月30日首页

关于微前端框架wujie的一次企业级应用实践demo?

作者 寻找光_sxy
2025年11月30日 13:33

前言

本文将介绍我一种wujie的一次具体的应用,包括使用的场景、方式等等,完成一个具体的demo;

为什么要用微前端

事情是这样的,我们之前的业务有一个vue3+ts+vite的后台项目,后来公司决定新开发一个新的业务线,但是由于人力有限,如果重新搭建一个新的后台时间和人力成本较大都,尤其是其中的权限登录功能的设计都比较复杂,所以我们综合考虑,有没有一种可以直接用旧后台的权限和登录功能,然后其它功能完全隔离的,且旧后台和新后台可用两个部门的人来开发,可以独立开发、测试、部署,甚至技术栈也可以不受影响呢?这里我们想到了微前端方案;

微前端方案选择

我们经过调研,目光逐步瞄向了两种微前端的方案:无界乾坤

对比我们的业务,经过调研发现无界相比于乾坤更有优势:

  • 1、对旧后台项目影响较小,侵入程度低:只需要在旧有后台的项目上新起page页,以及新增一个路由即可;
  • 2、可单独开发、部署:子应用可以单独开发、部署,也可以使用一个全新的技术栈,即使生产环境无界挂了,出现问题了,也可以直接访问子应用;

综上两种原因,我们决定使用无界的方案;

怎么用无界(demo演示)

我们的主应用是vue3,这里将子应用通过菜单栏的形式嵌入到父应用中间,点击菜单即可进入到子应用

登录场景,在子应用请求时,若发现登录失效,通过子组件通信window.$wujie.bus.$emit('notLogin')向父应用传递未登录消息,父应用执行后续逻辑

权限逻辑,天然就互通,当子应用的菜单权限在某些角色下不可见时,在父应用下直接隐藏掉菜单就行;如果是子应用下按钮权限等功能权限时,可在子应用单独再次调用权限接口,或通过父子应用通信方式获取权限信息 image.png

具体步骤

父应用改造

  • 下载新依赖
  • wujie相关文件
  • 路由 image.png

下载相关依赖

pnpm install wujie-vue3

创建wujie文件

用于补充wujie的相关逻辑:

  • wujietemplate相关属性
    • name: 子应用唯一标识
    • url: 子应用运行地址
    • props:向子应用传递的参数
  • 父子应用通信
    • 通知子应用路由发生改变
    • 通知子应用其他数据
    • 子应用告知父应用未登录
    • 子应用告知父应用其他信息 image.png
<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;

image.png

子应用改造

  • 运行环境判断
  • 路由通信
  • 嵌入子页面
  • 路由
  • 接口响应拦截器

image.png

运行环境判断

这里我们在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,在时间的应用还有一些需要注意或优化的点:

  • 子应用的运行地址可配置化
  • 子应用的预加载与保活
  • 多个子应用的配置

后续可根据自己的实际场景来配置

❌
❌