- Update yarn.lock - Add project implementation docs in docs/ - Add personal internship experience notes in 实习讲解/
404 lines
17 KiB
Markdown
404 lines
17 KiB
Markdown
# 微前端架构 MicroApps 模块化开发 — 面试版
|
||
|
||
> 简历原话:**"采用微前端架构 MicroApps 支持模块化开发"**
|
||
>
|
||
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
|
||
|
||
---
|
||
|
||
## 一、先搞清楚:这句话到底是什么意思?
|
||
|
||
拆成三部分理解:
|
||
|
||
| 关键词 | 含义 | 项目中对应 |
|
||
|--------|------|------------|
|
||
| **微前端** | 把一个大前端应用拆成多个小应用,每个小应用独立开发、独立部署 | 主应用(态势感知平台)+ 3 个子应用(首页、用户中心、AI Copilot) |
|
||
| **MicroApps** | 具体使用的微前端框架名,全称 `@micro-zoe/micro-app`(京东开源) | package.json 中声明的依赖,版本 1.0.0-rc.3 |
|
||
| **模块化开发** | 每个子应用由不同团队/人员独立开发,互不影响 | 3 个子应用各有独立的代码仓库、独立的域名、独立的部署地址 |
|
||
|
||
**一句话概括**:我用京东开源的 micro-app 框架,把平台拆成主应用 + 3 个子应用,实现了各模块独立开发、独立部署,同时通过消息通信保证它们协同工作。
|
||
|
||
---
|
||
|
||
## 二、为什么需要微前端?(面试必答)
|
||
|
||
### 没有微前端的问题
|
||
|
||
想象一下没有微前端的情况:
|
||
|
||
```
|
||
一个巨大的 Vue 项目,包含:
|
||
├── 首页/搜索功能(首页团队负责)
|
||
├── 用户中心/个人主页(用户团队负责)
|
||
├── AI Copilot 对话(AI 团队负责)
|
||
└── 态势感知/安全分析(你负责)
|
||
|
||
问题:
|
||
1. 所有人改同一个代码仓库,合并冲突不断
|
||
2. 一个模块出 bug,整个平台挂掉
|
||
3. 改一个小功能,要重新部署整个项目
|
||
4. 各团队技术栈想用不同的,但被绑死在一个项目里
|
||
```
|
||
|
||
### 用了微前端之后
|
||
|
||
```
|
||
主应用(壳):负责顶部导航栏、侧边栏、路由调度
|
||
├── 子应用1(首页团队的独立项目):负责首页/搜索
|
||
├── 子应用2(用户团队的独立项目):负责用户中心
|
||
├── 子应用3(AI团队的独立项目):负责 AI Copilot
|
||
└── 你自己开发的模块:直接写在主应用里(安全态势/生态/社区)
|
||
|
||
好处:
|
||
1. 各团队独立仓库、独立开发,互不干扰
|
||
2. 子应用挂了不影响主应用和其他子应用
|
||
3. 子应用可以独立部署,不用动主应用
|
||
4. 各子应用可以用不同的技术栈(虽然我们都是 Vue3)
|
||
```
|
||
|
||
---
|
||
|
||
## 三、项目里具体怎么用的?
|
||
|
||
### 3.1 整体架构图
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────┐
|
||
│ 主应用(你的项目) │
|
||
│ │
|
||
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
|
||
│ │ 顶部导航栏 │ │ 侧边菜单栏 │ │ 内容区域(路由) │ │
|
||
│ └──────────┘ └──────────┘ │ │ │
|
||
│ │ /explore → 子应用1 │ │
|
||
│ │ /user → 子应用2 │ │
|
||
│ │ /ai → 子应用3 │ │
|
||
│ │ /security → 你开发的 │ │
|
||
│ └───────────────────┘ │
|
||
└─────────────────────────────────────────────────────┘
|
||
│ │ │
|
||
▼ ▼ ▼
|
||
┌─────────────┐ ┌──────────┐ ┌──────────────┐
|
||
│ 子应用1 │ │ 子应用2 │ │ 子应用3 │
|
||
│ 首页/搜索 │ │ 用户中心 │ │ AI Copilot │
|
||
│ (独立域名) │ │ (独立域名) │ │ (独立域名) │
|
||
└─────────────┘ └──────────┘ └──────────────┘
|
||
```
|
||
|
||
### 3.2 3 个子应用分别是什么
|
||
|
||
| 子应用名 | 干什么用 | 独立域名(生产环境) |
|
||
|----------|----------|---------------------|
|
||
| `micoro-app-homeweb-app` | 首页、搜索、G-Star 开源榜 | `homepage-app.gitcode.com` |
|
||
| `user-center` | 用户个人主页、仓库、收藏、设置 | `usercenter-app.gitcode.com` |
|
||
| `copilot-app` | AI 智能助手对话 | `copilot-app.gitcode.com` |
|
||
|
||
每个子应用都有**4 套环境地址**(开发/测试/预发/生产),通过 `.env` 文件配置:
|
||
|
||
```bash
|
||
# .env.development(开发环境)
|
||
VITE_CHILD_HOMEWEB_HOST = 'https://test.gitcode.net/child/homeweb-app/'
|
||
VITE_USER_CENTER_HOST = 'https://test.gitcode.net/child/user-center/'
|
||
VITE_COPILOT_HOST = 'https://test.gitcode.net/child/aichat-app/'
|
||
|
||
# .env.production(生产环境)
|
||
VITE_CHILD_HOMEWEB_HOST = 'https://homepage-app.gitcode.com'
|
||
VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
|
||
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
|
||
```
|
||
|
||
---
|
||
|
||
## 四、怎么实现的?(面试核心)
|
||
|
||
### 4.1 怎么把子应用"嵌入"主应用
|
||
|
||
有两种方式,项目里都用了:
|
||
|
||
#### 方式一:声明式 — `<micro-app>` 标签(用于用户中心)
|
||
|
||
就像写 HTML 标签一样简单:
|
||
|
||
```vue
|
||
<!-- proxy.vue — 用户中心的代理组件 -->
|
||
<template>
|
||
<micro-app name="user-center" :url="url" iframe baseroute="/"></micro-app>
|
||
</template>
|
||
|
||
<script setup>
|
||
const url = import.meta.env.VITE_USER_CENTER_HOST; // 从环境变量取子应用地址
|
||
</script>
|
||
```
|
||
|
||
**意思是**:在页面上放一个 `<micro-app>` 标签,告诉它子应用叫 `user-center`,地址是 `url`,用 iframe 模式加载。就这么简单。
|
||
|
||
#### 方式二:命令式 — `microApp.renderApp()`(用于首页和 AI)
|
||
|
||
用 JavaScript 手动调用渲染:
|
||
|
||
```typescript
|
||
// proxy-homeweb.vue — 首页子应用的代理组件
|
||
onMounted(() => {
|
||
microApp.renderApp({
|
||
name: 'micoro-app-homeweb-app', // 子应用名
|
||
url: origin, // 子应用地址
|
||
container: '#homeweb-container', // 渲染到哪个 DOM 元素
|
||
data: { emitter, $router: router }, // 传给子应用的数据
|
||
iframe: true, // 用 iframe 隔离
|
||
baseroute: '/', // 路由前缀
|
||
});
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
microApp.unmountApp('micoro-app-homeweb-app'); // 页面离开时卸载子应用
|
||
});
|
||
```
|
||
|
||
**为什么两种方式都要用**:声明式更简单,但命令式更灵活,可以在渲染时传入更多数据(如事件总线、路由实例)。
|
||
|
||
### 4.2 子应用怎么和主应用"对话"?
|
||
|
||
微前端最难的部分不是加载子应用,而是**主应用和子应用之间的通信**。项目里用了 3 种通信方式:
|
||
|
||
#### 方式 1:`setData` — 主应用主动发消息给子应用
|
||
|
||
```typescript
|
||
// 主应用告诉用户中心"路由变了"
|
||
microApp.setData('user-center', {
|
||
type: 'application-route',
|
||
name: route.name,
|
||
params: route.params,
|
||
query: route.query
|
||
});
|
||
|
||
// 主应用告诉首页子应用"搜索关键词变了"
|
||
microApp.setData('micoro-app-homeweb-app', {
|
||
type: 'keyword-change',
|
||
keyword: searchKeyword
|
||
});
|
||
```
|
||
|
||
#### 方式 2:`addDataListener` — 主应用监听子应用的消息
|
||
|
||
```typescript
|
||
// 主应用监听用户中心发来的各种消息
|
||
microApp.addDataListener('user-center', (config) => {
|
||
// 子应用说"用户关注状态变了"
|
||
if (config.type === 'user_hasFollowed_update') {
|
||
otherStore.saveFollowed(config.data.status);
|
||
}
|
||
// 子应用说"用户资料更新了"
|
||
if (config.type === 'user_profile_update') {
|
||
saveAccountInfo(config.data);
|
||
}
|
||
// 子应用说"要跳转路由"
|
||
if (config.type === 'route-change') {
|
||
router.push({ name: config.name, params: config.params });
|
||
}
|
||
});
|
||
```
|
||
|
||
#### 方式 3:共享事件总线 — 通过 mitt 事件库
|
||
|
||
```typescript
|
||
// 主应用创建事件总线,传给子应用
|
||
microApp.renderApp({
|
||
name: 'micoro-app-homeweb-app',
|
||
data: {
|
||
emitter, // ← mitt 事件总线实例
|
||
$router: router // ← 路由实例
|
||
}
|
||
});
|
||
|
||
// 子应用通过事件总线通知主应用"路由变了"
|
||
// 主应用监听这个事件,同步到自己的路由
|
||
addEventListener('microRouterChange', (to) => {
|
||
router.push(to.fullPath);
|
||
});
|
||
```
|
||
|
||
**通信方式总结**:
|
||
|
||
```
|
||
主应用 → 子应用:microApp.setData('子应用名', 数据)
|
||
子应用 → 主应用:microApp.addDataListener('子应用名', 回调)
|
||
双向通信:共享 emitter 事件总线
|
||
```
|
||
|
||
### 4.3 路由怎么同步?
|
||
|
||
主应用和子应用各有各的路由,需要保持同步:
|
||
|
||
```typescript
|
||
// microAppConfig.ts — 路由同步工具
|
||
|
||
// 1. 子应用路由变了 → 同步到主应用
|
||
addEventListener('microRouterChange', (to) => {
|
||
router.push(to.fullPath); // 主应用跟着跳
|
||
});
|
||
|
||
// 2. 主应用路由变了 → 同步到子应用
|
||
router.beforeEach((to, from) => {
|
||
if (to.meta.micorApp) {
|
||
// 告诉子应用:你要跳到这个路径
|
||
to.meta.micorApp.forEach(appName => {
|
||
microApp.router.replace({ name: appName, path: to.fullPath });
|
||
});
|
||
}
|
||
});
|
||
```
|
||
|
||
**通俗理解**:就像两个人各开一辆车,要保持同一路线。主应用的路由变了,通知子应用跟上;子应用的路由变了,也通知主应用跟上。
|
||
|
||
### 4.4 页面加载优化
|
||
|
||
为了让子应用加载更快,做了两个优化:
|
||
|
||
#### 优化 1:DNS 预解析
|
||
|
||
```html
|
||
<!-- index.html — 提前解析子应用域名的 DNS -->
|
||
<link rel="preconnect" href="//homepage-app.gitcode.com" crossorigin />
|
||
<link rel="dns-prefetch" href="//homepage-app.gitcode.com">
|
||
<link rel="dns-prefetch" href="//usercenter-app.gitcode.com">
|
||
```
|
||
|
||
**通俗理解**:用户还没点到子应用的页面,浏览器就已经提前"打听"子应用域名的地址了,等真正加载时就更快。
|
||
|
||
#### 优化 2:开发环境跨域处理(Vite proxy)
|
||
|
||
子应用和主应用不在同一个域名下,但 micro-app 的 iframe 模式下,主子应用通信靠 `postMessage`(原生 API,天然支持跨域,不受 CORS 限制),所以**不需要额外配置 CORS**。
|
||
|
||
真正需要处理跨域的是**主应用调用后端 API**的场景。开发环境中,前端跑在 `localhost:443`,后端在 `http://222.20.126.217:8100`,域名不同会触发跨域。Vite 用 proxy 代理解决:
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
server: {
|
||
proxy: {
|
||
'/api': {
|
||
target: 'http://222.20.126.217:8100', // 后端地址
|
||
changeOrigin: true, // 修改 Origin 头,绕过同源策略
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**原理**:浏览器→Vite 开发服务器(同源,不跨域)→Vite 帮你转发到后端(服务器之间没有跨域限制)。生产环境则由 nginx 做反向代理,原理一样。
|
||
|
||
---
|
||
|
||
## 五、Vue 构建层面的适配
|
||
|
||
微前端框架需要在 Vue 构建工具中做一些特殊配置:
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
vue({
|
||
template: {
|
||
compilerOptions: {
|
||
isCustomElement: tag => /^micro-app/.test(tag)
|
||
// ← 告诉 Vue:遇到 <micro-app> 标签不要报错,它是自定义元素
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
**通俗理解**:Vue 默认不认识 `<micro-app>` 这个标签,会报警告。加上这个配置后,Vue 就知道"这是个自定义元素,不用管它"。
|
||
|
||
---
|
||
|
||
## 六、生命周期管理(防内存泄漏)
|
||
|
||
每个子应用的代理组件都遵循统一的生命周期:
|
||
|
||
```typescript
|
||
// 挂载时:加载子应用
|
||
onMounted(() => {
|
||
microApp.renderApp({ name: 'xxx', url: origin, container: '#xxx' });
|
||
});
|
||
|
||
// 卸载时:销毁子应用
|
||
onBeforeUnmount(() => {
|
||
microApp.unmountApp('xxx'); // 卸载子应用
|
||
microApp.clearDataListener(); // 清理数据监听器(避免内存泄漏)
|
||
});
|
||
```
|
||
|
||
**为什么重要**:如果不卸载,子应用的 DOM 事件监听器、定时器、网络请求都还在运行,会造成内存泄漏。
|
||
|
||
---
|
||
|
||
## 七、整体架构一句话总结
|
||
|
||
```
|
||
主应用(壳)
|
||
├── 顶部导航 + 侧边栏 + 路由调度(主应用负责)
|
||
├── 态势感知/生态/社区模块(直接写在主应用里)
|
||
└── 3 个子应用(通过 micro-app 框架嵌入)
|
||
├── 首页/搜索(独立仓库,独立域名)
|
||
├── 用户中心(独立仓库,独立域名)
|
||
└── AI Copilot(独立仓库,独立域名)
|
||
|
||
通信方式:setData(主→子)+ addDataListener(子→主)+ 事件总线(双向)
|
||
隔离方式:iframe 模式(样式/JS 完全隔离)
|
||
路由同步:双向监听,互相通知
|
||
```
|
||
|
||
---
|
||
|
||
## 八、面试问答准备
|
||
|
||
### Q1:"采用微前端架构 MicroApps 支持模块化开发"具体做了什么?
|
||
|
||
> 我们平台使用京东开源的 micro-app 框架,把整个应用拆成了主应用 + 3 个子应用。主应用负责导航栏、侧边栏和路由调度,3 个子应用(首页搜索、用户中心、AI Copilot)各自独立开发、独立部署。主应用通过 micro-app 框架将子应用嵌入到自己的页面中,使用 iframe 模式实现样式和 JS 的完全隔离,同时通过 setData、addDataListener 和事件总线三种方式实现主子应用之间的数据通信。
|
||
|
||
### Q2:为什么选择 micro-app 而不是 qiankun?
|
||
|
||
> micro-app 是京东开源的下一代微前端框架,相比 qiankun 有几个优势:第一,接入成本低,只需要一个 `<micro-app>` 标签就能加载子应用,不需要像 qiankun 那样改造子应用的入口文件和打包配置;第二,支持 iframe 模式,能实现更好的样式和 JS 隔离;第三,提供了 setData/addDataListener API 让主子应用通信更方便。
|
||
|
||
### Q3:主应用和子应用之间怎么通信?
|
||
|
||
> 我们用了三种方式:第一,主应用通过 `microApp.setData()` 主动推送数据给子应用,比如路由变化、搜索关键词;第二,主应用通过 `microApp.addDataListener()` 监听子应用发来的消息,比如用户关注状态更新、路由跳转请求;第三,通过共享的 mitt 事件总线实现双向通信,主应用创建事件总线实例后通过 renderApp 的 data 参数传给子应用。路由同步方面,主应用和子应用互相监听路由变化,保持双向同步。
|
||
|
||
### Q4:微前端的隔离和跨域怎么处理的?
|
||
|
||
> 我们用的是 iframe 模式(`iframe: true`),每个子应用运行在独立的 iframe 中。iframe 天然提供了 JS 作用域隔离和 CSS 样式隔离,子应用的全局变量、样式都不会污染主应用。跨域方面,iframe 模式下主子应用通信靠 `postMessage`,这是浏览器原生 API,天然支持跨域,不需要额外配置 CORS。开发环境主应用调后端 API 的跨域问题,则通过 Vite 的 proxy 代理解决。
|
||
|
||
### Q5:子应用加载会不会很慢?
|
||
|
||
> 我们做了两个优化:第一,在 index.html 中通过 DNS prefetch 提前解析子应用域名,用户还没点到子应用页面时浏览器就已经在解析域名了;第二,子应用只在用户真正访问对应路由时才加载,不是一进主应用就把所有子应用都加载出来,是按需加载的。
|
||
|
||
### Q6:开发环境和生产环境的子应用地址不一样怎么办?
|
||
|
||
> 通过 Vite 的环境变量机制管理。每个环境(开发/测试/预发/生产)都有独立的 `.env` 文件,里面配置了对应环境的子应用 URL。代码中通过 `import.meta.env.VITE_CHILD_HOMEWEB_HOST` 读取,构建时自动替换为对应环境的地址,不需要改代码。
|
||
|
||
---
|
||
|
||
## 九、关键数字(面试时用)
|
||
|
||
| 数据 | 数字 |
|
||
|------|------|
|
||
| 微前端框架 | micro-app 1.0.0-rc.3(京东开源) |
|
||
| 子应用数量 | 3 个 |
|
||
| 通信方式 | 3 种(setData / addDataListener / 事件总线) |
|
||
| 隔离方式 | iframe 模式(完全隔离) |
|
||
| 环境配置 | 4 套(开发/测试/预发/生产) |
|
||
| 代理组件 | 3 个(每个子应用对应一个) |
|
||
|
||
---
|
||
|
||
## 十、涉及的源码文件(需要看的时候查)
|
||
|
||
| 做什么 | 文件在哪 |
|
||
|--------|----------|
|
||
| 框架依赖声明 | `package.json`(@micro-zoe/micro-app) |
|
||
| 构建配置(自定义元素+CORS) | `vite.config.ts` |
|
||
| DNS 预解析 | `index.html` |
|
||
| 环境变量(子应用地址) | `.env.development` / `.env.production` 等 |
|
||
| 首页子应用代理 | `src/views/micro-page/proxy-homeweb.vue` |
|
||
| 用户中心子应用代理 | `src/views/UserCenter/proxy.vue` |
|
||
| AI Copilot 子应用代理 | `src/views/micro-page/proxy-copilot.vue` |
|
||
| 路由同步工具 | `src/utils/microAppConfig.ts` |
|
||
| 事件总线 | `src/utils/eventBus.ts` |
|
||
| 路由配置(带子应用标记) | `src/router/config/home.ts` / `user.ts` / `setting.ts` |
|
||
| 布局中的通信监听 | `src/layouts/v2/DefaultLayout/index.vue` |
|