Files
DFJ/02_详细设计文档/前端技术架构详设.md

24 KiB
Raw Blame History

前端技术架构详设文档

文档版本: v1.0
撰写人: 前端架构师
创建日期: 2024年9月11日

1. 前端架构总览

1.1 技术栈详细说明

// 核心技术栈配置
{
  "framework": "Taro 4.x",           // 跨端开发框架
  "ui": "React 18.2.0",             // UI框架
  "language": "TypeScript 5.0+",    // 开发语言
  "state": "Zustand 4.4.0",         // 状态管理
  "css": "Sass + CSS Modules",      // 样式方案
  "build": "Webpack 5 + SWC",       // 构建工具
  "lint": "ESLint + Prettier",      // 代码规范
  "test": "Jest + Testing Library"  // 测试框架
}

1.2 项目目录结构

src/
├── components/           # 通用组件
│   ├── GameBoard/       # 游戏棋盘组件
│   ├── PlaneShape/      # 飞机形状组件
│   ├── Modal/           # 模态框组件
│   └── Loading/         # 加载组件
├── pages/               # 页面组件
│   ├── entry/           # 入口页面
│   ├── room/            # 房间相关页面
│   └── game/            # 游戏页面
├── hooks/               # 自定义Hooks
│   ├── useGame.ts       # 游戏逻辑Hook
│   ├── useWebSocket.ts  # WebSocket Hook
│   └── useAuth.ts       # 认证Hook
├── store/               # 状态管理
│   ├── gameStore.ts     # 游戏状态
│   ├── userStore.ts     # 用户状态
│   └── roomStore.ts     # 房间状态
├── services/            # 业务服务
│   ├── api.ts           # API封装
│   ├── websocket.ts     # WebSocket服务
│   └── storage.ts       # 本地存储
├── utils/               # 工具函数
│   ├── gameLogic.ts     # 游戏逻辑工具
│   ├── helpers.ts       # 通用工具
│   └── constants.ts     # 常量定义
└── styles/              # 样式文件
    ├── variables.scss   # 变量定义
    ├── mixins.scss      # 混合器
    └── global.scss      # 全局样式

2. 状态管理架构

2.1 Zustand Store 设计

// 用户状态Store
interface UserState {
  user: User | null
  isAuthenticated: boolean
  login: (userInfo: WxUserInfo) => Promise<void>
  logout: () => void
  updateProfile: (profile: Partial<User>) => void
}

export const useUserStore = create<UserState>((set, get) => ({
  user: null,
  isAuthenticated: false,
  
  login: async (userInfo: WxUserInfo) => {
    try {
      const user = await apiService.login(userInfo)
      set({ user, isAuthenticated: true })
      // 持久化存储
      await storage.setUser(user)
    } catch (error) {
      throw new Error('登录失败')
    }
  },
  
  logout: () => {
    set({ user: null, isAuthenticated: false })
    storage.clearUser()
  },
  
  updateProfile: (profile: Partial<User>) => {
    const currentUser = get().user
    if (currentUser) {
      set({ user: { ...currentUser, ...profile } })
    }
  }
}))

// 游戏状态Store
interface GameState {
  // 当前游戏状态
  currentGame: GameSession | null
  gamePhase: GamePhase
  currentPlayer: string
  myBoard: GameBoard
  opponentBoard: GameBoard
  
  // 操作方法
  initGame: (gameData: GameInitData) => void
  placePlane: (plane: PlaneData) => boolean
  attack: (position: Position) => void
  updateGameState: (state: GameStateUpdate) => void
  resetGame: () => void
}

