【无界】微前端技术应用
主应用搭建
初始化
首先创建一个主应用,我这里使用 vite + vue3 来作为主应用
yarn create vite
- 依次输入项目名称:base-main
- 选择项目框架:vue3
- 使用 TypeScript
启动项目,看到如下界面表示项目初始化成功
引入无界
无界官方文档
我们主应用时 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;
此时启动项目查看
主应用引入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 项目中已经判断出处于微前端系统中
应用通信
子应用给主应用通信
我们实现子应用设置网页标题同步给主应用,主应用来改变网页标题
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;
查看效果
应用嵌套
多个子应用之间也可以互相嵌套引用,例如在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