观众朋友们,晚上好!欢迎来到“Vue 微前端那些事儿”系列讲座。我是你们的老朋友,今天咱们就来聊聊 Vue 微前端,争取用最接地气的方式,把这个听起来高大上的东西给它扒个精光。
开场白:微前端,你到底是啥玩意儿?
要说这微前端,其实也没啥神秘的,就是把一个庞大的前端应用拆成若干个小的、自治的应用,每个小应用都可以独立开发、独立部署、独立升级。 这样一来,就算某个小应用挂了,也不会影响整个大盘子。就好像一艘航空母舰,上面搭载了各种功能的舰载机,每架飞机都可以独立完成任务,就算一架坠毁了,也不会影响航母的整体作战能力。
第一部分:架构设计——搭个台子唱大戏
要做微前端,首先得有个架构。咱们这里就以一个简单的例子来说明,假设我们要构建一个包含“首页”、“商品列表”、“用户中心”三个模块的电商平台。
-
技术选型:
- 主应用 (Main App): Vue + Vue Router + Vuex (或者Pinia)
- 子应用 (Micro Apps): Vue + Vue Router + Vuex (或者Pinia),当然,子应用的技术栈不一定非得是Vue,也可以是 React, Angular,甚至老旧的 jQuery 项目(想想就刺激)。
- 通信方式: Custom Events, Shared Global State, Message Queue
- 构建工具: Webpack, Parcel, Vite (随便你喜欢哪个)
- 部署方式: Nginx, Docker
-
目录结构 (示例):
micro-frontend-demo/ ├── main-app/ # 主应用 │ ├── src/ │ │ ├── App.vue │ │ ├── router/ │ │ │ └── index.js │ │ ├── store/ │ │ │ └── index.js │ │ └── main.js │ ├── public/ │ └── package.json ├── micro-app-home/ # 子应用:首页 │ ├── src/ │ │ ├── App.vue │ │ ├── router/ │ │ │ └── index.js │ │ └── main.js │ ├── public/ │ └── package.json ├── micro-app-products/ # 子应用:商品列表 │ ├── src/ │ │ ├── App.vue │ │ ├── router/ │ │ │ └── index.js │ │ └── main.js │ ├── public/ │ └── package.json ├── micro-app-user/ # 子应用:用户中心 │ ├── src/ │ │ ├── App.vue │ │ ├── router/ │ │ │ └── index.js │ │ └── main.js │ ├── public/ │ └── package.json ├── nginx/ # Nginx 配置 (可选) └── README.md
-
主应用的核心代码:
// main-app/src/App.vue <template> <div id="app"> <h1>主应用</h1> <router-link to="/home">首页</router-link> | <router-link to="/products">商品列表</router-link> | <router-link to="/user">用户中心</router-link> <router-view /> </div> </template> <script> export default { name: 'App' } </script> // main-app/src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/home', name: 'Home', component: () => import(/* webpackChunkName: "home" */ '../components/MicroAppWrapper.vue').then(m => { return {template:`<micro-app name="home" url="http://localhost:8081/"></micro-app>`}; }) }, { path: '/products', name: 'Products', component: () => import(/* webpackChunkName: "products" */ '../components/MicroAppWrapper.vue').then(m => { return {template:`<micro-app name="products" url="http://localhost:8082/"></micro-app>`}; }) }, { path: '/user', name: 'User', component: () => import(/* webpackChunkName: "user" */ '../components/MicroAppWrapper.vue').then(m => { return {template:`<micro-app name="user" url="http://localhost:8083/"></micro-app>`}; }) } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router //main-app/src/components/MicroAppWrapper.vue <template> <div ref="microAppContainer"></div> </template> <script> import microApp from '@micro-zoe/micro-app' export default { mounted () { // 组件挂载后手动渲染微前端组件。 // 也可以在路由配置里面直接配置 microApp.start() } } </script> // main-app/src/main.js import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import microApp from '@micro-zoe/micro-app' Vue.config.productionTip = false Vue.use(microApp) new Vue({ router, store, render: h => h(App) }).$mount('#app')
-
子应用的核心代码(以首页为例):
// micro-app-home/src/App.vue <template> <div id="app"> <h1>首页</h1> <p>欢迎来到首页,这里是各种促销活动!</p> </div> </template> <script> export default { name: 'App' } </script> // micro-app-home/src/router/index.js import Vue from 'vue' import VueRouter from 'vue-router' Vue.use(VueRouter) const routes = [ { path: '/', name: 'Home', component: { template: '<div>首页内容</div>' } } ] const router = new VueRouter({ mode: 'history', // 注意:子应用也要使用history模式 base: window.__MICRO_APP_BASE_ROUTE__ || '/', // 非常重要! routes }) export default router // micro-app-home/src/main.js import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false let app = null; function render(props = {}) { const { container } = props; app = new Vue({ router, render: h => h(App) }).$mount(container ? container.querySelector('#app') : '#app') } // 独立运行时 if (!window.__MICRO_APP_ENVIRONMENT__) { render(); } // 导出生命周期 - bootstrap export async function bootstrap() { console.log('vue3 app bootstraped'); } // 导出生命周期 - mount export async function mount(props) { console.log('vue3 app mount', props); render(props); } // 导出生命周期 - unmount export async function unmount(props) { console.log('vue3 app unmount', props); app.$destroy(); app.$el.innerHTML = ''; app = null; }
注意事项:
- 子应用的
router
必须使用history
模式。 - 子应用的
router
的base
属性要设置为window.__MICRO_APP_BASE_ROUTE__ || '/'
。__MICRO_APP_BASE_ROUTE__
是主应用传递给子应用的,用于区分不同子应用的路由前缀。 - 子应用需要导出
bootstrap
,mount
,unmount
三个生命周期函数。
- 子应用的
第二部分:子应用通信——鸡犬之声相闻
微前端架构下,子应用之间难免需要通信,就像邻居之间借个酱油啥的。 常用的通信方式有以下几种:
-
Custom Events (自定义事件):
这是最简单粗暴的方式,就是主应用监听子应用抛出的事件,或者子应用监听主应用抛出的事件。
-
主应用监听子应用事件:
// main-app/src/App.vue mounted() { window.addEventListener('subAppEvent', (event) => { console.log('主应用收到子应用事件:', event.detail); }); }
-
子应用触发事件:
// micro-app-home/src/App.vue methods: { sendEventToMainApp() { window.dispatchEvent(new CustomEvent('subAppEvent', { detail: { message: 'Hello from Home!' } })); } }
-
-
Shared Global State (共享全局状态):
这种方式就是创建一个全局的共享状态,主应用和子应用都可以访问和修改这个状态。 可以用 Vuex 或者 Pinia 来实现。
-
创建共享状态:
// main-app/src/store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { sharedData: 'Initial shared data' }, mutations: { updateSharedData(state, payload) { state.sharedData = payload; } }, actions: { updateSharedData({ commit }, payload) { commit('updateSharedData', payload); } } }) export default store
-
主应用和子应用访问共享状态:
// main-app/src/App.vue computed: { sharedData() { return this.$store.state.sharedData; } }, methods: { updateSharedData() { this.$store.dispatch('updateSharedData', 'New data from Main App'); } } // micro-app-home/src/App.vue computed: { sharedData() { return window.microApp.getData().sharedData; // 通过micro-app提供的API获取 } }, methods: { updateSharedData() { window.microApp.dispatch({sharedData: 'New data from Home App'})// 通过micro-app提供的API修改 } }
注意: 子应用需要通过主应用暴露的API来访问和修改共享状态,不能直接访问主应用的 Vuex 实例。
-
-
Message Queue (消息队列):
这种方式就是使用一个消息队列(比如 RabbitMQ, Kafka)来作为主应用和子应用之间的通信桥梁。 主应用和子应用都向消息队列发送消息,然后消息队列负责将消息传递给目标应用。
这种方式比较复杂,但是可以实现更灵活、更可靠的通信。 这里就不展开讲了,有兴趣的同学可以自己研究一下。
-
props传递
主应用可以将数据当成 props 传递给子应用,实现单向数据流。
-
使用
window
对象挂载到
window
对象上的变量,在主应用和子应用都可以访问到,可以用来进行通信。
各种通信方式的优缺点:
通信方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Custom Events | 简单易用 | 需要手动管理事件监听和触发,容易出错 | 简单的、一次性的通信 |
Shared Global State | 方便共享状态,易于管理 | 需要考虑状态冲突的问题,状态管理不当容易导致性能问题 | 需要共享大量状态的场景 |
Message Queue | 灵活可靠,可以实现异步通信 | 比较复杂,需要引入额外的消息队列服务 | 需要高可靠、高并发的通信场景 |
props传递 | 数据流向清晰 | 只支持单向数据流,无法实现子应用主动向主应用通信 | 主应用需要向子应用传递数据的场景 |
window 对象 |
简单直接 | 全局污染,容易引起命名冲突,不推荐大量使用 | 传递少量数据,或者作为兜底方案 |
第三部分:路由隔离——各走各的路
微前端架构下,每个子应用都有自己的路由,如何保证每个子应用的路由互不干扰呢?
-
路由前缀:
这是最常用的方式,就是给每个子应用分配一个唯一的路由前缀。 比如,首页的路由前缀是
/home
,商品列表的路由前缀是/products
,用户中心的路由前缀是/user
。主应用根据 URL 的前缀来判断应该加载哪个子应用。
// main-app/src/router/index.js const routes = [ { path: '/home/*', // 匹配所有以 /home 开头的路由 name: 'Home', component: Home }, { path: '/products/*', // 匹配所有以 /products 开头的路由 name: 'Products', component: Products }, { path: '/user/*', // 匹配所有以 /user 开头的路由 name: 'User', component: User } ]
-
Hash 路由:
如果不想使用路由前缀,可以使用 Hash 路由。 每个子应用都使用自己的 Hash 路由,互不干扰。
// micro-app-home/src/router/index.js const router = new VueRouter({ mode: 'hash', // 使用 Hash 路由 base: '/', routes })
-
Web Components:
Web Components 是一种浏览器原生组件技术,可以将子应用封装成一个独立的 Web Component,然后直接在主应用中使用。
这种方式可以实现更彻底的路由隔离,但是需要对 Web Components 有一定的了解。
第四部分:状态共享——你中有我,我中有你
微前端架构下,有些状态需要在主应用和子应用之间共享,比如用户的登录信息、全局配置等。
-
Shared Global State (共享全局状态):
前面已经讲过了,可以使用 Vuex 或者 Pinia 来实现共享全局状态。
-
Cookies:
可以使用 Cookies 来存储一些全局的状态,比如用户的登录信息。 但是要注意 Cookies 的安全问题,不要存储敏感信息。
-
LocalStorage:
可以使用 LocalStorage 来存储一些全局的状态,比如用户的偏好设置。 但是要注意 LocalStorage 的容量限制,不要存储太大的数据。
-
主应用传递 props:
主应用可以将一些状态作为 props 传递给子应用。
第五部分:部署——上线见真章
微前端应用部署起来稍微麻烦一点,需要考虑各个子应用的部署方式。
-
独立部署:
每个子应用都独立部署到自己的服务器上,然后主应用通过 URL 来加载子应用。 这种方式比较灵活,但是需要管理多个服务器。
-
统一部署:
将所有子应用都打包到一起,然后部署到同一个服务器上。 这种方式比较简单,但是不够灵活。
-
Docker:
可以使用 Docker 来构建每个子应用的镜像,然后使用 Docker Compose 来编排所有子应用。 这种方式比较方便,可以实现快速部署和扩展。
Nginx 配置示例:
server {
listen 80;
server_name your-domain.com;
location / {
root /path/to/main-app/dist;
index index.html;
try_files $uri $uri/ /index.html;
}
location /home/ {
proxy_pass http://localhost:8081/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /products/ {
proxy_pass http://localhost:8082/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /user/ {
proxy_pass http://localhost:8083/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
第六部分:总结——微前端,路漫漫其修远兮
微前端是一种非常有用的架构模式,可以解决大型前端应用的复杂性问题。 但是,微前端也不是银弹,它会带来一些额外的复杂性,比如通信、路由、状态管理等。
在选择微前端架构之前,要仔细评估项目的需求和团队的能力,不要盲目跟风。 只有在真正需要的时候,才能发挥微前端的优势。
好了,今天的讲座就到这里。 希望大家对 Vue 微前端有了更深入的了解。 谢谢大家!
Q&A 环节(模拟):
观众: 老师,微前端的兼容性怎么样?老旧的浏览器支持吗?
老师: 好问题! 微前端本身不涉及浏览器兼容性问题,兼容性取决于子应用的技术栈。 如果子应用使用了 ES6+ 的语法,就需要进行 Babel 转译,以兼容老旧的浏览器。 另外,如果子应用使用了 Web Components,需要使用 Polyfill 来兼容老旧的浏览器。
观众: 老师,微前端的性能怎么样?会不会影响用户体验?
老师: 性能是微前端需要重点关注的问题。 子应用的加载速度、渲染速度、通信速度都会影响用户体验。 可以通过以下方式来优化性能:
- 代码分割: 将子应用的代码分割成多个小的 chunk,按需加载。
- 懒加载: 只在需要的时候才加载子应用。
- 缓存: 对子应用的代码和数据进行缓存。
- CDN: 使用 CDN 来加速子应用的加载速度。
- 优化通信: 减少主应用和子应用之间的通信次数。
观众: 老师,微前端的调试难度大吗?
老师: 调试微前端应用确实比调试单体应用要麻烦一些。 可以使用以下工具来辅助调试:
- 浏览器开发者工具: 可以使用浏览器开发者工具来调试主应用和子应用的代码。
- Vue Devtools: 可以使用 Vue Devtools 来调试 Vue 子应用。
- 日志: 在主应用和子应用中添加日志,方便排查问题。
- Mock 数据: 使用 Mock 数据来模拟子应用的接口,方便调试。
总的来说,微前端的调试需要一定的经验和技巧,但是只要掌握了方法,也可以高效地进行调试。
(完)