export const useGameStore = create<GameState>((set, get) => ({
  currentGame: null,
  gamePhase: GamePhase.WAITING,
  currentPlayer: '',
  myBoard: createEmptyBoard(),
  opponentBoard: createEmptyBoard(),
  
  initGame: (gameData: GameInitData) => {
    set({
      currentGame: gameData.session,
      gamePhase: GamePhase.PLACING,
      currentPlayer: gameData.firstPlayer,
      myBoard: createEmptyBoard(),
      opponentBoard: createEmptyBoard()
    })
  },
  
  placePlane: (plane: PlaneData): boolean => {
    const state = get()
    const newBoard = GameLogic.placePlane(state.myBoard, plane)
    
    if (newBoard) {
      set({ myBoard: newBoard })
      return true
    }
    return false
  },
  
  attack: (position: Position) => {
    const state = get()
    if (state.gamePhase !== GamePhase.BATTLING) return
    
    // 发送攻击请求
    websocketService.sendAttack(position)
  },
  
  updateGameState: (update: GameStateUpdate) => {
    set(state => ({
      ...state,
      ...update
    }))
  },
  
  resetGame: () => {
    set({
      currentGame: null,
      gamePhase: GamePhase.WAITING,
      currentPlayer: '',
      myBoard: createEmptyBoard(),
      opponentBoard: createEmptyBoard()
    })
  }
}))

// 房间状态Store
interface RoomState {
  currentRoom: Room | null
  availableRooms: Room[]
  connectionStatus: ConnectionStatus
  
  createRoom: () => Promise<Room>
  joinRoom: (roomCode: string) => Promise<boolean>
  leaveRoom: () => void
  refreshRoomList: () => Promise<void>
}

export const useRoomStore = create<RoomState>((set, get) => ({
  currentRoom: null,
  availableRooms: [],
  connectionStatus: ConnectionStatus.DISCONNECTED,
  
  createRoom: async (): Promise<Room> => {
    const room = await apiService.createRoom()
    set({ currentRoom: room })
    // 连接WebSocket
    await websocketService.joinRoom(room.code)
    return room
  },
  
  joinRoom: async (roomCode: string): Promise<boolean> => {
    try {
      const room = await apiService.joinRoom(roomCode)
      set({ currentRoom: room })
      await websocketService.joinRoom(roomCode)
      return true
    } catch (error) {
      return false
    }
  },
  
  leaveRoom: () => {
    const room = get().currentRoom
    if (room) {
      websocketService.leaveRoom(room.code)
      set({ currentRoom: null })
    }
  },
  
  refreshRoomList: async () => {
    const rooms = await apiService.getRoomList()
    set({ availableRooms: rooms })
  }
}))

2.2 状态持久化策略

// 状态持久化中间件
const persistMiddleware = <T>(
  config: StateCreator<T>,
  options: {
    name: string
    storage: Storage
    partialize?: (state: T) => Partial<T>
  }
) => (set: any, get: any, api: any) => 
  config(
    (partial, replace) => {
      const nextState = typeof partial === 'function' ? partial(get()) : partial
      const stateToStore = options.partialize ? options.partialize(nextState) : nextState
      
      // 更新状态
      set(partial, replace)
      
      // 持久化存储
      options.storage.setItem(options.name, JSON.stringify(stateToStore))
    },
    get,
    api
  )

// 使用示例
export const useUserStore = create<UserState>()(
  persistMiddleware(
    (set, get) => ({
      // 状态定义...
    }),
    {
      name: 'user-store',
      storage: wx.getStorageSync ? wxStorage : localStorage,
      partialize: (state) => ({ user: state.user, isAuthenticated: state.isAuthenticated })
    }
  )
)

3. 组件设计架构

3.1 基础组件设计

// 游戏棋盘组件
interface GameBoardProps {
  board: GameBoard
  mode: 'placement' | 'battle' | 'readonly'
  onCellClick?: (position: Position) => void
  onPlanePlace?: (plane: PlaneData) => void
  showAttackResults?: boolean
  className?: string
}

