结合 qiankun 微前端框架与 monorepo工程化方案,搭建支持 React+Umi4、Vue2、Vue3 多技术栈子应用的微前端体系。 源码仓库地址
一、环境准备与工程初始化
# 1. 检查环境版本 (推荐版本)
node -v # >= 16.18.0
pnpm -v # >= 7.0.0
# 2. 安装pnpm (已安装忽略)
npm install -g pnpm
二、目录架构
monorepo/
├── packages/
│ ├── main/ # 主应用(React+TS+Vite+qiankun)- 端口7000
│ ├── react-app/ # 子应用1(React+Umi4+TS)- 端口7001
│ ├── vue2-app/ # 子应用2(Vue2+Webpack+JS)- 端口7002
│ └── vue3-app/ # 子应用3(Vue3+Vite+TS)- 端口7003
├── common/ # 公共工具库(全局复用)
├── pnpm-workspace.yaml# pnpm workspace配置
└── package.json # 根项目配置
首先根目录初始化package.json,执行
pnpm init -y
在根目录创建 pnpm-workspace.yaml 文件,指定workspace范围,实现多包管理:
packages:
- 'packages/*' # 所有子应用与主应用存放目录
- 'common' # 公共工具库目录
三、主应用搭建(React+Vite+qiankun)
1.创建主应用项目
# 进入packages目录并创建main目录
cd packages && mkdir main && cd main
# 使用Vite创建React+TS项目
pnpm create vite@latest . -- --template react-ts
# 安装依赖
pnpm install
# 安装核心依赖:qiankun(微前端框架)、react-router-dom@6(路由)、axios(请求)
pnpm install qiankun axios react-router-dom@6
# 安装开发依赖:@types/node(Node类型定义)
pnpm install @types/node -D
2. 配置Vite(vite.config.ts)
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
// 配置别名,简化路径引入
alias: { '@': path.resolve(__dirname, './src') }
},
server: {
port: 7000, // 主应用固定端口,子应用基于此端口通信
cors: true, // 允许跨域(子应用加载必须开启)
},
build: {
outDir: 'dist', // 输出目录
assetsDir: 'assets' // 静态资源目录
}
})
3.实现主应用核心逻辑(src/App.tsx)
通过qiankun的 registerMicroApps 注册子应用,start 启动微前端框架,同时实现子应用切换导航:
//App.tsx
import { useEffect } from 'react'
import { BrowserRouter } from 'react-router-dom'
import { registerMicroApps, start } from 'qiankun'
import './App.scss'
function AppContent() {
const handleNavigate = (path: string) => {
window.location.assign(path)
}
useEffect(() => {
// 1. 注册所有子应用配置
registerMicroApps(
[
{
name: 'react-app', // 唯一标识,必须和子应用package.json的name一致
entry: 'http://localhost:7001', // 子应用运行端口
container: '#micro-app-container', // 渲染容器ID
activeRule: '/sub1', // 路由匹配规则,主应用路由到/sub1时加载该子应用
// props: { userInfo: { name: '主应用用户' } }, // 主应用向子应用 传参
},
{
name: 'vue2-app',
entry: 'http://localhost:7002',
container: '#micro-app-container',
activeRule: '/sub2',
},
{
name: 'vue3-app',
entry: 'http://localhost:7003',
container: '#micro-app-container',
activeRule: '/sub3',
},
],
)
//启动微前端框架
start({
sandbox: { experimentalStyleIsolation: true }, // 开启实验性样式隔离
singular: true, // 单实例模式:同一时间只加载一个子应用
})
}, [])
const navItems = [
{ path: '/sub1', label: '子应用1: React+Umi4' },
{ path: '/sub2', label: '子应用2: Vue2+Webpack' },
{ path: '/sub3', label: '子应用3: Vue3+Vite' },
]
return (
<div className="main-app">
<h1>主应用</h1>
{ /* 主应用导航栏 - 切换子应用 */ }
<nav className="main-nav">
{navItems.map((item) => (
<button
key={item.path}
onClick={() => handleNavigate(item.path)}
>
{item.label}
</button>
))}
</nav>
<div id="micro-app-container" className="micro-container"></div>
</div>
)
}
function App() {
return (
<BrowserRouter
basename="/"
>
<AppContent />
</BrowserRouter>
)
}
export default App
四、子应用搭建
子应用需适配qiankun的加载规范,实现独立运行与嵌入主应用两种模式兼容。以下分别搭建React+Umi4、Vue2、Vue3三种技术栈的子应用。
子应用1:React+Umi4+TS
1. 创建项目并安装依赖
# 进入react-app
cd packages/react-app
# 创建Umi4项目(选择Simple App模板)
pnpm create umi@latest .
# 安装依赖
pnpm install
# 安装qiankun插件
pnpm add @umijs/plugins
2. 配置package.json
指定项目名称(需与主应用注册的name一致),配置开发端口:
{
"name": "react-app",
"scripts": {
"dev": "set PORT=7001 && umi dev", // Windows环境指定端口
// "dev": "PORT=7001 umi dev", // Mac/Linux环境指定端口
"build": "umi build",
"start": "umi start"
}
}
3 配置Umi(.umirc.ts)
启用qiankun插件,配置基础路由:
import { defineConfig } from "umi";
export default defineConfig({
base: '/sub1', // 基础路由(与主应用activeRule一致)
npmClient: 'pnpm',
plugins: ["@umijs/plugins/dist/qiankun"], // 启用qiankun插件
qiankun: {
slave: {} // 声明为子应用
},
});
子应用2:Vue2+Webpack+JS
1.创建项目并安装依赖
# 进入vue2-app目录
cd packages/vue2-app
# 使用Vue CLI 4创建Vue2项目
npx @vue/cli@4 create .
# 安装vue-router@3.x(Vue2配套版本)
pnpm install vue-router@3.x
2.配置vue.config.js
const { name } = require("./package.json");
module.exports = {
// 开发环境跨域配置
devServer: {
port: 7002,
headers: {
"Access-Control-Allow-Origin": "*", // 允许所有跨域请求
},
},
// Webpack配置(适配qiankun)
configureWebpack: {
output: {
library: `${name}-[name]`, // 暴露全局变量
libraryTarget: "umd", // 打包为UMD格式(支持AMD/CMD/全局变量)
jsonpFunction: `webpackJsonp_${name}`, // 避免与其他应用的jsonpFunction冲突
},
// 可选:外部依赖抽离(从主应用引入,减少子应用体积)
// externals: {
// 'vue': 'Vue',
// 'vue-router': 'VueRouter'
// }
},
};
3.配置public-path.js(解决静态资源路径问题)
创建public-path.js文件
// 解决qiankun加载时静态资源路径错误问题
if (window.__POWERED_BY_QIANKUN__) {
// eslint-disable-next-line
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
4. 配置路由(src/router/index.js)
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "Home",
component: Home
}
];
const router = new VueRouter({
mode: "history",
// 动态设置base:qiankun环境下为/sub2,独立运行时为/
base: window.__POWERED_BY_QIANKUN__ ? "/sub2" : "/",
routes
});
export default router;
5. 改造入口文件(src/main.js)
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "../public-path.js"; // 引入public-path配置
Vue.config.productionTip = false;
let instance = null; // 存储Vue实例
// 渲染函数(核心:适配qiankun容器挂载)
function render(props = {}) {
const { container } = props;
// 挂载到主应用容器或独立容器
instance = new Vue({
router,
render: (h) => h(App)
}).$mount(container ? container.querySelector("#app") : "#app");
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
console.log("Vue2子应用独立运行");
render();
}
// 1. bootstrap(可选):子应用启动前初始化(只执行一次)
export async function bootstrap(props) {
console.log("Vue2子应用启动初始化", props);
}
// 2. mount(必须):子应用挂载(每次进入子应用时执行)
export async function mount(props) {
console.log("Vue2子应用接收主应用参数", props);
// 可选:监听主应用全局状态变化
// if (props.setGlobalState) {
// props.onGlobalStateChange((state, prevState) => {
// console.log("主应用状态变化:", prevState, "->", state);
// }, true);
// }
render(props);
}
// 3. unmount(必须):子应用卸载(每次离开子应用时执行)
export async function unmount(props) {
console.log("Vue2子应用卸载", props);
// 销毁Vue实例,避免内存泄漏
if (instance) {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
}
子应用3:Vue3+Vite+TS
1.创建项目并安装依赖
cd packages/vue3-app
# 使用Vite创建Vue3+TS项目
pnpm create vite@latest . -- --template vue-ts
# 安装依赖
pnpm install
# 安装qiankun适配插件、vue-router@4、pinia
pnpm install vite-plugin-qiankun -D
pnpm install vue-router@4 pinia
2.配置Vite(vite.config.ts)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [
vue(),
// 配置qiankun插件:指定子应用名称,开启开发模式
qiankun('vue3-app', { useDevMode: true })
],
base: '/sub3', // 基础路由(与主应用activeRule一致)
server: {
port: 7003,
cors: true, // 允许跨域
headers: {
'Access-Control-Allow-Origin': '*',
},
},
})
3.配置路由(src/router/index.ts)
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/sub3' : "/"),
routes: [
{
path: '/',
name: 'Home',
component: Home
}
]
})
export default router
4.改造入口文件(src/main.ts)
// @ts-nocheck
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import { renderWithQiankun } from "vite-plugin-qiankun/dist/helper"
let app = null; // 存储Vue3应用实例
// 渲染函数
function render(props = {}) {
const { container } = props;
// 若应用已挂载,先卸载避免重复渲染
if (app) {
app.unmount();
app = null;
}
app = createApp(App)
app.use(router)
// 挂载到指定容器
app.mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行时直接渲染
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// 适配qiankun生命周期
renderWithQiankun({
// 启动初始化
bootstrap() {
console.log("Vue3子应用启动初始化")
},
// 挂载
mount(props) {
console.log("Vue3子应用接收主应用参数", props)
render(props)
},
// 更新(可选)
update(props) {
console.log("Vue3子应用更新", props)
},
// 卸载
unmount() {
console.log("Vue3子应用卸载")
app.unmount()
app = null
}
})
五、工程启动配置
vue2-app的package.json中scripts新增 "dev": "vue-cli-service serve",
在根目录package.json中配置脚本,实现一键启动所有应用:
{
"name": "monorepo",
"version": "1.0.0",
"scripts": {
"dev:main": "pnpm --filter main dev", // 启动主应用
"dev:react": "pnpm --filter react-app dev", // 启动React子应用
"dev:vue2": "pnpm --filter vue2-app serve", // 启动Vue2子应用
"dev:vue3": "pnpm --filter vue3-app dev", // 启动Vue3子应用
"dev": "pnpm -r --parallel dev" // 并行启动所有应用
}
}
启动命令:
# 根目录执行
pnpm dev
启动成功后,访问 http://localhost:7000,点击导航即可切换不同技术栈的子应用。
六、公共工具库共享(monorepo核心优势)
通过monorepo的workspace机制,实现公共工具库在所有应用中共享,避免重复开发与维护。
1.新建common文件夹,在该目录下执行pnpm init -y
//示例common/package.json
{
"name": "common", // 公共库名称(应用中通过此名称引入)
"version": "1.0.0",
"description": "微前端应用公共工具库",
"main": "index.js", // 入口文件
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
2.编写公共工具函数
创建格式化日期工具(common/format.js):
/**
* 格式化日期
* @param {Date|string|number} date 日期
* @param {string} format 格式,默认 'YYYY-MM-DD HH:mm'
* @returns {string} 格式化后的日期字符串
*/
export function formatDate(date, format = "YYYY-MM-DD HH:mm") {
if (!date) return "";
const d = new Date(date);
if (isNaN(d.getTime())) return "";
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
const hours = String(d.getHours()).padStart(2, "0");
const minutes = String(d.getMinutes()).padStart(2, "0");
const seconds = String(d.getSeconds()).padStart(2, "0");
return format
.replace("YYYY", year)
.replace("MM", month)
.replace("DD", day)
.replace("HH", hours)
.replace("mm", minutes)
.replace("ss", seconds);
}
3.配置公共库入口(common/index.js)
/**
* 公共工具库入口文件
*/
// 导出格式化函数
export * from "./format.js";
4. 在应用main/packages.json等的"dependencies"手动添加 "common": "workspace:*",然后执行pnpm i
//在组件中引入使用:
import { formatDate } from 'common'
function AppContent() {
// ... 其他逻辑
return (
<div className="main-app">
<h1>qiankun微前端主应用</h1>
<p>当前时间:{formatDate(new Date())}</p>
{/* ... 其他内容 */}
</div>
)
}
七、总结
项目整体是用来练手去学习微前端项目,适合用来简单入门,上手,实际情况中还有很多未考虑的地方,为了实现多技术栈大乱炖也确实碰了很多壁,参考了很多才成功运行不报错。微前端子应用是同一个技术栈就更方便管理。
后续可扩展方向:全局状态管理(qiankun的initGlobalState)、权限统一控制、构建打包优化、CI/CD流程集成等。