当前位置: 首页 > article >正文

【无界】微前端技术应用

主应用搭建

初始化

首先创建一个主应用,我这里使用 vite + vue3 来作为主应用

yarn create vite

image-20250115111645109

  • 依次输入项目名称:base-main
  • 选择项目框架:vue3
  • 使用 TypeScript

启动项目,看到如下界面表示项目初始化成功

image-20250115111800441

引入无界

无界官方文档

我们主应用时 vue3,所以可以引入 wujie-vue3 ,其他版本的参考文档

yarn add wujie-vue3 

修改 main.ts,挂载 WujieVue,方便在全局直接使用 WujieVue 组件

import {createApp} from 'vue'
import './style.css'
import App from './App.vue'
import WujieVue from "wujie-vue3";


const app = createApp(App)
app.use(WujieVue)
app.mount('#app')

创建子应用

子应用我准备了两个

  • vue2应用(自行搭建)
  • React应用

React应用搭建

全局安装 create-react-app

npm install -g create-react-app

创建一个新的React项目

create-react-app react-sub

引入React路由

yarn add react-router-dom@6

新建 src/router/index.js

import { createBrowserRouter } from 'react-router-dom';
import Layout from '../components/Layout';
import Home from '../pages/Home';
import About from '../pages/About';

// 路由配置
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        index: true,
        element: <Home />
      },
      {
        path: 'about',
        element: <About />
      }
    ]
  }
]);

export default router;

修改 index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
// import App from './App';
import {RouterProvider} from 'react-router-dom';
import router from './router';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
    <RouterProvider router={router}></RouterProvider>
);

Layout 组件内容

react-sub\src\components\Layout.jsx

import React from "react";
import { Outlet } from "react-router-dom";

const Layout = () => {
  return (
    <Outlet />
  );
};

export default Layout;

Home 组件内容

import React from "react";
import styles from "../styles/pages/Home.module.css";

const Home = () => {
  const byWujie = window.__POWERED_BY_WUJIE__;

  return (
    <>
      <div className={styles["home-container"]}>
        <h1>欢迎来到React子应用</h1>
        {
          byWujie ? <div>处于无界微前端</div> : <div>处于独立运行</div>
        }
      </div>
    </>
  );
};

export default Home;

此时启动项目查看

image-20250115134215051

主应用引入React子应用

在主应用中修改 App.vue

<script setup lang="ts"></script>

<template>
  <div class="home-container">
    <h1>欢迎来到主应用</h1>
    <div class="react-sub-container">
      <h2>React子应用</h2>
      <WujieVue name="react-sub" url="http://localhost:3000" :sync="true"/>
    </div>
  </div>
</template>

<style scoped>
.home-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
}
.home-container h1 {
  font-size: 24px;
  font-weight: bold;
}
.react-sub-container {
  width: 50%;
  height: 50%;
  border: 1px solid #000;
  margin-top: 20px;
}
.react-sub-container h2 {
  font-size: 18px;
  font-weight: bold;
}
</style>

直接使用 WujieVue 标签,传入一个 name 名称,子应用的URL,:sync=“true” 表示同步子应用的路由,页面刷新仍可以保持子应用的地址

此时再启动主应用,查看页面,可以发现 React 项目中已经判断出处于微前端系统中

image-20250115134545834

应用通信

子应用给主应用通信

我们实现子应用设置网页标题同步给主应用,主应用来改变网页标题

React项目添加如下代码

useEffect(() => {
    window.$wujie?.bus.$emit("changeTitle", "React子应用");
}, []);

然后主应用中监听 changeTitle 方法

import { createApp } from "vue";
import "./style.css";
import App from "./App.vue";
import WujieVue from "wujie-vue3";
const { bus } = WujieVue;

const app = createApp(App);
app.use(WujieVue);
app.mount("#app");

// 监听子应用发送的消息
bus.$on("changeTitle", function (title: string) {
  console.log("主应用发送的消息", title);
  document.title = title;
});

主应用给子应用通信

主应用添加代码,通过 bus.$emit 发送 changeSubTitle 事件

<script setup lang="ts">
import WujieVue from "wujie-vue3";
const { bus } = WujieVue;
import { ref } from "vue";

const subTitle = ref("");
const changeSubTitle = () => {
  bus.$emit("changeSubTitle", subTitle.value);
};
</script>

