微前端项目搭建完整教程 - 基于 Qiankun
前言
微前端是一种将前端应用分解为更小、更简单的块的架构风格,每个块可以由不同的团队独立开发、测试和部署。本文将详细介绍如何从零开始搭建一个基于 Qiankun 的微前端项目。
目录
技术选型
本教程采用以下技术栈:
| 应用类型 | 技术栈 | 构建工具 | 端口 |
|---|---|---|---|
| 主应用 | React 19 + TypeScript | Vite | 7000 |
| 子应用1 | React + TypeScript | Umi 4 | 7001 |
| 子应用2 | Vue 2 | Webpack (Vue CLI) | 7002 |
| 子应用3 | Vue 3 + TypeScript | Vite | 7003 |
| 微前端框架 | Qiankun 2.x | - | - |
| 包管理器 | pnpm + workspace | - | - |
项目架构
monorepo/
├── packages/ # 所有应用目录
│ ├── main/ # 主应用 (React + Vite)
│ ├── react-app/ # 子应用1 (React + Umi4)
│ ├── vue2-app/ # 子应用2 (Vue2 + Webpack)
│ └── vue3-app/ # 子应用3 (Vue3 + Vite)
├── common/ # 公共工具库
├── scripts/ # 启动脚本
├── package.json # 根目录配置
└── pnpm-workspace.yaml # pnpm workspace 配置
环境准备
1. 安装 Node.js
确保已安装 Node.js (推荐 v18 或更高版本)
bash
node -v
npm -v
2. 安装 pnpm
bash
npm install -g pnpm
pnpm -v
项目初始化
1. 创建项目根目录
bash
mkdir monorepo
cd monorepo
2. 初始化 package.json
bash
pnpm init
修改 package.json:
json
{
"name": "monorepo",
"version": "1.0.0",
"description": "微前端 monorepo 项目",
"scripts": {
"dev:main": "pnpm --filter main dev",
"dev:react": "pnpm --filter react-app dev",
"dev:vue2": "pnpm --filter vue2-app serve",
"dev:vue3": "pnpm --filter vue3-app dev",
"dev": "node ./scripts/start.js",
"build:all": "pnpm --filter main build && pnpm --filter react-app build && pnpm --filter vue2-app build && pnpm --filter vue3-app build"
}
}
3. 配置 pnpm workspace
创建 pnpm-workspace.yaml:
yaml
packages:
- 'packages/*'
- 'common'
主应用搭建
1. 创建主应用
bash
cd packages
pnpm create vite main -- --template react-ts
cd main
pnpm install
2. 安装依赖
bash
pnpm add qiankun react-router-dom axios
pnpm add -D @types/node sass
3. 配置 Vite
修改 vite.config.ts:
typescript
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'
}
})
4. 创建 public-path.ts
创建 src/public-path.ts:
typescript
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
5. 修改主应用入口
修改 src/main.tsx:
typescript
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
6. 创建主应用组件
修改 src/App.tsx:
typescript
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(() => {
registerMicroApps(
[
{
name: 'react-app',
entry: 'http://localhost:7001',
container: '#micro-app-container',
activeRule: '/sub1',
},
{
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
子应用搭建
子应用1:React + Umi4
1. 创建项目
bash
cd packages
pnpm create umi react-app
cd react-app
pnpm install
2. 安装依赖
bash
pnpm add @umijs/plugins
3. 配置 Umi
修改 .umirc.ts:
typescript
import { defineConfig } from "umi";
export default defineConfig({
base: '/sub1',
routes: [
{ path: "/", component: "index" },
{ path: "/docs", component: "docs" },
],
npmClient: 'pnpm',
plugins: ["@umijs/plugins/dist/qiankun"],
qiankun: {
slave: {}
},
});
4. 修改 package.json
json
{
"scripts": {
"dev": "set PORT=7001 && umi dev",
"build": "umi build"
}
}
子应用2:Vue2 + Webpack
1. 创建项目
bash
cd packages
vue create vue2-app
cd vue2-app
pnpm install
2. 创建 public-path.js
创建 public-path.js:
javascript
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3. 修改入口文件
修改 src/main.js:
javascript
import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import "../public-path.js";
Vue.config.productionTip = false;
let instance = null;
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__) {
render();
}
export async function bootstrap(props) {
console.log("vue2应用启动", props);
}
export async function mount(props) {
console.log("vue2子应用:接收主应用参数", props);
render(props);
}
export async function unmount(props) {
console.log("vue2应用卸载", props);
if (instance) {
instance.$destroy();
instance.$el.innerHTML = "";
instance = null;
}
}
4. 配置 Vue CLI
修改 vue.config.js:
javascript
const { name } = require("./package.json");
module.exports = {
devServer: {
port: 7002,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
configureWebpack: {
output: {
library: `${name}-[name]`,
libraryTarget: "umd",
jsonpFunction: `webpackJsonp_${name}`,
},
},
};
子应用3:Vue3 + Vite
1. 创建项目
bash
cd packages
pnpm create vite vue3-app -- --template vue-ts
cd vue3-app
pnpm install
2. 安装依赖
bash
pnpm add vue-router pinia
pnpm add -D vite-plugin-qiankun
3. 创建 public-path.js
创建 public-path.js:
javascript
if (window.__POWERED_BY_QIANKUN__) {
}
4. 修改入口文件
修改 src/main.ts:
typescript
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import router from './router'
import '../public-path.js'
import { renderWithQiankun} from "vite-plugin-qiankun/dist/helper"
let app = null
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()
}
renderWithQiankun({
bootstrap() {
console.log("vue3-vite子应用启动")
},
mount(props) {
console.log("vue3-vite子应用挂载", props)
render(props)
},
update(props) {
console.log("vue3-vite子应用更新", props)
},
unmount() {
console.log("vue3-vite子应用卸载")
app.unmount()
app = null
}
})
5. 配置 Vite
修改 vite.config.ts:
typescript
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import qiankun from 'vite-plugin-qiankun'
export default defineConfig({
plugins: [vue(), qiankun('vue3-app', { useDevMode: true })],
base: '/sub3',
server: {
port: 7003,
cors: true,
headers: {
'Access-Control-Allow-Origin': '*',
},
},
})
公共工具库
1. 创建公共库
bash
cd ..
mkdir common
cd common
pnpm init
2. 创建工具函数
创建 utils.js:
javascript
export function setStorage(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
export function getStorage(key) {
const value = localStorage.getItem(key);
return value ? JSON.parse(value) : null;
}
export function removeStorage(key) {
localStorage.removeItem(key);
}
创建 format.js:
javascript
export function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
3. 创建入口文件
创建 index.js:
javascript
export * from "./utils.js";
export * from "./format.js";
4. 在各应用中使用
在各应用的 package.json 中添加依赖:
json
{
"dependencies": {
"common": "workspace:*"
}
}
在代码中导入使用:
typescript
import { formatDate, setStorage } from 'common';
const currentDate = formatDate(new Date());
setStorage('data', { timestamp: Date.now() });
项目启动脚本
创建 scripts/start.js:
javascript
const { spawn } = require("child_process");
const { join } = require("path");
const rootDir = join(__dirname, "..");
const projects = [
{
name: "主应用 (main)",
cwd: join(rootDir, "packages", "main"),
command: "pnpm",
args: ["dev"],
port: 7000,
color: "\x1b[36m",
},
{
name: "React 子应用 (react-app)",
cwd: join(rootDir, "packages", "react-app"),
command: "pnpm",
args: ["dev"],
port: 7001,
color: "\x1b[33m",
},
{
name: "Vue2 子应用 (vue2-app)",
cwd: join(rootDir, "packages", "vue2-app"),
command: "pnpm",
args: ["serve"],
port: 7002,
color: "\x1b[32m",
},
{
name: "Vue3 子应用 (vue3-app)",
cwd: join(rootDir, "packages", "vue3-app"),
command: "pnpm",
args: ["dev"],
port: 7003,
color: "\x1b[35m",
},
];
const reset = "\x1b[0m";
const green = "\x1b[32m";
console.log(`${green}正在启动所有微前端应用...${reset}\n`);
const processes = projects.map((project) => {
const { name, cwd, command, args, port, color } = project;
console.log(`${color}[${name}]${reset} 正在启动 (端口: ${port})...`);
const proc = spawn(command, args, {
cwd,
stdio: "inherit",
shell: true,
});
proc.on("error", (error) => {
console.error(`${color}[${name}]${reset} 启动失败:`, error.message);
});
proc.on("exit", (code) => {
if (code !== 0 && code !== null) {
console.error(`${color}[${name}]${reset} 进程退出,代码: ${code}`);
}
});
return { name, port, color, process: proc };
});
setTimeout(() => {
console.log(`\n${green}所有应用已启动!${reset}`);
processes.forEach(({ name, port, color }) => {
console.log(`${color}${name}: http://localhost:${port}${reset}`);
});
console.log("\n按 Ctrl+C 停止所有服务\n");
}, 2000);
process.on("SIGINT", () => {
console.log(`\n${green}正在停止所有服务...${reset}`);
processes.forEach(({ name, color, process: proc }) => {
console.log(`${color}[${name}]${reset} 正在停止...`);
proc.kill("SIGINT");
});
setTimeout(() => {
process.exit(0);
}, 1000);
});
process.on("SIGTERM", () => {
processes.forEach(({ process: proc }) => {
proc.kill("SIGTERM");
});
process.exit(0);
});
运行项目
1. 安装所有依赖
在项目根目录执行:
bash
pnpm install
2. 启动所有应用
bash
pnpm dev
3. 访问应用
打开浏览器访问:http://localhost:7000
点击导航按钮切换不同的子应用:
- 子应用1 (React + Umi4): http://localhost:7000/sub1
- 子应用2 (Vue2 + Webpack): http://localhost:7000/sub2
- 子应用3 (Vue3 + Vite): http://localhost:7000/sub3
4. 单独启动某个应用
bash
pnpm dev:main # 只启动主应用
pnpm dev:react # 只启动 React 子应用
pnpm dev:vue2 # 只启动 Vue2 子应用
pnpm dev:vue3 # 只启动 Vue3 子应用
5. 构建所有应用
bash
pnpm build:all
常见问题
1. 跨域问题
确保所有子应用的开发服务器都配置了 CORS:
javascript
headers: {
"Access-Control-Allow-Origin": "*",
}
2. 静态资源路径问题
使用 public-path.js 解决静态资源加载问题:
javascript
if (window.__POWERED_BY_QIANKUN__) {
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
3. 样式隔离
在主应用中开启样式隔离:
typescript
start({
sandbox: { experimentalStyleIsolation: true }
})
4. 应用间通信
Qiankun 提供了多种通信方式:
props 传递:
typescript
registerMicroApps([
{
name: 'sub-app',
entry: '//localhost:port',
container: '#container',
activeRule: '/sub',
props: { data: 'from main' }
}
])
全局状态管理:
typescript
import { initGlobalState } from 'qiankun'
const actions = initGlobalState(state)
actions.onGlobalStateChange((state, prev) => {
console.log(state, prev)
})
actions.setGlobalState(state)
5. 路由模式
确保主应用和子应用的路由模式一致(都使用 history 模式)。
总结
本教程详细介绍了如何从零开始搭建一个基于 Qiankun 的微前端项目,包括:
- 使用 pnpm workspace 管理 monorepo 项目
- 搭建 React + Vite 主应用
- 搭建三个不同技术栈的子应用(React + Umi4、Vue2 + Webpack、Vue3 + Vite)
- 配置 Qiankun 微前端框架
- 创建公共工具库实现代码复用
- 编写启动脚本简化开发流程
微前端架构的优势:
- 技术栈无关:可以使用不同的框架和技术栈
- 独立开发部署:各团队可以独立开发和部署
- 代码复用:通过公共库实现代码共享
- 渐进式升级:可以逐步迁移和升级应用
希望本教程对你有所帮助!