各位好,我是你们今天的主讲人,咱们今天聊聊Vue微前端这事儿。这年头,单体应用动不动就大到没朋友,改个小bug,整个项目重新部署,简直是噩梦。微前端就像是给你的巨石阵(单体应用)来了个乾坤大挪移,把它拆成一堆小模块,各自为政,互不干扰,部署效率嗖嗖的!
微前端?听起来挺玄乎,到底是个啥?
简单来说,微前端就是把一个大型前端应用拆分成多个小型、自治的应用,这些应用可以由不同的团队独立开发、测试和部署。每个小应用就像一个独立的乐高积木,可以灵活组合,最终拼成一个完整的应用。
为什么要用微前端?
- 独立开发和部署: 各个团队可以独立开发、测试和部署自己的微应用,互不影响,减少了代码冲突和部署风险。
- 技术栈无关: 每个微应用可以使用不同的技术栈,可以根据业务需求选择最适合的技术。
- 增量升级: 可以逐步迁移旧应用到新的技术栈,不用一次性重构整个应用。
- 代码复用: 可以将公共组件或模块抽离成共享库,供多个微应用使用。
- 团队自治: 各个团队可以拥有更大的自主权,可以更快地响应业务需求。
Vue 微前端,怎么玩?
Vue 作为前端界的扛把子之一,当然也少不了微前端的解决方案。目前比较流行的方案主要有以下两种:
- Webpack Module Federation: Webpack 5 带来的神器,可以让你像引用本地模块一样引用远程模块,简单粗暴,性能杠杠的。
- Qiankun: 蚂蚁金服开源的微前端框架,基于 single-spa,功能强大,生态完善,适合复杂的微前端场景。
方案一:Webpack Module Federation (MF)
Webpack MF 的核心思想是:共享模块。它允许不同的 Webpack 构建应用共享模块,从而实现微前端的集成。
实战演练:
咱们来创建一个简单的例子,包含一个基座应用(app-main
)和两个微应用(app-vue1
和 app-vue2
)。
1. 基座应用 (app-main):
-
安装依赖:
mkdir app-main && cd app-main npm init -y npm install vue vue-router webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
-
webpack.config.js:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), publicPath: 'http://localhost:8080/', // 注意这里的publicPath,很重要 }, devServer: { port: 8080, historyApiFallback: true, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader' }, { test: /.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, { test: /.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), new ModuleFederationPlugin({ name: 'app_main', // 必须唯一 remotes: { app_vue1: 'app_vue1@http://localhost:8081/remoteEntry.js', // 引入远程模块 app_vue2: 'app_vue2@http://localhost:8082/remoteEntry.js', }, shared: ['vue', 'vue-router'] // 共享依赖 }) ], resolve: { extensions: ['.vue', '.js'], alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } }, };
-
src/index.js:
import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; const app = createApp(App); app.use(router); app.mount('#app');
-
src/App.vue:
<template> <div> <h1>Main App</h1> <router-link to="/vue1">Vue 1</router-link> | <router-link to="/vue2">Vue 2</router-link> <router-view></router-view> </div> </template> <script> export default { name: 'App', } </script>
-
src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: '/', redirect: '/vue1' }, { path: '/vue1', name: 'Vue1', component: () => import('app_vue1/VueApp') // 动态引入远程模块 }, { path: '/vue2', name: 'Vue2', component: () => import('app_vue2/VueApp') // 动态引入远程模块 } ]; const router = createRouter({ history: createWebHistory(), routes }); export default router;
-
public/index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Main App</title> </head> <body> <div id="app"></div> </body> </html>
2. 微应用 1 (app-vue1):
-
安装依赖:
mkdir app-vue1 && cd app-vue1 npm init -y npm install vue vue-router webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
-
webpack.config.js:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const { ModuleFederationPlugin } = require('webpack').container; const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), publicPath: 'http://localhost:8081/', // 注意这里的publicPath,很重要 }, devServer: { port: 8081, historyApiFallback: true, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader' }, { test: /.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, { test: /.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), new ModuleFederationPlugin({ name: 'app_vue1', // 必须唯一 filename: 'remoteEntry.js', // 导出入口文件 exposes: { './VueApp': './src/App.vue' // 暴露模块 }, shared: ['vue', 'vue-router'] // 共享依赖 }) ], resolve: { extensions: ['.vue', '.js'], alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } }, };
-
src/index.js:
import { createApp } from 'vue'; import App from './App.vue'; const app = createApp(App); app.mount('#app');
-
src/App.vue:
<template> <div> <h2>Vue 1 App</h2> <p>This is the Vue 1 micro-frontend.</p> </div> </template> <script> export default { name: 'App', } </script>
-
public/index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue 1 App</title> </head> <body> <div id="app"></div> </body> </html>
3. 微应用 2 (app-vue2):
- 代码结构和配置与
app-vue1
类似,只需要修改以下几点:- 端口号改为
8082
。 webpack.config.js
中的name
改为'app_vue2'
。src/App.vue
的内容改成 Vue2 相关的。
- 端口号改为
启动应用:
分别启动 app-vue1
、app-vue2
和 app-main
:
cd app-vue1 && npm run serve
cd app-vue2 && npm run serve
cd app-main && npm run serve
打开 http://localhost:8080
,你就能看到基座应用,并且可以通过路由切换到两个微应用。
Webpack MF 的优点:
- 简单易用: 配置简单,学习成本低。
- 性能优秀: 共享模块,减少了重复加载。
- 无框架限制: 可以与其他框架集成。
Webpack MF 的缺点:
- 依赖管理: 需要仔细管理共享依赖,避免版本冲突。
- 构建复杂: 需要配置多个 Webpack 构建。
- 路由管理: 需要手动处理路由跳转。
方案二:Qiankun
Qiankun 是一个基于 single-spa 的微前端框架,它提供了一套完整的微前端解决方案,包括应用注册、路由管理、生命周期管理等。
实战演练:
咱们还是用上面的例子,用 Qiankun 来实现微前端。
1. 基座应用 (app-main):
-
安装依赖:
mkdir app-main && cd app-main npm init -y npm install vue vue-router qiankun --save npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
-
webpack.config.js:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), publicPath: '/', }, devServer: { port: 8080, historyApiFallback: true, headers: { 'Access-Control-Allow-Origin': '*', // 允许跨域 }, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader' }, { test: /.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, { test: /.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), ], resolve: { extensions: ['.vue', '.js'], alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } }, };
-
src/index.js:
import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; import { registerMicroApps, start } from 'qiankun'; const app = createApp(App); app.use(router); app.mount('#app'); // 注册微应用 registerMicroApps([ { name: 'app-vue1', entry: '//localhost:8081', // 微应用入口 container: '#container', // 容器 activeRule: '/vue1', // 激活规则 }, { name: 'app-vue2', entry: '//localhost:8082', // 微应用入口 container: '#container', // 容器 activeRule: '/vue2', // 激活规则 }, ]); // 启动 Qiankun start();
-
src/App.vue:
<template> <div> <h1>Main App</h1> <router-link to="/vue1">Vue 1</router-link> | <router-link to="/vue2">Vue 2</router-link> <div id="container"></div> <!-- 微应用容器 --> </div> </template> <script> export default { name: 'App', } </script>
-
src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: '/', redirect: '/vue1' }, { path: '/vue1', name: 'Vue1', // component: () => import('app_vue1/VueApp') // 不要在这里引入微应用 }, { path: '/vue2', name: 'Vue2', // component: () => import('app_vue2/VueApp') // 不要在这里引入微应用 } ]; const router = createRouter({ history: createWebHistory(), routes }); export default router;
-
public/index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Main App</title> </head> <body> <div id="app"></div> </body> </html>
2. 微应用 1 (app-vue1):
-
安装依赖:
mkdir app-vue1 && cd app-vue1 npm init -y npm install vue vue-router --save npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
-
webpack.config.js:
const HtmlWebpackPlugin = require('html-webpack-plugin'); const path = require('path'); module.exports = { mode: 'development', devtool: 'source-map', entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), publicPath: 'http://localhost:8081/', // 注意这里的publicPath,很重要 library: `app-vue1`, libraryTarget: 'umd', jsonpFunction: `webpackJsonp_app_vue1`, }, devServer: { port: 8081, historyApiFallback: true, headers: { 'Access-Control-Allow-Origin': '*', // 允许跨域 }, }, module: { rules: [ { test: /.vue$/, use: 'vue-loader' }, { test: /.css$/, use: [ 'vue-style-loader', 'css-loader' ] }, { test: /.js$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'] } } } ] }, plugins: [ new HtmlWebpackPlugin({ template: './public/index.html' }), ], resolve: { extensions: ['.vue', '.js'], alias: { 'vue': 'vue/dist/vue.esm-bundler.js' } }, };
-
src/index.js:
import { createApp } from 'vue'; import App from './App.vue'; import router from './router'; let instance = null; function render(props = {}) { const { container } = props; instance = createApp(App); instance.use(router); instance.mount(container ? container.querySelector('#app') : '#app'); } // 独立运行时 if (!window.__POWERED_BY_QIANKUN__) { render(); } export async function bootstrap(props) { console.log('[vue] vue app bootstraped', props); } export async function mount(props) { console.log('[vue] props from main framework', props); render(props); } export async function unmount(props) { console.log('[vue] props from main framework', props); instance.unmount(); instance = null; }
-
src/App.vue:
<template> <div> <h2>Vue 1 App</h2> <p>This is the Vue 1 micro-frontend.</p> </div> </template> <script> export default { name: 'App', } </script>
-
src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'; const routes = [ { path: '/', redirect: '/vue1' }, { path: '/vue1', name: 'Vue1', component: { template: '<div><h1>Vue1 Content</h1></div>' } } ]; const router = createRouter({ history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/vue1' : '/'), routes }); export default router;
-
public/index.html:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Vue 1 App</title> </head> <body> <div id="app"></div> </body> </html>
3. 微应用 2 (app-vue2):
- 代码结构和配置与
app-vue1
类似,只需要修改以下几点:- 端口号改为
8082
。 webpack.config.js
中的library
和jsonpFunction
改为'app-vue2'
和'webpackJsonp_app_vue2'
。- 修改路由history的base
- 端口号改为
启动应用:
分别启动 app-vue1
、app-vue2
和 app-main
:
cd app-vue1 && npm run serve
cd app-vue2 && npm run serve
cd app-main && npm run serve
打开 http://localhost:8080
,你就能看到基座应用,并且可以通过路由切换到两个微应用。
Qiankun 的优点:
- 功能强大: 提供了一套完整的微前端解决方案。
- 生态完善: 社区活跃,文档丰富。
- 兼容性好: 可以与其他框架集成。
Qiankun 的缺点:
- 学习成本高: 配置复杂,需要学习 Qiankun 的 API。
- 性能损耗: 基于 iframe 或 shadow DOM,有一定的性能损耗。
- 侵入性强: 需要修改微应用的入口文件。
总结:
特性 | Webpack Module Federation | Qiankun |
---|---|---|
易用性 | 简单 | 复杂 |
性能 | 优秀 | 较好 |
功能 | 基础 | 强大 |
侵入性 | 低 | 高 |
适用场景 | 简单的微前端场景 | 复杂的微前端场景 |
技术栈无关 | 支持 | 支持 |
选哪个?
- 如果你的项目比较简单,只需要简单的模块共享,那么 Webpack Module Federation 是一个不错的选择。
- 如果你的项目比较复杂,需要完整的微前端解决方案,那么 Qiankun 可能更适合你。
微前端的坑:
- 状态管理: 微应用之间的状态如何共享?
- 通信: 微应用之间如何通信?
- UI 统一: 如何保证微应用的 UI 风格一致?
- 版本管理: 如何管理微应用的版本?
- 部署: 如何部署微应用?
- 权限管理: 如何管理微应用的权限?
这些问题需要根据具体的项目情况进行选择和实现。
好了,今天的 Vue 微前端讲座就到这里,希望对大家有所帮助。记住,微前端不是银弹,不要为了用而用,要根据实际情况选择合适的方案。祝大家早日成为微前端大师!