export const GameBoard: React.FC<GameBoardProps> = ({
  board,
  mode,
  onCellClick,
  onPlanePlace,
  showAttackResults = false,
  className = ''
}) => {
  const [selectedCell, setSelectedCell] = useState<Position | null>(null)
  const [dragState, setDragState] = useState<DragState | null>(null)
  
  // 处理单元格点击
  const handleCellClick = useCallback((position: Position) => {
    if (mode === 'readonly') return
    
    if (mode === 'placement') {
      handlePlacementClick(position)
    } else if (mode === 'battle') {
      handleBattleClick(position)
    }
  }, [mode, onCellClick, onPlanePlace])
  
  // 处理飞机放置
  const handlePlacementClick = (position: Position) => {
    // 飞机放置逻辑
    if (onPlanePlace) {
      const plane = createPlaneAt(position, currentDirection)
      onPlanePlace(plane)
    }
  }
  
  // 处理攻击点击
  const handleBattleClick = (position: Position) => {
    if (onCellClick) {
      onCellClick(position)
    }
  }
  
  return (
    <div className={`game-board ${className} ${mode}`}>
      {board.cells.map((row, x) =>
        row.map((cell, y) => (
          <BoardCell
            key={`${x}-${y}`}
            cell={cell}
            position={{ x, y }}
            isSelected={isSelected(position)}
            onClick={handleCellClick}
            showAttackResult={showAttackResults}
          />
        ))
      )}
    </div>
  )
}

// 棋盘单元格组件
interface BoardCellProps {
  cell: BoardCell
  position: Position
  isSelected: boolean
  onClick: (position: Position) => void
  showAttackResult: boolean
}

const BoardCell: React.FC<BoardCellProps> = ({
  cell,
  position,
  isSelected,
  onClick,
  showAttackResult
}) => {
  const handleClick = () => onClick(position)
  
  const cellClass = [
    'board-cell',
    cell.state,
    isSelected && 'selected',
    cell.planeId && 'has-plane'
  ].filter(Boolean).join(' ')
  
  return (
    <div 
      className={cellClass}
      onClick={handleClick}
      data-position={`${position.x}-${position.y}`}
    >
      {showAttackResult && cell.attackResult && (
        <div className={`attack-result ${cell.attackResult.type}`}>
          {cell.attackResult.type === 'HIT' ? '🎯' : '💥'}
        </div>
      )}
      {cell.planeId && (
        <div className="plane-indicator" />
      )}
    </div>
  )
}

3.2 页面组件架构

// 游戏对战页面
const BattlePage: React.FC = () => {
  const gameState = useGameStore()
  const userState = useUserStore()
  const { connectWebSocket, sendMessage } = useWebSocket()
  
  // 游戏初始化
  useEffect(() => {
    initializeGame()
  }, [])
  
  // WebSocket消息处理
  useEffect(() => {
    const handleMessage = (message: GameMessage) => {
      switch (message.type) {
        case 'GAME_STATE_UPDATE':
          gameState.updateGameState(message.data)
          break
        case 'ATTACK_RESULT':
          handleAttackResult(message.data)
          break
        case 'PLAYER_DISCONNECTED':
          handlePlayerDisconnect(message.data)
          break
      }
    }
    
    connectWebSocket(handleMessage)
  }, [])
  
  const initializeGame = async () => {
    try {
      const gameData = await apiService.getGameState(gameState.currentGame?.id)
      gameState.initGame(gameData)
    } catch (error) {
      showError('游戏初始化失败')
    }
  }
  
  const handleAttack = (position: Position) => {
    gameState.attack(position)
  }
  
  const handleAttackResult = (result: AttackResult) => {
    gameState.updateGameState({
      opponentBoard: applyAttackResult(gameState.opponentBoard, result)
    })
    
    if (result.gameEnded) {
      showGameResult(result.winner)
    }
  }
  
  return (
    <div className="battle-page">
      <GameHeader 
        currentPlayer={gameState.currentPlayer}
        gamePhase={gameState.gamePhase}
      />
      
      <div className="battle-boards">
        <div className="my-board-section">
          <h3>我的棋盘</h3>
          <GameBoard
            board={gameState.myBoard}
            mode="readonly"
            showAttackResults={true}
          />
        </div>
        
        <div className="opponent-board-section">
          <h3>对手棋盘</h3>
          <GameBoard
            board={gameState.opponentBoard}
            mode="battle"
            onCellClick={handleAttack}
            showAttackResults={true}
          />
        </div>
      </div>
      
      <GameControls 
        gamePhase={gameState.gamePhase}
        currentPlayer={gameState.currentPlayer}
        onSurrender={() => gameState.surrender()}
      />
    </div>
  )
}

