每天一个高级前端知识 - Day 15
今日主题:前端架构设计 - 微前端、组件库、Monorepo的最佳实践
核心概念:架构是技术与组织的桥梁
好的架构不是技术的堆砌,而是对业务复杂度和团队协作的深刻理解后的权衡。
🏗️ 架构设计的四个维度
┌─────────────────────────────────────────────┐
│ 业务架构 (Business) │
│ 领域驱动设计、业务流程、价值流 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 应用架构 (Application) │
│ 微前端、模块联邦、路由、状态管理 │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 技术架构 (Technical) │
│ 框架选型、构建工具、性能优化、CI/CD │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ 基础设施 (Infrastructure) │
│ Monorepo、部署环境、监控告警 │
└─────────────────────────────────────────────┘
📦 Monorepo架构(Turborepo + pnpm)
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"],
"env": ["NEXT_PUBLIC_*"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"],
"outputs": []
},
"lint": {
"dependsOn": ["^lint"]
},
"dev": {
"cache": false,
"persistent": true
},
"clean": {
"cache": false
}
}
}
项目结构:
monorepo/
├── apps/
│ ├── web/ # 主应用 (Next.js)
│ ├── admin/ # 管理后台 (Vite + React)
│ ├── mobile/ # 移动端 (React Native)
│ └── docs/ # 文档站点 (Docusaurus)
├── packages/
│ ├── ui/ # UI组件库
│ ├── config/ # 共享配置 (ESLint, TS, Tailwind)
│ ├── types/ # 共享TypeScript类型
│ ├── utils/ # 工具函数库
│ └── api/ # API客户端
├── tooling/
│ ├── eslint-config/ # 自定义ESLint配置
│ ├── typescript-config/ # TS配置预设
│ └── prettier-config/ # Prettier配置
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── tsconfig.json
🎯 微前端架构实战(qiankun + Module Federation)
// apps/web/src/micro-frontend/index.ts
import { registerMicroApps, start, initGlobalState } from 'qiankun';
import { loadRemoteModule } from '@angular-architects/module-federation';
// 全局状态管理
const initialState = {
user: null,
theme: 'light',
permissions: []
};
const actions = initGlobalState(initialState);
actions.onGlobalStateChange((state, prev) => {
console.log('全局状态变更:', state);
// 同步到localStorage
localStorage.setItem('global-state', JSON.stringify(state));
});
// 子应用配置
const microApps = [
{
name: 'react-dashboard',
entry: '//localhost:3001',
container: '#subapp-container',
activeRule: '/dashboard',
props: {
getGlobalState: () => actions.getGlobalState(),
setGlobalState: (state) => actions.setGlobalState(state)
}
},
{
name: 'vue-admin',
entry: '//localhost:3002',
container: '#subapp-container',
activeRule: '/admin',
props: { permissions: ['admin'] }
},
{
name: 'angular-editor',
entry: '//localhost:3003',
container: '#subapp-container',
activeRule: '/editor',
// 预加载
loader: (loading) => {
console.log('子应用加载状态:', loading);
}
}
];
// 生命周期钩子
registerMicroApps(microApps, {
beforeLoad: (app) => {
console.log('before load', app.name);
return Promise.resolve();
},
beforeMount: (app) => {
console.log('before mount', app.name);
return Promise.resolve();
},
afterMount: (app) => {
console.log('after mount', app.name);
// 上报监控数据
reportAppLoaded(app.name);
return Promise.resolve();
},
beforeUnmount: (app) => {
console.log('before unmount', app.name);
return Promise.resolve();
}
});
// 启动微前端
start({
prefetch: 'all', // 预加载所有子应用
sandbox: {
strictStyleIsolation: true, // 严格样式隔离
experimentalStyleIsolation: true
},
singular: false // 支持同时挂载多个子应用
});
// 动态加载子应用
export async function loadSubApp(name: string) {
const app = microApps.find(a => a.name === name);
if (!app) throw new Error(`App ${name} not found`);
// 使用Module Federation动态加载
const module = await loadRemoteModule({
remoteEntry: app.entry,
remoteName: name,
exposedModule: './App'
});
return module;
}
🎨 组件库架构(分层设计)
// packages/ui/src/index.ts
// 分层导出
export * from './atoms'; // 原子组件: Button, Input, Icon
export * from './molecules'; // 分子组件: FormField, Card, Modal
export * from './organisms'; // 有机体: Header, Sidebar, DataTable
export * from './templates'; // 模板: DashboardLayout, AuthLayout
export * from './hooks'; // 共享Hooks
export * from './tokens'; // 设计令牌
// 按需加载支持
export { default as Button } from './atoms/Button';
export type { ButtonProps } from './atoms/Button';
// packages/ui/src/atoms/Button/Button.tsx
import React, { forwardRef } from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '../../utils/cn';
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-accent hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline'
},
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
icon: 'h-10 w-10'
}
},
defaultVariants: {
variant: 'default',
size: 'default'
}
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, loading, children, disabled, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
disabled={disabled || loading}
{...props}
>
{loading && <LoaderIcon className="mr-2 h-4 w-4 animate-spin" />}
{children}
</button>
);
}
);
Button.displayName = 'Button';
🔧 状态管理架构(多模式并存)
// packages/store/src/index.ts
// 根据不同场景选择不同方案
// 1. 服务端状态: React Query / TanStack Query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5分钟
cacheTime: 1000 * 60 * 10, // 10分钟
retry: 3,
refetchOnWindowFocus: false
}
}
});
// 2. 客户端状态: Zustand (轻量级)
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface UserStore {
user: User | null;
permissions: string[];
setUser: (user: User) => void;
logout: () => void;
hasPermission: (permission: string) => boolean;
}
export const useUserStore = create<UserStore>()(
persist(
(set, get) => ({
user: null,
permissions: [],
setUser: (user) => set({ user, permissions: user.permissions }),
logout: () => set({ user: null, permissions: [] }),
hasPermission: (permission) => get().permissions.includes(permission)
}),
{ name: 'user-storage' }
)
);
// 3. UI状态: useReducer + Context (局部)
import React, { createContext, useContext, useReducer } from 'react';
interface UIState {
sidebarOpen: boolean;
theme: 'light' | 'dark';
notifications: Notification[];
}
type UIAction =
| { type: 'TOGGLE_SIDEBAR' }
| { type: 'SET_THEME'; payload: 'light' | 'dark' }
| { type: 'ADD_NOTIFICATION'; payload: Notification };
const uiReducer = (state: UIState, action: UIAction): UIState => {
switch (action.type) {
case 'TOGGLE_SIDEBAR':
return { ...state, sidebarOpen: !state.sidebarOpen };
case 'SET_THEME':
return { ...state, theme: action.payload };
case 'ADD_NOTIFICATION':
return { ...state, notifications: [...state.notifications, action.payload] };
default:
return state;
}
};
const UIContext = createContext<{ state: UIState; dispatch: React.Dispatch<UIAction> } | null>(null);
export const UIProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [state, dispatch] = useReducer(uiReducer, {
sidebarOpen: true,
theme: 'light',
notifications: []
});
return (
<UIContext.Provider value={{ state, dispatch }}>
{children}
</UIContext.Provider>
);
};
// 4. 表单状态: React Hook Form
import { useForm, UseFormProps } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
export function useAppForm<T extends z.ZodSchema>(schema: T, options?: UseFormProps<z.infer<T>>) {
return useForm<z.infer<T>>({
resolver: zodResolver(schema),
mode: 'onBlur',
...options
});
}
🚀 CI/CD流水线设计
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
jobs:
quality:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
with:
version: 8
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Type check
run: pnpm turbo type-check
- name: Lint
run: pnpm turbo lint
- name: Unit test
run: pnpm turbo test
- name: Build
run: pnpm turbo build
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: |
apps/*/dist
apps/*/.next
packages/*/dist
e2e:
needs: quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Run E2E tests
run: pnpm turbo e2e
- name: Upload E2E report
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
deploy:
needs: [quality, e2e]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- name: Deploy to Vercel
run: |
pnpm vercel --prod --token=${{ secrets.VERCEL_TOKEN }}
- name: Deploy to CDN
run: |
pnpm turbo deploy --scope=@company/ui
aws s3 sync packages/ui/dist s3://cdn.company.com/ui --delete
- name: Create Sentry release
uses: getsentry/action-release@v1
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
with:
environment: production
version: ${{ github.sha }}
📊 架构决策记录
# ADR-001: 选择Turborepo作为Monorepo工具
## 状态
已采纳
## 上下文
团队规模增长到15人,管理5个应用和8个共享包,需要:
- 快速的构建和测试
- 代码复用和共享
- 统一的工具链
- 独立部署能力
## 决策
选择Turborepo + pnpm workspaces
## 理由
1. 增量构建:缓存机制使构建速度提升80%
2. 远程缓存:CI/CD中共享缓存
3. 任务编排:清晰的依赖关系管理
4. 零配置:开箱即用的TypeScript支持
## 后果
- 迁移成本:1周
- 学习成本:团队成员需了解turbo.json配置
- CI成本:需要配置TURBO_TOKEN
## 备选方案
- Nx: 功能更强但配置复杂
- Lerna: 功能有限,维护不活跃
- 原生pnpm workspaces: 缺少任务编排
🎯 今日挑战
设计一个完整的B端应用架构,要求:
- 使用Monorepo管理(至少3个应用 + 2个共享包)
- 实现微前端架构(至少2个子应用)
- 设计组件库分层(atoms/molecules/organisms)
- 实现多种状态管理策略
- 配置完整的CI/CD流水线
- 编写架构决策记录(至少3个ADR)
明日预告:性能监控与优化 - 从用户体验到Core Web Vitals的完整实践
💡 架构箴言:"架构是面向未来的妥协"——为未知的变化预留扩展点,但不过度设计!