主应用搭建

初始化

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

1
yarn create vite

image-20250115111645109

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

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

image-20250115111800441

引入无界

无界官方文档

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

1
yarn add wujie-vue3 

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

1
2
3
4
5
6
7
8
9
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

1
npm install -g create-react-app

创建一个新的React项目

1
create-react-app react-sub

引入React路由

1
yarn add react-router-dom@6

新建 src/router/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
5
6
7
8
9
10
import React from "react";
import { Outlet } from "react-router-dom";

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

export default Layout;

Home 组件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<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项目添加如下代码

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

然后主应用中监听 changeTitle 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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 应用中添加无界依赖

1
npm install wujie-vue2

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

1
2
3
import WujieVue from 'wujie-vue2'

Vue.use(WujieVue)

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

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

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

1
window.$wujie?.props

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

1
2
3
4
5
6
7
8
9
10
11
12
13
// 请求拦截器
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的作用是美化控制台输出的一个工具

1
yarn add chalk@4

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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 中添加一个启动命令

1
2
3
"scripts": {
"start": "node start.js"
},

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

1
yarn start

image-20250116153005618