第10章:微前端与大型项目架构
本章目标
- 掌握Monorepo项目管理策略
- 学会微前端架构设计与实现
- 了解组件库开发与维护
- 掌握大型项目最佳实践
10.1 Monorepo项目管理
项目结构设计
1. 典型Monorepo结构
my-monorepo/
├── apps/ # 应用程序
│ ├── web-app/ # 主Web应用
│ ├── admin-app/ # 管理后台
│ ├── mobile-app/ # 移动端应用
│ └── docs/ # 文档站点
├── packages/ # 共享包
│ ├── ui-components/ # UI组件库
│ ├── utils/ # 工具函数
│ ├── api-client/ # API客户端
│ ├── types/ # 类型定义
│ └── config/ # 配置包
├── tools/ # 工具和脚本
│ ├── build-scripts/ # 构建脚本
│ ├── eslint-config/ # ESLint配置
│ └── jest-config/ # Jest配置
├── docs/ # 项目文档
├── package.json # 根package.json
├── pnpm-workspace.yaml # pnpm工作区配置
├── turbo.json # Turborepo配置
└── nx.json # Nx配置(可选)
2. pnpm工作区配置
yaml
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
- 'tools/*'
json
// 根目录package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev --parallel",
"lint": "turbo run lint",
"test": "turbo run test",
"clean": "turbo run clean",
"changeset": "changeset",
"version-packages": "changeset version",
"release": "turbo run build --filter=!@repo/docs && changeset publish"
},
"devDependencies": {
"@changesets/cli": "^2.26.0",
"turbo": "^1.10.0",
"typescript": "^5.0.0"
},
"packageManager": "pnpm@8.6.0"
}
Turborepo配置
1. 基础Turborepo配置
json
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
},
"clean": {
"cache": false
},
"type-check": {
"dependsOn": ["^build"]
}
}
}
2. 高级构建配置
json
// turbo.json - 高级配置
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["NODE_ENV", "API_URL"]
},
"build:prod": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["NODE_ENV", "API_URL", "ANALYTICS_ID"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
},
"remoteCache": {
"signature": true
}
}
依赖管理策略
1. 包间依赖配置
json
// apps/web-app/package.json
{
"name": "@repo/web-app",
"dependencies": {
"@repo/ui-components": "workspace:*",
"@repo/utils": "workspace:*",
"@repo/api-client": "workspace:*",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@repo/eslint-config": "workspace:*",
"@repo/typescript-config": "workspace:*"
}
}
json
// packages/ui-components/package.json
{
"name": "@repo/ui-components",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
},
"devDependencies": {
"@repo/typescript-config": "workspace:*"
}
}
2. 版本管理
javascript
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/docs"]
}
10.2 微前端架构设计
微前端基础架构
1. Module Federation配置
javascript
// apps/shell/webpack.config.js - 主应用配置
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3000,
},
plugins: [
new ModuleFederationPlugin({
name: 'shell',
remotes: {
userApp: 'userApp@http://localhost:3001/remoteEntry.js',
productApp: 'productApp@http://localhost:3002/remoteEntry.js',
orderApp: 'orderApp@http://localhost:3003/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'react-router-dom': { singleton: true },
},
}),
],
};
javascript
// apps/user-app/webpack.config.js - 微应用配置
const ModuleFederationPlugin = require('@module-federation/webpack');
module.exports = {
mode: 'development',
devServer: {
port: 3001,
},
plugins: [
new ModuleFederationPlugin({
name: 'userApp',
filename: 'remoteEntry.js',
exposes: {
'./UserApp': './src/App',
'./UserList': './src/components/UserList',
'./UserProfile': './src/components/UserProfile',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true },
'react-router-dom': { singleton: true },
},
}),
],
};
2. 微前端路由管理
typescript
// apps/shell/src/App.tsx - 主应用路由
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from './components/ErrorBoundary';
import { Layout } from './components/Layout';
// 动态导入微应用
const UserApp = React.lazy(() => import('userApp/UserApp'));
const ProductApp = React.lazy(() => import('productApp/ProductApp'));
const OrderApp = React.lazy(() => import('orderApp/OrderApp'));
export function App() {
return (
<BrowserRouter>
<Layout>
<Routes>
<Route path="/" element={<div>欢迎来到主应用</div>} />
<Route
path="/users/*"
element={
<ErrorBoundary>
<Suspense fallback={<div>加载用户模块中...</div>}>
<UserApp />
</Suspense>
</ErrorBoundary>
}
/>
<Route
path="/products/*"
element={
<ErrorBoundary>
<Suspense fallback={<div>加载产品模块中...</div>}>
<ProductApp />
</Suspense>
</ErrorBoundary>
}
/>
<Route
path="/orders/*"
element={
<ErrorBoundary>
<Suspense fallback={<div>加载订单模块中...</div>}>
<OrderApp />
</Suspense>
</ErrorBoundary>
}
/>
</Routes>
</Layout>
</BrowserRouter>
);
}
微前端通信机制
1. 事件总线通信
typescript
// packages/shared/src/eventBus.ts
type EventCallback = (data: any) => void;
class EventBus {
private events: Map<string, EventCallback[]> = new Map();
on(event: string, callback: EventCallback) {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
// 返回取消订阅函数
return () => {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
};
}
emit(event: string, data?: any) {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
off(event: string, callback?: EventCallback) {
if (!callback) {
this.events.delete(event);
return;
}
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
}
export const eventBus = new EventBus();
// 预定义事件类型
export const EVENTS = {
USER_LOGIN: 'user:login',
USER_LOGOUT: 'user:logout',
CART_UPDATE: 'cart:update',
NOTIFICATION_SHOW: 'notification:show',
} as const;
2. 状态共享机制
typescript
// packages/shared/src/store.ts
import { create } from 'zustand';
import { subscribeWithSelector } from 'zustand/middleware';
interface GlobalState {
user: {
id: string | null;
name: string | null;
email: string | null;
};
cart: {
items: Array<{ id: string; quantity: number }>;
total: number;
};
theme: 'light' | 'dark';
}
interface GlobalActions {
setUser: (user: GlobalState['user']) => void;
clearUser: () => void;
addToCart: (item: { id: string; quantity: number }) => void;
removeFromCart: (id: string) => void;
setTheme: (theme: GlobalState['theme']) => void;
}
export const useGlobalStore = create<GlobalState & GlobalActions>()(
subscribeWithSelector((set, get) => ({
// 初始状态
user: {
id: null,
name: null,
email: null,
},
cart: {
items: [],
total: 0,
},
theme: 'light',
// 操作方法
setUser: (user) => set({ user }),
clearUser: () => set({
user: { id: null, name: null, email: null }
}),
addToCart: (item) => set((state) => {
const existingItem = state.cart.items.find(i => i.id === item.id);
if (existingItem) {
existingItem.quantity += item.quantity;
} else {
state.cart.items.push(item);
}
return { cart: { ...state.cart } };
}),
removeFromCart: (id) => set((state) => ({
cart: {
...state.cart,
items: state.cart.items.filter(item => item.id !== id)
}
})),
setTheme: (theme) => set({ theme }),
}))
);
// 订阅状态变化
useGlobalStore.subscribe(
(state) => state.user,
(user) => {
// 用户状态变化时的副作用
if (user.id) {
console.log('用户已登录:', user);
} else {
console.log('用户已登出');
}
}
);
10.3 组件库开发与维护
组件库架构
1. 组件库项目结构
packages/ui-components/
├── src/
│ ├── components/ # 组件源码
│ │ ├── Button/
│ │ │ ├── Button.tsx
│ │ │ ├── Button.test.tsx
│ │ │ ├── Button.stories.tsx
│ │ │ └── index.ts
│ │ ├── Input/
│ │ └── index.ts
│ ├── hooks/ # 自定义Hook
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ └── index.ts # 主入口
├── dist/ # 构建输出
├── docs/ # 组件文档
├── .storybook/ # Storybook配置
├── package.json
├── tsconfig.json
├── rollup.config.js # 构建配置
└── README.md
2. 组件开发规范
typescript
// src/components/Button/Button.tsx
import React, { forwardRef } from 'react';
import { cn } from '../../utils/cn';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** 按钮变体 */
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
/** 按钮尺寸 */
size?: 'sm' | 'md' | 'lg';
/** 是否为加载状态 */
loading?: boolean;
/** 加载状态文本 */
loadingText?: string;
/** 图标 */
icon?: React.ReactNode;
/** 图标位置 */
iconPosition?: 'left' | 'right';
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({
children,
className,
variant = 'primary',
size = 'md',
loading = false,
loadingText,
icon,
iconPosition = 'left',
disabled,
...props
}, ref) => {
const baseClasses = 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
const variantClasses = {
primary: 'bg-primary text-primary-foreground hover:bg-primary/90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
ghost: 'hover:bg-accent hover:text-accent-foreground',
danger: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
};
const sizeClasses = {
sm: 'h-9 px-3 text-sm',
md: 'h-10 px-4 py-2',
lg: 'h-11 px-8 text-lg',
};
const isDisabled = disabled || loading;
return (
<button
ref={ref}
className={cn(
baseClasses,
variantClasses[variant],
sizeClasses[size],
className
)}
disabled={isDisabled}
{...props}
>
{loading && (
<svg
className="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
)}
{!loading && icon && iconPosition === 'left' && (
<span className="mr-2">{icon}</span>
)}
{loading ? loadingText || children : children}
{!loading && icon && iconPosition === 'right' && (
<span className="ml-2">{icon}</span>
)}
</button>
);
}
);
Button.displayName = 'Button';
组件文档与测试
1. Storybook配置
typescript
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { Button } from './Button';
const meta: Meta<typeof Button> = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: '通用按钮组件,支持多种变体和状态。',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost', 'danger'],
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
loading: {
control: 'boolean',
},
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
args: {
children: 'Primary Button',
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
children: 'Secondary Button',
variant: 'secondary',
},
};
export const Loading: Story = {
args: {
children: 'Loading Button',
loading: true,
loadingText: 'Loading...',
},
};
export const WithIcon: Story = {
args: {
children: 'With Icon',
icon: '🚀',
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="danger">Danger</Button>
</div>
),
};
2. 组件测试
typescript
// src/components/Button/Button.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Test Button</Button>);
expect(screen.getByRole('button', { name: 'Test Button' })).toBeInTheDocument();
});
it('applies correct variant classes', () => {
const { rerender } = render(<Button variant="primary">Primary</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-primary');
rerender(<Button variant="secondary">Secondary</Button>);
expect(screen.getByRole('button')).toHaveClass('bg-secondary');
});
it('handles click events', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('shows loading state', () => {
render(<Button loading loadingText="Loading...">Submit</Button>);
const button = screen.getByRole('button');
expect(button).toBeDisabled();
expect(screen.getByText('Loading...')).toBeInTheDocument();
});
it('renders with icon', () => {
render(<Button icon="🚀">With Icon</Button>);
expect(screen.getByText('🚀')).toBeInTheDocument();
});
});
组件库构建与发布
1. Rollup构建配置
javascript
// rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import { terser } from 'rollup-plugin-terser';
import postcss from 'rollup-plugin-postcss';
export default {
input: 'src/index.ts',
output: [
{
file: 'dist/index.js',
format: 'cjs',
sourcemap: true,
},
{
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true,
},
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
declaration: true,
declarationDir: 'dist',
}),
postcss({
extract: true,
minimize: true,
}),
terser(),
],
external: ['react', 'react-dom'],
};
2. 自动化发布流程
yaml
# .github/workflows/release-ui.yml
name: Release UI Components
on:
push:
paths:
- 'packages/ui-components/**'
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build components
run: pnpm --filter @repo/ui-components build
- name: Run tests
run: pnpm --filter @repo/ui-components test
- name: Build Storybook
run: pnpm --filter @repo/ui-components build-storybook
- name: Deploy Storybook
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./packages/ui-components/storybook-static
- name: Release
run: pnpm changeset publish
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
本章小结
本章我们学习了:
- Monorepo管理:项目结构设计、Turborepo配置和依赖管理
- 微前端架构:Module Federation配置、路由管理和通信机制
- 组件库开发:组件规范、文档测试和构建发布
- 大型项目实践:架构设计、工程化配置和团队协作
练习题
- 搭建一个完整的Monorepo项目,包含多个应用和共享包
- 实现一个微前端架构,支持独立开发和部署
- 开发一个组件库,包含完整的文档和测试
- 配置自动化的构建、测试和发布流程
教程总结
通过本教程的学习,您已经掌握了现代前端工程化的完整体系:
🎯 核心技能
- 工程化思维:理解前端工程化的价值和必要性
- 工具链掌握:熟练使用现代构建工具和开发工具
- 质量保障:建立完整的代码质量和测试体系
- 性能优化:掌握构建和运行时性能优化技巧
- 自动化流程:实现CI/CD和自动化部署
🚀 实践能力
- 能够搭建现代化的前端开发环境
- 能够设计和实现可扩展的项目架构
- 能够建立完整的开发工作流和质量保障体系
- 能够优化项目性能和开发体验
- 能够管理大型项目和团队协作
📈 持续学习
前端技术发展迅速,建议您:
- 关注新技术和工具的发展趋势
- 参与开源项目和技术社区
- 在实际项目中应用所学知识
- 分享经验和最佳实践
恭喜您完成了前端工程化的学习之旅! 🎉