4. 自定义Hooks设计

4.1 游戏逻辑Hooks

// 游戏逻辑Hook
export const useGame = () => {
  const gameStore = useGameStore()
  const [loading, setLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)
  
  // 开始游戏
  const startGame = async (roomCode: string) => {
    setLoading(true)
    setError(null)
    
    try {
      const gameData = await apiService.startGame(roomCode)
      gameStore.initGame(gameData)
    } catch (err) {
      setError('游戏启动失败')
    } finally {
      setLoading(false)
    }
  }
  
  // 放置飞机
  const placePlane = (plane: PlaneData) => {
    const success = gameStore.placePlane(plane)
    if (!success) {
      setError('飞机放置失败,请检查位置')
      return false
    }
    
    // 如果已放置3架飞机自动进入准备状态
    if (gameStore.myBoard.planes.length === 3) {
      confirmPlacement()
    }
    
    return true
  }
  
  // 确认飞机布置
  const confirmPlacement = async () => {
    try {
      await apiService.confirmPlacement(gameStore.myBoard.planes)
      gameStore.updateGameState({ gamePhase: GamePhase.WAITING_OPPONENT })
    } catch (err) {
      setError('确认布置失败')
    }
  }
  
  // 执行攻击
  const attack = async (position: Position) => {
    if (gameStore.gamePhase !== GamePhase.BATTLING) return
    
    try {
      const result = await apiService.attack(position)
      gameStore.updateGameState({
        opponentBoard: applyAttackResult(gameStore.opponentBoard, result)
      })
      
      if (result.gameEnded) {
        gameStore.updateGameState({ gamePhase: GamePhase.FINISHED })
      }
    } catch (err) {
      setError('攻击失败')
    }
  }
  
  return {
    ...gameStore,
    loading,
    error,
    startGame,
    placePlane,
    confirmPlacement,
    attack,
    clearError: () => setError(null)
  }
}

// WebSocket Hook
export const useWebSocket = () => {
  const [socket, setSocket] = useState<WebSocketConnection | null>(null)
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected')
  const [lastMessage, setLastMessage] = useState<GameMessage | null>(null)
  
  // 连接WebSocket
  const connect = useCallback((onMessage: (message: GameMessage) => void) => {
    if (socket?.readyState === WebSocket.OPEN) return
    
    const ws = new WebSocketConnection({
      url: WS_URL,
      onOpen: () => setConnectionState('connected'),
      onClose: () => setConnectionState('disconnected'),
      onError: () => setConnectionState('error'),
      onMessage: (message) => {
        setLastMessage(message)
        onMessage(message)
      }
    })
    
    setSocket(ws)
    setConnectionState('connecting')
  }, [socket])
  
  // 发送消息
  const sendMessage = useCallback((message: GameMessage) => {
    if (socket?.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify(message))
    }
  }, [socket])
  
  // 断线重连
  useEffect(() => {
    let reconnectTimer: NodeJS.Timeout
    
    if (connectionState === 'disconnected') {
      reconnectTimer = setTimeout(() => {
        if (socket) {
          connect(() => {})  // 重连
        }
      }, 3000)
    }
    
    return () => clearTimeout(reconnectTimer)
  }, [connectionState, connect])
  
  // 清理连接
  useEffect(() => {
    return () => {
      if (socket) {
        socket.close()
      }
    }
  }, [])
  
  return {
    connectionState,
    lastMessage,
    connect,
    sendMessage,
    disconnect: () => socket?.close()
  }
}

4.2 UI交互Hooks

