配置好了vue2的子应用接入

This commit is contained in:
2026-06-20 23:42:22 +08:00
commit 2499043df4
19 changed files with 2648 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
*.local
.DS_Store

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

8
.idea/microapp-main.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/microapp-main.iml" filepath="$PROJECT_DIR$/.idea/microapp-main.iml" />
</modules>
</component>
</project>

574
docs/micro-app面试题.md Normal file
View File

@@ -0,0 +1,574 @@
# @micro-zoe/micro-app 微前端面试题
> 基于 Vue 3 + Vite 主应用搭建实战经验整理,涵盖从项目初始化、配置、到 bug 排查的全流程。
---
## 一、基础概念
### Q1什么是微前端micro-app 相比其他方案有什么优势?
**答:**
微前端是一种将多个独立的前端应用组合成一个统一应用的架构模式。每个子应用可以独立开发、独立部署、使用不同的技术栈。
`@micro-zoe/micro-app`(京东出品)的核心优势:
- **零依赖**:不依赖 single-spa完全自研
- **类 WebComponent**:使用 `<micro-app>` 标签加载子应用,对开发者透明
- **双沙箱模式**:同时支持 with 沙箱和 iframe 沙箱
- **不限制技术栈**主应用和子应用可以任意组合Vue 2/3、React、原生 HTML 等)
- **兼容 Vite**:通过 iframe 模式完美支持 Vite 构建的子应用
| 对比维度 | micro-app | qiankun |
|----------|-----------|---------|
| 接入成本 | 低(类 WebComponent | 中(需改造子应用入口) |
| Vite 支持 | 原生支持iframe 模式) | 需额外配置 |
| 沙箱方案 | with + iframe 双模式 | 基于 Proxy 的沙箱 |
| 包体积 | ~300KB | ~100KB |
---
### Q2micro-app 主应用和子应用之间是如何隔离的?
**答:**
micro-app 提供**三层隔离**
1. **样式隔离**`disable-scopecss`,默认开启):
- 为子应用的每个样式规则自动添加 `micro-app[name=xxx]` 前缀,限制样式作用域
2. **JS 沙箱隔离**`disable-sandbox`,默认开启):
- **with 沙箱**:通过 `with(window.proxy)` + `new Function()` 执行子应用 JS将子应用的全局变量操作代理到隔离的 microWindow 上
- **iframe 沙箱**:将子应用放入 iframe 中运行,利用浏览器原生的 iframe 隔离
3. **元素隔离**`shadowDOM`,可选):
- 将子应用放入 Shadow DOM彻底隔离 DOM 树
---
## 二、项目搭建
### Q3使用 Vue 3 + Vite 搭建 micro-app 主应用需要哪些步骤?
**答:**
核心 4 步:
```ts
// 1. 安装依赖
npm install @micro-zoe/micro-app vue-router@4
// 2. vite.config.ts — 将 <micro-app> 注册为自定义元素
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => /^micro-app/.test(tag)
}
}
})
],
server: { port: 8080 }
})
// 3. main.ts — 在 Vue 实例化前启动 micro-app
import microApp from '@micro-zoe/micro-app'
microApp.start({ /* 全局配置 */ })
createApp(App).use(router).mount('#app')
// 4. 页面中使用 <micro-app> 标签加载子应用
<micro-app name="my-app" url="http://localhost:3000/" baseroute="/my-app" />
```
---
### Q4为什么要在 `vite.config.ts` 中配置 `isCustomElement`
**答:**
`<micro-app>` 是一个**自定义 HTML 标签**,不是 Vue 组件。Vue 模板编译器默认会把所有非标准 HTML 标签当作 Vue 组件去寻找,找不到就会报警告:
```
[Vue warn]: Failed to resolve component: micro-app
```
通过 `isCustomElement: (tag) => /^micro-app/.test(tag)` 告诉编译器:以 `micro-app` 开头的标签是自定义元素,跳过 Vue 组件解析。
**延伸**:如果子应用中也使用了 `<micro-app>`(嵌套微前端),同样需要配置。
---
### Q5主应用路由如何设计才能正确匹配子应用
**答:**
使用 Vue Router 的**通配符** `:page*` 匹配子应用所有内部路由:
```ts
const routes = [
{ path: '/', redirect: '/home' },
{ path: '/home', component: () => import('@/views/Home.vue') },
// 关键::page* 匹配 /child-app 及其下所有子路由
{ path: '/child-app/:page*', component: () => import('@/views/ChildApp.vue') }
]
```
**为什么用 `:page*` 而不是 `*`**
- `*` 只在 Vue Router 3 中可用
- Vue Router 4 中必须使用 `:page*`(或 `:pathMatch(.*)*`)来匹配多级路径
- 这样 `/child-app/page1``/child-app/a/b/c` 都能被正确捕获
---
## 三、沙箱机制(重点 Bug 来源)
### Q6micro-app 的两种沙箱模式有什么区别?如何选择?
**答:**
| 特性 | with 沙箱 | iframe 沙箱 |
|------|-----------|-------------|
| 配置 | `iframe: false`(默认) | `iframe: true` |
| 原理 | `with(window.proxy)` + `new Function()` | 浏览器原生 iframe |
| ES Module | ❌ 不支持 `import`/`export` | ✅ 原生支持 |
| 性能 | 较快(无额外渲染) | 略慢(需创建 iframe |
| 隔离性 | 中等Proxy 代理) | 强(浏览器原生隔离) |
| 调试体验 | 较好(同 document | 较差(跨 iframe |
| 适用场景 | Webpack 构建的 UMD 应用 | **Vite 构建的应用** |
**选择原则:**
- **Vite 子应用 → 必须 `iframe: true`**
- Webpack UMD 子应用 → `iframe: false` 即可
- 对样式/JS 隔离要求极高的场景 → `iframe: true`
---
### Q7遇到 `Cannot use import statement outside a module` 报错,是什么原因?如何解决?
**答:**
这是 micro-app 主应用对接 Vite 子应用时**必踩的坑**。
**报错堆栈:**
```
[micro-app] app vue2-app: {
error: SyntaxError: Cannot use import statement outside a module
at new Function (<anonymous>)
}
```
**根因分析:**
1. Vite 开发服务器输出的 JS 是 `<script type="module">` 格式的 ES Module
2. micro-app 默认的 **with 沙箱** 使用 `new Function()` 来执行子应用的 JS 代码
3. `new Function()` 创建的函数运行在「非模块」作用域,无法解析 `import`/`export` 语法
**解决方案:**
```ts
// src/config/subApps.ts
{
name: 'vue2-app',
url: 'http://localhost:5173/',
baseroute: '/child-app',
iframe: true, // ← 关键:开启 iframe 沙箱
}
```
**为什么 iframe 能解决?**
iframe 是浏览器原生的独立文档环境Vite 的 `<script type="module">` 在 iframe 中可以正常执行。
---
### Q8`new Function()` 和 `eval()` 有什么区别?为什么 micro-app 用 `new Function()`
**答:**
| 特性 | `eval()` | `new Function()` |
|------|----------|-------------------|
| 作用域 | 访问当前局部作用域 | 只能访问全局作用域 |
| 性能 | 慢(影响局部变量优化) | 较快(引擎可优化) |
| 安全性 | 较低(泄漏局部变量) | 较高(作用域隔离) |
micro-app 选择 `new Function()` 的原因:
- 它创建的函数只在全局作用域执行,配合 `with(window.proxy)` 可以精确控制子应用对全局变量的访问
- `eval()` 会渗透到调用方的局部变量,无法做到完全隔离
**但局限性也很明显:** `new Function()` 无法处理 ES Module 语法,这就是为什么 Vite 子应用必须使用 iframe 模式。
---
## 四、TypeScript 类型问题
### Q9micro-app 的 `lifeCycles` 回调中,为什么 `e.name` 会报 TS 类型错误?
**答:**
这是 TypeScript 类型定义与实际使用的差异问题。
**错误代码:**
```ts
lifeCycles: {
created(e) {
console.log(e.name) // TS2339: Property 'name' does not exist on type 'CustomEvent<any>'
}
}
```
**根因:**
根据 `@micro-zoe/micro-app` 的类型定义文件 `global.d.ts`
```ts
interface lifeCyclesType {
created?(e: CustomEvent, appName: string): void
beforemount?(e: CustomEvent, appName: string): void
// ...
}
```
生命周期回调接收**两个参数**:第一个是 `CustomEvent` 事件对象,**第二个才是 `appName` 字符串**。
**正确写法:**
```ts
lifeCycles: {
created(_e, appName) {
console.log(`子应用 ${appName} 被创建`)
}
}
```
---
### Q10`vue-tsc --noEmit` 在 CI/CD 中有什么作用?我们遇到了什么类型错误?
**答:**
`vue-tsc --noEmit` 是 Vue 项目的类型检查命令:
- 只做类型检查,不输出编译产物
- 相当于 `tsc --noEmit`,但额外支持 `.vue` 文件
我们在项目中遇到了两类类型错误:
1. **lifeCycles 回调参数类型错误**`e.name` 不存在于 `CustomEvent` 类型 → 改用第二个参数 `appName`
2. **未使用的导入**`getSubAppConfig``ChildApp.vue` 中导入但未使用 → 移除无用导入
**配置建议:**`package.json` 中将类型检查加入构建流程:
```json
{
"scripts": {
"build": "vue-tsc --noEmit && vite build"
}
}
```
---
## 五、子应用容器与通信
### Q11`<micro-app>` 标签有哪些核心属性?各有什么作用?
**答:**
| 属性 | 类型 | 说明 |
|------|------|------|
| `name` | `string` | **必填**,子应用唯一标识,字母开头 |
| `url` | `string` | **必填**子应用入口地址HTML 路径) |
| `baseroute` | `string` | 路由前缀,主应用分配给子应用的路由基路径 |
| `iframe` | `boolean` | 是否使用 iframe 沙箱Vite 子应用必须开启) |
| `keep-alive` | `boolean` | 保活子应用,切换路由时不销毁 |
| `router-mode` | `string` | 路由模式:`native`(原生同步)/ `native-scope`(带作用域) |
| `disable-scopecss` | `boolean` | 禁用样式隔离 |
| `disable-sandbox` | `boolean` | 禁用 JS 沙箱(调试时可用) |
| `data` | `object` | 向子应用传递的初始数据 |
| `shadowDOM` | `boolean` | 是否使用 Shadow DOM |
---
### Q12主应用如何向子应用传递数据有哪几种方式
**答:**
三种方式:
**方式一:通过 `data` 属性(初始化传参)**
```vue
<micro-app name="app1" url="..." :data="{ token: 'abc', userId: 1 }" />
```
```ts
// 子应用接收
const data = window.microApp?.getData()
```
**方式二:通过 `microApp.setData()` API动态传参**
```ts
import microApp from '@micro-zoe/micro-app'
microApp.setData('app1', { path: '/detail' })
```
```ts
// 子应用监听
window.microApp?.addDataListener((data) => {
console.log('收到数据:', data)
if (data.path) router.push(data.path)
}, true) // autoTrigger = true初始化时立即触发一次
```
**方式三:通过 `EventCenterForMicroApp`(双向通信)**
```ts
// 主应用
import { EventCenterForMicroApp } from '@micro-zoe/micro-app'
const eventCenter = new EventCenterForMicroApp('app1')
eventCenter.dispatch({ type: '通知', payload: {} })
```
```ts
// 子应用
window.microApp?.dispatch({ type: '回复', payload: {} })
```
---
### Q13`keep-alive` 保活机制的原理是什么?有什么注意事项?
**答:**
原理:当 `<micro-app>` 元素从 DOM 中移除时(如路由切换),不执行 `unmount()`,而是调用 `hiddenKeepAliveApp()` 将子应用隐藏。当元素重新插入 DOM 时,调用 `showKeepAliveApp()` 恢复显示。
**优点:** 避免重复加载资源,保持子应用状态
**注意事项:**
- 保活的子应用仍然占用内存,需注意内存泄漏
- 子应用的定时器、事件监听需要在 `unmount` 生命周期中手动清理
-`microApp.getActiveApps({ excludeHiddenApp: false })` 可以查看所有活跃应用(包括隐藏的保活应用)
---
## 六、生命周期
### Q14micro-app 子应用加载经历了哪些生命周期?顺序是什么?
**答:**
```
主应用生命周期(全局 lifeCycles
created → beforemount → (beforeshow) → mounted → (afterhidden) → unmount
子应用内部生命周期window 上的钩子):
window.mount() → window.unmount()
```
**完整流程:**
| 阶段 | 全局回调 | 说明 |
|------|----------|------|
| 资源加载完成 | `created` | HTML 和静态资源已下载 |
| 挂载前 | `beforemount` | 沙箱准备就绪,即将渲染 |
| 挂载前keep-alive 恢复) | `beforeshow` | 保活应用恢复显示前触发 |
| 渲染完成 | `mounted` | 子应用已渲染到容器中 |
| 保活隐藏后 | `afterhidden` | 保活应用被隐藏后触发 |
| 卸载 | `unmount` | 子应用被销毁 |
| 报错 | `error` | 加载或执行出错时触发 |
---
### Q15全局 `lifeCycles` 和子应用内部的 `mount/unmount` 有什么区别?
**答:**
| 维度 | 全局 lifeCycles | 子应用 mount/unmount |
|------|-----------------|---------------------|
| 定义位置 | 主应用 `microApp.start()` | 子应用 `window['micro-app-xxx']` |
| 关注点 | 框架层面的加载状态 | 子应用业务层面的渲染状态 |
| 参数 | `(e: CustomEvent, appName: string)` | 无参数(或接收主应用传递的配置) |
| 典型用途 | 全局 loading 控制、埋点 | 初始化 Vue Router、挂载根组件 |
```ts
// 子应用Vue 2入口 — 导出生命周期
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = {
mount() {
instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
},
unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
}
}
} else {
// 独立运行时直接渲染
new Vue({ router, render: h => h(App) }).$mount('#app')
}
```
---
## 七、Vue 2 子应用对接
### Q16Vue 2 Webpack 子应用需要做哪些改造才能接入 micro-app
**答:**
需要 4 处改造:
**1. webpack 配置 — UMD 输出**
```js
// vue.config.js 或 webpack.config.js
module.exports = {
output: {
libraryTarget: 'umd',
library: 'vue2App'
}
}
```
**2. devServer 配置 — 跨域**
```js
module.exports = {
devServer: {
port: 3000,
headers: {
'Access-Control-Allow-Origin': '*'
}
}
}
```
**3. 入口文件 — 导出生命周期**
```ts
let instance = null
function mount() {
instance = new Vue({ router, render: h => h(App) }).$mount('#child-app-root')
}
function unmount() {
instance.$destroy()
instance.$el.innerHTML = ''
}
if (window.__MICRO_APP_ENVIRONMENT__) {
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount }
} else {
mount()
}
```
**4. 路由 base — 动态适配**
```ts
const router = new VueRouter({
mode: 'history',
base: window.__MICRO_APP_BASE_ROUTE__ || '/',
routes
})
```
---
### Q17子应用挂载的 DOM 元素 id 为什么不能用 `#app`
**答:**
因为主应用已经使用了 `<div id="app"></div>`。如果子应用也挂载到 `#app`,会与主应用的根节点冲突,导致:
- 子应用覆盖主应用内容
- 样式污染
- 路由混乱
**正确做法:** 子应用使用独立的 id
```html
<!-- 子应用的 index.html -->
<div id="child-app-root"></div>
```
```ts
// 子应用入口
new Vue({ ... }).$mount('#child-app-root')
```
---
## 八、排错与调试
### Q18子应用加载白屏如何排查
**答:**
按以下顺序排查:
1. **检查跨域**:浏览器 Network 面板看子应用资源是否被 CORS 阻止 → 确认子应用 devServer 配置了 `Access-Control-Allow-Origin: *`
2. **检查控制台错误**
- `Cannot use import statement outside a module` → 子应用是 Vite 项目,需配置 `iframe: true`
- `Failed to resolve component: micro-app` → Vite 未配置 `isCustomElement`
- `Unexpected token '<'` → 子应用静态资源路径错误,检查 `url` 配置
3. **检查沙箱模式**:临时设置 `disable-sandbox: true` 确认是否是沙箱引起的问题
4. **检查子应用生命周期**:在 `created`/`mounted`/`error` 回调中打印日志,确认子应用加载到哪一步
5. **查看 micro-app 日志**:启用 `__DEV__` 模式可看到详细日志
---
### Q19为什么 Vite 子应用的静态资源会 404
**答:**
两个常见原因:
**1. 资源路径使用绝对路径:**
```html
<!-- 子应用中这样写会 404因为主应用在 localhost:8080 -->
<script src="/assets/index.js"></script>
<!-- 实际应该请求子应用 localhost:5173/assets/index.js -->
```
解决:子应用 Vite 配置 `base` 或使用相对路径
**2. `baseroute` 与 `base` 不匹配:**
```ts
// 主应用
<micro-app baseroute="/child-app" />
// 子应用 vite.config.ts
export default defineConfig({
base: '/child-app/' // 必须与 baseroute 一致
})
```
---
## 九、实战总结
### Q20这套微前端架构在生产环境中部署需要注意什么
**答:**
1. **子应用独立部署**:每个子应用有自己独立的 CI/CD 流水线,版本号独立管理
2. **统一资源路径管理**:通过环境变量管理各子应用的 URL
```ts
export const subApps = [{
name: 'vue2-app',
url: import.meta.env.VITE_CHILD_APP_URL || 'http://localhost:5173/',
baseroute: '/child-app',
}]
```
3. **Nginx 配置**
- 所有子应用必须配置 CORS 头
- 主应用和子应用建议部署在**同源**下(同协议、同域名、同端口),可避免大量跨域问题
- 如果异构部署,需要配置反向代理统一转发
4. **公共依赖提取**:将 Vue、Vue Router 等公共依赖配置为 external避免重复加载
```ts
// vite.config.ts
build: {
rollupOptions: {
external: ['vue', 'vue-router']
}
}
```
5. **灰度发布**:利用 `microApp.start()` 的 `plugins.global` 机制,可以在加载子应用 HTML 时做 A/B 测试控制
6. **错误兜底**:配置全局 `error` 回调 + 子应用容器的 `@error` 事件,展示友好的降级页面
---
## 附录:快速检查清单
| 检查项 | 正确配置 |
|--------|----------|
| Vite isCustomElement | `(tag) => /^micro-app/.test(tag)` |
| Vite 子应用沙箱 | `iframe: true`(必须) |
| Webpack 子应用沙箱 | `iframe: false` 即可 |
| 子应用跨域 | `Access-Control-Allow-Origin: *` |
| 子应用挂载 id | 不能用 `#app`,需独立命名 |
| 路由通配符Vue Router 4 | `:page*`,不是 `*` |
| lifeCycles 回调参数 | `(e: CustomEvent, appName: string)` |
| 子应用路由 base | `window.__MICRO_APP_BASE_ROUTE__ \|\| '/'` |

