记录一次微前端改造:把 10+ 个独立 Vue 项目整合到一起
最近主导了公司前端项目的微前端化改造,踩了不少坑,也有一些收获。趁着记忆还新鲜,把过程记录下来,希望能给有类似需求的同学一些参考。
声明:我也是第一次做微前端,很多地方可能不够规范,欢迎大家指正。
一、背景:我们遇到了什么问题?
1.1 项目现状
公司的前端项目经过几年发展,变成了这样:
- 10+ 个独立的 Vue 项目:认证系统、主业务平台、教育模块、社区模块、H5 移动端等
- 每个项目都是独立的 Git 仓库,有自己的 CI/CD 流程,独立部署
- 技术栈不太统一:有的是 Vue 2 + Vue CLI,有的是 Vue 3 + Vite
- 用户体验比较割裂:在不同系统间跳转时,要重新加载整个页面
1.2 想解决的问题
| 问题 | 具体表现 |
|---|---|
| 体验不连贯 | 系统间跳转白屏时间长,感觉像在用不同的网站 |
| 登录状态 | 虽然用 Cookie 共享了 token,但体验还是不够好 |
| 重复建设 | 每个项目都有一套类似的基础代码 |
说实话,一开始我对微前端也没什么概念,是领导提出想把这些系统整合成一个统一入口,我才开始研究这个方向。
二、技术选型:为什么选 micro-app?
2.1 调研过的方案
| 方案 | 我的理解 | 为什么没选/选了 |
|---|---|---|
| iframe | 最简单,天然隔离 | 体验不好,通信麻烦 |
| qiankun | 最成熟,基于 single-spa | 配置比较复杂,子应用改造成本高 |
| micro-app | 京东出的,基于 Web Components | 接入简单,改造成本低 ✅ |
| Module Federation | Webpack 5 的功能 | 我们有的项目还在用 Vue CLI 4 |
2.2 选择 micro-app 的原因
说实话,主要是因为简单。
看了官方文档后,发现子应用几乎不需要怎么改,基座应用也就几行代码:
javascript
// 基座应用启动
import microApp from '@micro-zoe/micro-app'
microApp.start()
// 加载子应用就一个标签
<micro-app name="app1" url="http://localhost:8081/"></micro-app>
对于我这种微前端新手来说,能快速跑起来比什么都重要。
三、项目结构
3.1 实际情况
需要说明的是,我们的项目结构是这样的:
# 这些都是独立的 Git 仓库,我只是在本地建了个文件夹把它们放一起方便开发
project/ # 本地文件夹(不是 Git 仓库)
├── project-base/ # 基座应用 - 独立 Git 仓库
├── project-main/ # 主业务 - 独立 Git 仓库
├── project-auth/ # 认证系统 - 独立 Git 仓库
├── project-edu/ # 教育模块 - 独立 Git 仓库
├── project-community/ # 社区模块 - 独立 Git 仓库
└── ...
每个项目都是独立部署的,有自己的 Docker 镜像和部署流程。微前端改造并没有改变这一点,只是加了一个基座应用来统一加载它们。
3.2 端口规划
本地开发时,我给每个应用分配了不同的端口:
| 应用 | 端口 | 路由前缀 |
|---|---|---|
| project-base | 8080 | / |
| project-main | 8081 | /main |
| project-auth | 8082 | /auth |
| project-community | 8083 | /communities |
| project-edu | 8085 | /edu |
四、具体怎么做的
4.1 基座应用
基座应用是新建的,用的 Vite + Vue 3。
1. 安装和初始化 micro-app
typescript
// project-base/src/main.ts
import microApp from "@micro-zoe/micro-app";
microApp.start({
plugins: {
modules: {},
},
});
2. Vite 需要配置一下,不然会报错
typescript
// project-base/vite.config.ts
vue({
template: {
compilerOptions: {
// 告诉 Vue:micro-app 是自定义元素,别当组件解析
isCustomElement: (tag) => /^micro-app/.test(tag),
},
},
});
3. 路由配置
typescript
// 用通配符匹配子应用的所有路由
const routes = [
{
path: "/main/:page*",
component: () => import("@/views/main.vue"),
},
{
path: "/auth/:page*",
component: () => import("@/views/auth.vue"),
},
// ...
];
4. 子应用挂载组件
vue
<!-- project-base/src/views/main.vue -->
<template>
<micro-app name="project-main" :url="url" baseroute="/main" />
</template>
<script setup>
const url = "http://localhost:8081/child/main/";
</script>
4.2 子应用改造
子应用的改造确实不多,主要是这几步:
1. 新建 public-path.ts
typescript
// src/public-path.ts
if (window.__MICRO_APP_ENVIRONMENT__) {
__webpack_public_path__ = window.__MICRO_APP_PUBLIC_PATH__;
}
2. 改造入口文件
typescript
// src/main.ts
import "./public-path"; // 必须放最前面
let app = null;
let router = null;
let history = null;
function mount() {
history = createWebHistory(
window.__MICRO_APP_BASE_ROUTE__ || process.env.BASE_URL
);
router = createRouter({ history, routes });
app = createApp(App);
app.use(router);
app.mount("#app");
}
function unmount() {
app?.unmount();
history?.destroy();
app = null;
router = null;
history = null;
}
// 判断运行环境
if (window.__MICRO_APP_ENVIRONMENT__) {
// 微前端环境
window[`micro-app-${window.__MICRO_APP_NAME__}`] = { mount, unmount };
} else {
// 独立运行
mount();
}
3. 配置跨域(开发环境)
javascript
// vue.config.js
module.exports = {
devServer: {
headers: {
"Access-Control-Allow-Origin": "*",
},
},
};
4.3 状态共享
这块我承认做得不太规范。
按理说应该用 micro-app 提供的通信机制,但我们之前的项目已经有一套基于 localStorage + Cookie 的方案在用了,而且子应用还需要支持独立运行,所以就没改。
typescript
// 公共组件库里的 store
export default {
set(key, data) {
localStorage.setItem(key, JSON.stringify({ data }));
},
get(key) {
const cache = localStorage.getItem(key);
return cache ? JSON.parse(cache).data : null;
},
// token 用 cookie 存,这样可以跨子域共享
set_cookie(name, value) {
document.cookie = `${name}=${value}; path=/; max-age=${30 * 24 * 60 * 60}`;
},
};
这个方案的问题:
- 不够实时,一个应用改了数据,另一个应用需要刷新才能看到
- 没有利用 micro-app 的能力
但也有好处:
- 简单,团队都能理解
- 子应用可以独立运行
- 刷新页面不丢数据
后续有时间可能会改成用 micro-app 的 setGlobalData,但目前这样也能用。
五、部署
每个应用还是独立部署,只是 Nginx 配置需要调整一下。
基座应用
nginx
server {
listen 80;
location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
}
}
子应用
nginx
server {
listen 80;
location /child/main {
root /usr/share/nginx/html;
try_files $uri $uri/ /child/main/index.html;
}
}
六、踩过的坑
6.1 静态资源 404
子应用的图片加载不出来,因为路径变了。
解决 :确保 public-path.ts 配置正确,而且要放在入口文件最前面 import。
6.2 路由跳转 URL 不变
子应用内部跳转后,浏览器地址栏没变化。
解决 :路由的 base 要用 window.__MICRO_APP_BASE_ROUTE__。
6.3 热更新有问题
开发时子应用热更新会导致页面白屏。
解决 :子应用关闭热更新 hot: false。有点麻烦,但暂时没找到更好的办法。
6.4 样式偶尔会串
虽然 micro-app 有样式隔离,但有些全局样式还是会影响。
解决:尽量用 scoped 样式,避免写太通用的选择器。
七、目前的效果
改造完成后:
- ✅ 用户可以在一个页面里切换不同的系统,不用重新加载
- ✅ 各个子应用还是可以独立开发、独立部署
- ✅ 子应用也可以单独访问(方便开发调试)
还存在的问题:
- ⚠️ 状态共享方案不够优雅
- ⚠️ 没有用到 micro-app 的很多高级功能(预加载、keep-alive 等)
- ⚠️ 首次加载子应用还是有点慢
八、后续想做的
- 研究一下预加载:micro-app 支持 preFetch,应该能提升切换速度
- 优化状态共享:看看能不能用 micro-app 的全局数据机制
- 统一技术栈:慢慢把 Vue 2 的项目升级到 Vue 3
九、一些想法
做完这次改造,有几点感受:
-
微前端不是银弹。如果项目本身就不大,或者团队就几个人,可能真没必要搞这么复杂。
-
先跑起来再说。一开始不要想着做到完美,能用就行,后面再慢慢优化。
-
保持子应用的独立性。我觉得这点很重要,子应用能独立运行,开发调试都方便很多。
-
文档很重要。我们内部写了一份接入文档,新同事照着做基本都能跑起来。
十、参考资料
以上就是这次微前端改造的记录。如有错误或者更好的方案,欢迎评论区交流~