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

1027 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 前端技术架构详设文档
> **文档版本**: v1.0
> **撰写人**: 前端架构师
> **创建日期**: 2024年9月11日
## 1. 前端架构总览
### 1.1 技术栈详细说明
```typescript
// 核心技术栈配置
{
"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 设计
```typescript
// 用户状态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 状态持久化策略
```typescript
// 状态持久化中间件
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 基础组件设计
```typescript
// 游戏棋盘组件
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 页面组件架构
```typescript
// 游戏对战页面
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
```typescript
// 游戏逻辑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
```typescript
// 模态框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系统
```scss
// 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 组件样式库
```scss
// 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 渲染优化
```typescript
// 使用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 状态更新优化
```typescript
// 批量状态更新
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 错误边界设计
```typescript
// 游戏错误边界
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
}
}