13
index.html Normal file
View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MicroApp 主应用</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

1412
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "microapp-main",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"@micro-zoe/micro-app": "^1.0.0-rc.31",
"vue": "^3.4.0",
"vue-router": "^4.3.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"typescript": "^5.4.0",
"vite": "^5.4.0",
"vue-tsc": "^2.0.0"
}
}

1
public/vite.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#667eea" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>

After

Width:  |  Height:  |  Size: 293 B

101
src/App.vue Normal file
View File

@@ -0,0 +1,101 @@
<template>
<div id="main-app">
<header class="app-header">
<div class="logo" @click="$router.push('/')">
<h1>MicroApp 主应用</h1>
</div>
<nav class="nav-links">
<router-link to="/home">首页</router-link>
<router-link to="/child-app">Vue2 子应用</router-link>
</nav>
</header>
<main class="app-main">
<router-view />
</main>
</div>
</template>
<script setup lang="ts">
// 根组件 — 提供全局布局(头部导航 + 内容区)
</script>
<style>
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, 'Microsoft YaHei', sans-serif;
color: #333;
background: #f5f6f7;
}
#app {
height: 100%;
}
#main-app {
display: flex;
flex-direction: column;
height: 100%;
}
/* 头部导航 */
.app-header {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 24px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
flex-shrink: 0;
}
.logo {
cursor: pointer;
}
.logo h1 {
font-size: 18px;
font-weight: 600;
letter-spacing: 1px;
}
.nav-links {
display: flex;
gap: 8px;
}
.nav-links a {
color: rgba(255, 255, 255, 0.85);
text-decoration: none;
padding: 6px 16px;
border-radius: 6px;
font-size: 14px;
transition: all 0.2s ease;
}
.nav-links a:hover {
color: #fff;
background: rgba(255, 255, 255, 0.15);
}
.nav-links a.router-link-active {
color: #fff;
background: rgba(255, 255, 255, 0.2);
font-weight: 500;
}
/* 主内容区 */
.app-main {
flex: 1;
overflow: auto;
}
</style>

