24 KiB
24 KiB
前端技术架构详设文档
文档版本: 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
}
}