// 模态框Hook
export const useModal = () => {
  const [isOpen, setIsOpen] = useState(false)
  const [title, setTitle] = useState('')
  const [content, setContent] = useState<React.ReactNode>(null)
  
  const openModal = (modalTitle: string, modalContent: React.ReactNode) => {
    setTitle(modalTitle)
    setContent(modalContent)
    setIsOpen(true)
  }
  
  const closeModal = () => {
    setIsOpen(false)
  }
  
  const Modal = ({ children }: { children?: React.ReactNode }) => {
    if (!isOpen) return null
    
    return (
      <div className="modal-overlay" onClick={closeModal}>
        <div className="modal-content" onClick={(e) => e.stopPropagation()}>
          <div className="modal-header">
            <h3>{title}</h3>
            <button onClick={closeModal}>×</button>
          </div>
          <div className="modal-body">
            {content || children}
          </div>
        </div>
      </div>
    )
  }
  
  return {
    isOpen,
    openModal,
    closeModal,
    Modal
  }
}

// 加载状态Hook
export const useLoading = () => {
  const [loading, setLoading] = useState(false)
  const [loadingText, setLoadingText] = useState('加载中...')
  
  const showLoading = (text = '加载中...') => {
    setLoadingText(text)
    setLoading(true)
  }
  
  const hideLoading = () => {
    setLoading(false)
  }
  
  const LoadingComponent = () => {
    if (!loading) return null
    
    return (
      <div className="loading-overlay">
        <div className="loading-spinner">
          <div className="spinner"></div>
          <p>{loadingText}</p>
        </div>
      </div>
    )
  }
  
  return {
    loading,
    showLoading,
    hideLoading,
    LoadingComponent
  }
}

5. 样式架构设计

5.1 设计Token系统