51
src/config/subApps.ts Normal file
View File

@@ -0,0 +1,51 @@
/**
* 子应用配置列表
*
* 后续对接 Vue 2 子应用时,在此处添加/修改配置即可
*
* @see https://jd-opensource.github.io/micro-app/docs.html#/configure
*/
export interface SubAppConfig {
/** 应用名称,全局唯一,字母开头 */
name: string
/** 子应用地址(开发环境填写 devServer 地址) */
url: string
/** 基座分配给子应用的路由前缀 */
baseroute: string
/** 是否使用 iframe 沙箱Vite 子应用必须开启Webpack 子应用可选) */
iframe?: boolean
/** 是否保活子应用,避免重复加载 */
keepAlive?: boolean
/** 路由模式native | native-scope */
routerMode?: 'native' | 'native-scope'
/** 是否禁用样式隔离 */
disableScopecss?: boolean
/** 是否禁用沙箱 */
disableSandbox?: boolean
}
// ============================================================
// 当前对接的子应用列表
// 后续对接 Vue 2 项目时,修改 url 为实际的子应用地址
// ============================================================
export const subApps: SubAppConfig[] = [
{
name: 'vue2-app',
// TODO: 替换为你的 Vue 2 子应用实际地址
url: 'http://localhost:5173/',
baseroute: '/child-app',
// Vite 子应用必须开启 iframe 模式
// with 沙箱的 new Function() 不支持 ES Module 的 import/export 语法)
iframe: true,
keepAlive: true,
routerMode: 'native'
}
]
/**
* 根据名称查找子应用配置
*/
export function getSubAppConfig(name: string): SubAppConfig | undefined {
return subApps.find((app) => app.name === name)
}

