每天一个高级前端知识 - Day 15

0 阅读4分钟

每天一个高级前端知识 - 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端应用架构,要求:

  1. 使用Monorepo管理(至少3个应用 + 2个共享包)
  2. 实现微前端架构(至少2个子应用)
  3. 设计组件库分层(atoms/molecules/organisms)
  4. 实现多种状态管理策略
  5. 配置完整的CI/CD流水线
  6. 编写架构决策记录(至少3个ADR)

明日预告:性能监控与优化 - 从用户体验到Core Web Vitals的完整实践

💡 架构箴言:"架构是面向未来的妥协"——为未知的变化预留扩展点,但不过度设计!