<template>
  <div class="home-container">
    <h1>欢迎来到主应用</h1>
    <div class="react-sub-container">
      <h2>React子应用</h2>
      <div>
        <input v-model="subTitle" />
        <button @click="changeSubTitle">修改子应用标题</button>
      </div>
      <hr/>
      <WujieVue name="react-sub" url="http://localhost:3000" :sync="true" />
    </div>
  </div>
</template>

子应用监听 changeSubTitle

import React from "react";
import styles from "../styles/pages/Home.module.css";
import { useEffect, useState } from "react";

const Home = () => {
  const byWujie = window.__POWERED_BY_WUJIE__;
  const [subTitle, setSubTitle] = useState("默认的子应用标题");

  useEffect(() => {
    // 发送消息给主应用
    window.$wujie?.bus.$emit("changeTitle", "React子应用");

    // 监听主应用发送的消息
    window.$wujie?.bus.$on("changeSubTitle", (title) => {
      setSubTitle(title);
    });
  }, []);

  return (
    <div className={styles["home-container"]}>
      <h1>{subTitle}</h1>
      {byWujie ? <div>处于无界微前端</div> : <div>处于独立运行</div>}
    </div>
  );
};

export default Home;

查看效果

wujie1

应用嵌套

多个子应用之间也可以互相嵌套引用,例如在vue2子应用中嵌套React应用

首先在 vue2 应用中添加无界依赖

npm install wujie-vue2

然后再 main.js 中全局挂载 WujieVue 组件

import WujieVue from 'wujie-vue2'

Vue.use(WujieVue)

接着就可以像在主应用中一样,在需要的地方直接添加其他子应用的地址

<!--引用React项目-->
<WujieVue
  name="react-sub"
  url="http://localhost:3001"
  :sync="true"
  width="100%"
  height="100%"
  :props="wujieProps"
/>
data(){
    return{
        wujieProps: {
          token: localStorage.getItem('sysToken'),
          locale: localStorage.getItem('locale'),
          path: '/workbenches',
        },
    }
}

我们可以通过props给子应用传递参数,在子应用中,通过下面的方式获取参数

window.$wujie?.props

例如下面的案例,我们在React子应用的请求拦截器上从props获取vue2应用传递过来的token,然后再每次请求时都携带这个token,实现鉴权信息共享