// styles/variables.scss - 设计变量定义
:root {
  // 颜色系统 - 基于原型的深色科技主题
  --color-primary: #6366f1;
  --color-secondary: #40e0d0;
  --color-accent: #ff6b6b;
  --color-success: #51cf66;
  --color-warning: #ffd43b;
  --color-danger: #ff6b6b;
  
  // 背景色
  --bg-primary: #0f1419;
  --bg-secondary: #1a1a2e;
  --bg-card: #16213e;
  --bg-overlay: rgba(15, 20, 25, 0.9);
  
  // 渐变背景
  --gradient-primary: linear-gradient(135deg, #0f0f1a 0%, #1a1a2e 30%, #16213e 70%, #0f3460 100%);
  --gradient-card: linear-gradient(145deg, #1e2a47 0%, #2a3f5f 100%);
  
  // 文字颜色
  --text-primary: #ffffff;
  --text-secondary: #b8c5d3;
  --text-muted: #6c7983;
  --text-disabled: #4a5568;
  
  // 边框和分割线
  --border-color: #2d3748;
  --border-hover: #4a5568;
  --divider: #2d3748;
  
  // 阴影
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
  --shadow-md: 0 4px 6px rgba(0, 0, 0, 0.16);
  --shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.2);
  --shadow-glow: 0 0 20px rgba(99, 102, 241, 0.3);
  
  // 间距系统
  --space-xs: 4px;
  --space-sm: 8px;
  --space-md: 16px;
  --space-lg: 24px;
  --space-xl: 32px;
  --space-xxl: 48px;
  
  // 字体大小
  --font-xs: 12px;
  --font-sm: 14px;
  --font-base: 16px;
  --font-lg: 18px;
  --font-xl: 20px;
  --font-2xl: 24px;
  --font-3xl: 30px;
  
  // 圆角
  --radius-sm: 4px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-xl: 16px;
  --radius-full: 50%;
  
  // Z-index层级
  --z-modal: 1000;
  --z-loading: 999;
  --z-toast: 998;
  --z-header: 100;
}

5.2 组件样式库

// styles/components.scss - 组件样式
.game-board {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  grid-template-rows: repeat(10, 1fr);
  gap: 2px;
  padding: var(--space-md);
  background: var(--gradient-card);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  
  &.placement {
    .board-cell {
      cursor: pointer;
      
      &:hover {
        background: var(--color-primary);
        opacity: 0.7;
      }
    }
  }
  
  &.battle {
    .board-cell {
      cursor: crosshair;
      
      &:hover:not(.attacked) {
        background: var(--color-accent);
        opacity: 0.8;
      }
    }
  }
}

.board-cell {
  aspect-ratio: 1;
  background: var(--bg-secondary);
  border: 1px solid var(--border-color);
  border-radius: var(--radius-sm);
  position: relative;
  transition: all 0.2s ease;
  
  &.has-plane {
    background: var(--color-secondary);
  }
  
  &.attacked-miss {
    background: var(--bg-card);
    
    &::after {
      content: '○';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: var(--text-muted);
      font-size: var(--font-sm);
    }
  }
  
  &.attacked-hit {
    background: var(--color-danger);
    
    &::after {
      content: '✕';
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      color: var(--text-primary);
      font-size: var(--font-base);
      font-weight: bold;
    }
  }
  
  &.selected {
    background: var(--color-primary);
    box-shadow: var(--shadow-glow);
  }
}

.attack-result {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: var(--font-lg);
  animation: attackResult 0.8s ease-out;
  
  &.HIT {
    color: var(--color-success);
  }
  
  &.MISS {
    color: var(--text-muted);
  }
  
  &.DESTROY {
    color: var(--color-danger);
    font-size: var(--font-xl);
  }
}

@keyframes attackResult {
  0% {
    transform: translate(-50%, -50%) scale(0);
    opacity: 0;
  }
  50% {
    transform: translate(-50%, -50%) scale(1.5);
    opacity: 1;
  }
  100% {
    transform: translate(-50%, -50%) scale(1);
    opacity: 1;
  }
}

6. 性能优化策略

6.1 渲染优化

// 使用React.memo优化组件渲染
export const GameBoard = React.memo<GameBoardProps>(({
  board,
  mode,
  onCellClick,
  onPlanePlace
}) => {
  // 只有相关props变化时才重渲染
}, (prevProps, nextProps) => {
  return (
    prevProps.board === nextProps.board &&
    prevProps.mode === nextProps.mode &&
    prevProps.onCellClick === nextProps.onCellClick &&
    prevProps.onPlanePlace === nextProps.onPlanePlace
  )
})

// 使用useMemo缓存计算结果
const BoardCell = React.memo<BoardCellProps>(({ cell, position, onClick }) => {
  const cellClass = useMemo(() => {
    return [
      'board-cell',
      cell.state,
      cell.planeId && 'has-plane'
    ].filter(Boolean).join(' ')
  }, [cell.state, cell.planeId])
  
  const handleClick = useCallback(() => {
    onClick(position)
  }, [onClick, position])
  
  return (
    <div className={cellClass} onClick={handleClick}>
      {/* 单元格内容 */}
    </div>
  )
})

6.2 状态更新优化

// 批量状态更新
class GameStateManager {
  private updateQueue: StateUpdate[] = []
  private isUpdating = false
  
  queueUpdate(update: StateUpdate) {
    this.updateQueue.push(update)
    if (!this.isUpdating) {
      this.flushUpdates()
    }
  }
  
  private async flushUpdates() {
    this.isUpdating = true
    
    // 合并所有更新
    const mergedUpdate = this.updateQueue.reduce((merged, update) => {
      return { ...merged, ...update }
    }, {})
    
    // 批量应用更新
    useGameStore.setState(state => ({
      ...state,
      ...mergedUpdate
    }))
    
    this.updateQueue = []
    this.isUpdating = false
  }
}

7. 错误处理机制

7.1 错误边界设计

// 游戏错误边界
class GameErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean; error: Error | null }
> {
  constructor(props: any) {
    super(props)
    this.state = { hasError: false, error: null }
  }
  
  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error }
  }
  
  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('游戏错误:', error, errorInfo)
    
    // 上报错误
    this.reportError(error, errorInfo)
  }
  
  private reportError(error: Error, errorInfo: ErrorInfo) {
    // 调用错误上报服务
    errorReportingService.log({
      message: error.message,
      stack: error.stack,
      componentStack: errorInfo.componentStack
    })
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-fallback">
          <h2>游戏出错了</h2>
          <p>我们已经记录了错误信息,请稍后重试。</p>
          <button onClick={() => window.location.reload()}>刷新页面</button>
        </div>
      )
    }
    
    return this.props.children
  }
}