36
src/main.ts Normal file
View File

@@ -0,0 +1,36 @@
import { createApp } from 'vue'
import microApp from '@micro-zoe/micro-app'
import App from './App.vue'
import router from './router'
// 启动 micro-app 微前端框架
// https://jd-opensource.github.io/micro-app/docs.html#/start
microApp.start({
// 预加载:当浏览器空闲时预加载子应用,加快首屏速度
preFetchApps: [],
// 当子应用未匹配到路由时的默认行为
// 'default-page' 或自定义地址
// defaultPage: '',
// 全局生命周期
lifeCycles: {
created(_e, appName) {
console.log(`[micro-app] 子应用 ${appName} 被创建`)
},
beforemount(_e, appName) {
console.log(`[micro-app] 子应用 ${appName} 即将挂载`)
},
mounted(_e, appName) {
console.log(`[micro-app] 子应用 ${appName} 挂载完成`)
},
unmount(_e, appName) {
console.log(`[micro-app] 子应用 ${appName} 已卸载`)
},
error(_e, appName) {
console.error(`[micro-app] 子应用 ${appName} 加载错误:`, _e)
}
}
})
const app = createApp(App)
app.use(router)
app.mount('#app')

29
src/router/index.ts Normal file
View File

@@ -0,0 +1,29 @@
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
redirect: '/home'
},
{
path: '/home',
name: 'home',
component: () => import('@/views/Home.vue')
},
{
// 子应用路由 — `:page*` 通配符匹配子应用内部所有路由
// 例如:/child-app、/child-app/page1、/child-app/page2/xxx
path: '/child-app/:page*',
name: 'childApp',
component: () => import('@/views/ChildApp.vue')
}
]
const router = createRouter({
// history 模式 — 主应用和子应用都使用 history 模式
// 通过 baseroute 区分路由归属
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router

140
src/views/ChildApp.vue Normal file
View File

@@ -0,0 +1,140 @@
<template>
<div class="child-app-wrapper">
<!--
micro-app 子应用容器
使用 key 绑定 url url 变化时重新加载子应用
-->
<micro-app
:key="currentApp.url"
:name="currentApp.name"
:url="currentApp.url"
:baseroute="currentApp.baseroute"
:iframe="currentApp.iframe ?? false"
:keep-alive="currentApp.keepAlive ?? true"
:router-mode="currentApp.routerMode ?? 'native'"
:disable-scopecss="currentApp.disableScopecss ?? false"
:disable-sandbox="currentApp.disableSandbox ?? false"
@created="onCreated"
@beforemount="onBeforeMount"
@mounted="onMounted"
@unmount="onUnmount"
@error="onError"
>
<!-- 子应用加载中的占位内容 -->
<div class="loading-placeholder">
<div class="spinner"></div>
<p>正在加载子应用 {{ currentApp.name }}...</p>
<p class="loading-hint">
请确保子应用已启动<code>{{ currentApp.url }}</code>
</p>
</div>
</micro-app>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useRoute } from 'vue-router'
import { subApps } from '@/config/subApps'
const route = useRoute()
/**
* 当前正在使用的子应用配置
*
* 这里根据路由前缀匹配对应的子应用。
* 如果只有一个子应用,直接使用第一个配置;
* 多子应用时,需要根据 route.path 匹配 baseroute。
*/
const currentApp = computed(() => {
// 根据当前路径匹配子应用
const matched = subApps.find((app) =>
route.path.startsWith(app.baseroute)
)
return matched || subApps[0] || { name: 'unknown', url: '', baseroute: '/' }
})
// ============================================
// micro-app 生命周期回调
// ============================================
function onCreated() {
console.log(`[ChildApp] 子应用 ${currentApp.value.name} 被创建`)
}
function onBeforeMount() {
console.log(`[ChildApp] 子应用 ${currentApp.value.name} 即将挂载`)
}
function onMounted() {
console.log(`[ChildApp] 子应用 ${currentApp.value.name} 挂载完成`)
}
function onUnmount() {
console.log(`[ChildApp] 子应用 ${currentApp.value.name} 已卸载`)
}
function onError(error: Error) {
console.error(`[ChildApp] 子应用 ${currentApp.value.name} 加载出错:`, error)
}
</script>
<style scoped>
.child-app-wrapper {
width: 100%;
height: 100%;
position: relative;
}
/* 确保 micro-app 标签占满容器 */
.child-app-wrapper :deep(micro-app) {
width: 100%;
height: 100%;
display: block;
}
/* 加载占位 */
.loading-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
min-height: 400px;
color: #999;
}
.spinner {
width: 36px;
height: 36px;
border: 3px solid #e0e0e0;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-bottom: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-placeholder p {
font-size: 14px;
margin-top: 4px;
}
.loading-hint {
font-size: 12px !important;
color: #bbb;
margin-top: 8px !important;
}
.loading-hint code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>