// 请求拦截器
request.interceptors.request.use(
  (config) => {
    // 从 localStorage 获取 token
    config.headers["Authorization"] = window.$wujie?.props.token || "";
    config.headers["X-Locale"] = window.$wujie?.props.locale || "";
    config.headers["X-TraceId"] = "132456";
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

一次启动多个项目

首先在主应用安装chalk,chalk的作用是美化控制台输出的一个工具

yarn add chalk@4

然后在主应用的根目录添加 start.js 脚本文件

const { exec } = require("child_process");
const path = require("path");
const chalk = require("chalk");

// 定义项目路径
const mainPath = __dirname;
const reactPath = path.join(__dirname, "../react-sub");

// 定义应用信息
const apps = [
  {
    name: "主应用",
    path: mainPath,
    port: 5173,
    command: "yarn dev",
  },
  {
    name: "React子应用",
    path: reactPath,
    port: 3001,
    command: "yarn start",
  },
];

// 美化边框
const box = {
  topLeft: "╭",
  topRight: "╮",
  bottomLeft: "╰",
  bottomRight: "╯",
  horizontal: "─",
  vertical: "│",
};

// 创建分隔线
const createLine = (width = 50) => box.horizontal.repeat(width);

// 创建带边框的文本框
const createBox = (text, width = 50) => {
  const padding = Math.max(0, width - text.length - 2);
  const leftPad = Math.floor(padding / 2);
  const rightPad = padding - leftPad;
  return [
    `${box.topLeft}${createLine(width)}${box.topRight}`,
    `${box.vertical}${" ".repeat(leftPad)}${text}${" ".repeat(rightPad)}${
      box.vertical
    }`,
    `${box.bottomLeft}${createLine(width)}${box.bottomRight}`,
  ].join("\n");
};

// 检查是否需要安装依赖
const checkAndInstall = (projectPath) => {
  return new Promise((resolve, reject) => {
    if (!require("fs").existsSync(path.join(projectPath, "node_modules"))) {
      console.log(
        chalk.yellow(`📦 正在安装 ${path.basename(projectPath)} 依赖...`)
      );
      exec("yarn install", { cwd: projectPath }, (error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      });
    } else {
      resolve();
    }
  });
};

// 启动项目
const startProject = (app) => {
  return new Promise((resolve) => {
    console.log(chalk.cyan(`🚀 正在启动 ${app.name}...`));
    const child = exec(app.command, { cwd: app.path });

    let isStarted = false;

    child.stdout.on("data", (data) => {
      if (
        data.includes("App running at:") ||
        data.includes("Local:   http://") ||
        data.includes("localhost:") ||
        data.includes("successfully") ||
        data.includes("ready in")
      ) {
        if (!isStarted) {
          isStarted = true;
          console.log(
            chalk.green(`${app.name}已启动: http://localhost:${app.port}`)
          );
          resolve();
        }
      } else if (data.includes("ERROR")) {
        console.log(chalk.red(`❌ [${app.name}] ${data}`));
      }
    });

    child.stderr.on("data", (data) => {
      if (data.includes("ERROR")) {
        console.error(chalk.red(`❌ [${app.name}] ${data}`));
      }
    });

    setTimeout(() => {
      if (!isStarted) {
        console.log(chalk.yellow(`⚠️  ${app.name}启动时间较长,但仍在继续...`));
      }
    }, 10000);
  });
};

// 主函数
async function startAll() {
  console.clear();
  console.log(
    "\n" + chalk.blue(createBox(" 🌟 无界微前端启动程序 ", 48)) + "\n"
  );

  try {
    console.log(chalk.blue("📋 [1/2] 检查依赖..."));
    await Promise.all(apps.map((app) => checkAndInstall(app.path)));

    console.log(chalk.blue("\n🚀 [2/2] 启动应用...\n"));
    await Promise.all(apps.map((app) => startProject(app)));

    // 创建应用信息表格
    console.log("\n" + chalk.green(createBox(" ✅ 所有应用启动成功 ", 48)));
    console.log(chalk.white("\n📌 应用访问地址:"));
    console.log(chalk.gray(createLine(48)));

    apps.forEach((app) => {
      console.log(
        chalk.cyan(`  ${app.name.padEnd(12)}`) +
          chalk.yellow(`➜  http://localhost:${app.port}`)
      );
    });

    console.log(chalk.gray(createLine(48)));
    console.log(
      chalk.gray("\n💡 提示:主应用已集成所有子应用,访问主应用即可\n")
    );
  } catch (error) {
    console.error(chalk.red("\n❌ 启动失败:"), error);
    process.exit(1);
  }
}

startAll();

我们只需要维护好 apps 数组中的启动命令即可

然后在主应用的 package.json 中添加一个启动命令

"scripts": {
    "start": "node start.js"
},

现在运行启动命令,就可以同时把多个项目启动起来

yarn start

image-20250116153005618


http://www.kler.cn/a/511825.html

相关文章:

  • 基于SpringBoot+Vue的智慧动物园管理系统的设计与实现
  • 2025年1月17日(点亮三色LED)
  • 2024年博客之星年度评选—创作影响力评审入围名单公布
  • 如何通过 Apache Airflow 将数据导入 Elasticsearch
  • Jenkins-Pipeline简述
  • 基于Python+Gurobi的库存分配问题建模求解
  • 【大数据】机器学习----------降维与度量学习
  • 【自动驾驶BEV感知之tesla发展历程】
  • git命令手册
  • Ubuntu 24.04 LTS 更改软件源
  • 故障诊断 | BWO白鲸算法优化KELM故障诊断(Matlab)
  • ARP 表、MAC 表、路由表、跨网段 ARP
  • (二)afsim第三方库编译(qt编译)
  • K8S 集群搭建和访问 Kubernetes 仪表板(Dashboard)
  • Java高频面试之SE-15
  • DenseNet-密集连接卷积网络
  • 服务器硬盘RAID速度分析
  • 【算法】集合List和队列
  • 第二十四课 Vue中子组件调用父组件数据
  • 从 Spark 到 StarRocks:实现58同城湖仓一体架构的高效转型
  • 算法日记4:796. 子矩阵的和(二维前缀和)
  • 前端炫酷动画--图片(一)
  • 2024年博客之星主题创作|猫头虎分享AI技术洞察:2025年AI发展趋势前瞻与展望
  • 火狐浏览器Firefox一些配置
  • C# 可空值类型
  • 在视频汇聚平台EasyNVR平台中使用RTSP拉流的具体步骤