167
src/views/Home.vue Normal file
View File

@@ -0,0 +1,167 @@
<template>
<div class="home-page">
<div class="hero">
<h2>欢迎使用 MicroApp 微前端主应用</h2>
<p class="desc">
基于 <code>@micro-zoe/micro-app</code> + Vue 3 + Vite 搭建
</p>
</div>
<div class="sub-app-list">
<h3>已接入的子应用</h3>
<div class="cards">
<div
v-for="app in subApps"
:key="app.name"
class="sub-app-card"
@click="$router.push(app.baseroute)"
>
<div class="card-icon">📦</div>
<div class="card-info">
<h4>{{ app.name }}</h4>
<p class="card-url">{{ app.url }}</p>
<p class="card-route">路由前缀{{ app.baseroute }}</p>
</div>
<div class="card-arrow"></div>
</div>
</div>
<div v-if="subApps.length === 0" class="empty">
<p>暂未接入任何子应用</p>
<p class="hint">请在 <code>src/config/subApps.ts</code> 中配置子应用信息</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { subApps } from '@/config/subApps'
</script>
<style scoped>
.home-page {
max-width: 960px;
margin: 0 auto;
padding: 40px 24px;
}
.hero {
text-align: center;
padding: 48px 0 40px;
}
.hero h2 {
font-size: 28px;
font-weight: 700;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero .desc {
margin-top: 12px;
color: #666;
font-size: 15px;
}
.hero code {
background: #e8e8e8;
padding: 2px 8px;
border-radius: 4px;
font-size: 13px;
}
.sub-app-list {
margin-top: 24px;
}
.sub-app-list h3 {
font-size: 16px;
color: #444;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #eee;
}
.cards {
display: flex;
flex-direction: column;
gap: 12px;
}
.sub-app-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: all 0.2s ease;
border: 1px solid transparent;
}
.sub-app-card:hover {
border-color: #667eea;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.15);
transform: translateY(-1px);
}
.card-icon {
font-size: 36px;
flex-shrink: 0;
}
.card-info {
flex: 1;
}
.card-info h4 {
font-size: 16px;
font-weight: 600;
color: #333;
}
.card-url {
font-size: 13px;
color: #999;
margin-top: 4px;
}
.card-route {
font-size: 12px;
color: #667eea;
margin-top: 2px;
}
.card-arrow {
font-size: 20px;
color: #ccc;
flex-shrink: 0;
transition: color 0.2s;
}
.sub-app-card:hover .card-arrow {
color: #667eea;
}
.empty {
text-align: center;
padding: 48px 0;
color: #999;
}
.empty .hint {
margin-top: 8px;
font-size: 13px;
}
.empty code {
background: #e8e8e8;
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
</style>

19
src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// micro-app 全局变量声明
declare global {
interface Window {
__MICRO_APP_ENVIRONMENT__?: boolean
__MICRO_APP_NAME__?: string
__MICRO_APP_BASE_ROUTE__?: string
microApp?: any
}
}
export {}

24
tsconfig.json Normal file
View File

@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

10
tsconfig.node.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

24
vite.config.ts Normal file
View File

@@ -0,0 +1,24 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 将 micro-app 标签识别为自定义元素
isCustomElement: (tag) => /^micro-app/.test(tag)
}
}
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 8080
}
})