經過前七天從哲學思維到後端架構的完整建構,今天我們要解決一個關鍵問題:如何將後端的聚合邊界和業務邏輯在前端實現系統化的組件設計?
這不只是UI組件的技術問題,更是從領域模型到使用者介面的完整映射工程。每個前端組件都應該對應明確的業務概念,每個互動流程都應該反映領域邏輯的自然演進。
傳統的設計系統往往只關注視覺一致性:
設計系統 = 顏色 + 字體 + 間距 + 組件庫
但基於DDD的設計系統應該體現業務語言到視覺語言的系統化翻譯:
DDD設計系統 = 領域概念 + 使用者意圖 + 互動模式 + 視覺表達
以你在統智科技的電子票券系統為例,我們可以建立這樣的映射關係:
票券聚合 → 前端組件域映射:
// 後端聚合邊界
Ticket聚合 ↔ TicketDomain組件域
├── Ticket實體 ↔ TicketCard組件
├── RedemptionCode值對象 ↔ QRCodeDisplay組件
└── TicketStatus值對象 ↔ StatusIndicator組件
User聚合 ↔ UserDomain組件域
├── User實體 ↔ UserProfile組件
├── Preferences值對象 ↔ SettingsPanel組件
└── AuthToken值對象 ↔ AuthStatus組件
這種映射確保了前端組件與業務邏輯的一致性,每個組件都有明確的職責邊界。
Brad Frost的Atomic Design方法論需要在DDD語境下重新詮釋:
原子(Atoms):領域值對象的UI表達
分子(Molecules):領域實體的屬性組合
有機體(Organisms):領域聚合的完整表達
模板(Templates):領域服務的協調層
頁面(Pages):具體的業務場景實例
graph TB
subgraph "原子層 (Atoms) - 值對象UI"
A1[StatusBadge]
A2[QRCode]
A3[PriceDisplay]
A4[ExpiryDate]
A5[RedemptionButton]
end
subgraph "分子層 (Molecules) - 實體屬性"
M1[TicketInfo]
M2[RedemptionPanel]
M3[PriceInfo]
M4[StatusPanel]
end
subgraph "有機體層 (Organisms) - 聚合功能"
O1[TicketCard]
O2[RedemptionFlow]
O3[TicketList]
O4[UserDashboard]
end
subgraph "模板層 (Templates) - 領域服務"
T1[TicketRedemptionWorkflow]
T2[UserTicketManagement]
T3[StoreIntegration]
end
subgraph "頁面層 (Pages) - 業務場景"
P1[CoffeeRedemptionPage]
P2[TicketHistoryPage]
P3[StoreDashboardPage]
end
A1 --> M1
A2 --> M2
A3 --> M1
A4 --> M1
A5 --> M2
M1 --> O1
M2 --> O2
M3 --> O1
M4 --> O3
O1 --> T2
O2 --> T1
O3 --> T2
T1 --> P1
T2 --> P2
T1 --> P3
具體實現範例:
// 原子層:Status值對象的UI表達
interface StatusBadgeProps {
status: TicketStatus;
size?: 'sm' | 'md' | 'lg';
variant?: 'filled' | 'outlined';
}
const StatusBadge: React.FC<StatusBadgeProps> = ({
status,
size = 'md',
variant = 'filled'
}) => {
const getStatusConfig = (): StatusConfig => {
switch (status) {
case TicketStatus.AVAILABLE:
return { color: 'green', label: '可使用', icon: 'check-circle' };
case TicketStatus.USED:
return { color: 'gray', label: '已使用', icon: 'check' };
case TicketStatus.EXPIRED:
return { color: 'red', label: '已過期', icon: 'x-circle' };
case TicketStatus.PENDING:
return { color: 'yellow', label: '處理中', icon: 'clock' };
}
};
const config = getStatusConfig();
return (
<span className={`status-badge ${config.color} ${size} ${variant}`}>
<Icon name={config.icon} />
{config.label}
</span>
);
};
// 分子層:Ticket實體的屬性組合
interface TicketInfoProps {
ticket: Ticket;
showDetails?: boolean;
}
const TicketInfo: React.FC<TicketInfoProps> = ({ ticket, showDetails = true }) => {
return (
<div className="ticket-info">
<div className="ticket-header">
<h3>{ticket.productName}</h3>
<StatusBadge status={ticket.status} />
</div>
{showDetails && (
<div className="ticket-details">
<PriceDisplay amount={ticket.value} originalPrice={ticket.originalPrice} />
<ExpiryDate date={ticket.expiryDate} />
<div className="store-info">
<span>適用店家:{ticket.applicableStores.join(', ')}</span>
</div>
</div>
)}
</div>
);
};
// 有機體層:Ticket聚合的完整表達
interface TicketCardProps {
ticket: Ticket;
onRedeem?: (ticket: Ticket) => void;
onViewDetails?: (ticket: Ticket) => void;
}
const TicketCard: React.FC<TicketCardProps> = ({
ticket,
onRedeem,
onViewDetails
}) => {
const canRedeem = ticket.isRedeemable();
const isNearExpiry = ticket.isNearExpiry();
return (
<div className={`ticket-card ${isNearExpiry ? 'near-expiry' : ''}`}>
<TicketInfo ticket={ticket} />
{canRedeem && (
<RedemptionPanel
ticket={ticket}
onRedeem={onRedeem}
/>
)}
<div className="ticket-actions">
<button
onClick={() => onViewDetails?.(ticket)}
className="btn-secondary"
>
查看詳情
</button>
{canRedeem && (
<button
onClick={() => onRedeem?.(ticket)}
className="btn-primary"
>
立即使用
</button>
)}
</div>
{isNearExpiry && (
<div className="expiry-warning">
<Icon name="alert-triangle" />
即將過期,請盡快使用
</div>
)}
</div>
);
};
Core層:領域核心概念
Shared層:通用UI組件和服務
Feature層:具體業務功能實現
src/
├── core/ # 核心領域概念
│ ├── domain/
│ │ ├── entities/ # 領域實體
│ │ │ ├── Ticket.ts
│ │ │ ├── User.ts
│ │ │ └── Store.ts
│ │ ├── value-objects/ # 值對象
│ │ │ ├── Money.ts
│ │ │ ├── TicketStatus.ts
│ │ │ └── RedemptionCode.ts
│ │ └── events/ # 領域事件
│ │ ├── TicketRedeemed.ts
│ │ └── TicketExpired.ts
│ ├── infrastructure/
│ │ ├── api/ # API客戶端
│ │ ├── storage/ # 本地存儲
│ │ └── payment/ # 支付集成
│ └── types/ # 型別定義
│
├── shared/ # 共享組件和服務
│ ├── components/
│ │ ├── atoms/ # 原子組件
│ │ │ ├── StatusBadge.tsx
│ │ │ ├── PriceDisplay.tsx
│ │ │ ├── QRCode.tsx
│ │ │ └── ExpiryDate.tsx
│ │ ├── molecules/ # 分子組件
│ │ │ ├── TicketInfo.tsx
│ │ │ ├── RedemptionPanel.tsx
│ │ │ └── StoreLocator.tsx
│ │ └── layout/ # 佈局組件
│ │ ├── Header.tsx
│ │ ├── Navigation.tsx
│ │ └── Footer.tsx
│ ├── hooks/ # 共享Hook
│ │ ├── useLocalStorage.ts
│ │ ├── useApiQuery.ts
│ │ └── useGeolocation.ts
│ ├── services/ # 共享服務
│ │ ├── NotificationService.ts
│ │ ├── AnalyticsService.ts
│ │ └── CacheService.ts
│ └── utils/ # 工具函數
│ ├── formatters.ts
│ ├── validators.ts
│ └── dateHelpers.ts
│
├── features/ # 功能特性模組
│ ├── tickets/ # 票券管理功能
│ │ ├── components/
│ │ │ ├── TicketCard.tsx
│ │ │ ├── TicketList.tsx
│ │ │ └── TicketFilter.tsx
│ │ ├── hooks/
│ │ │ ├── useTickets.ts
│ │ │ └── useTicketRedeem.ts
│ │ ├── services/
│ │ │ └── TicketService.ts
│ │ └── pages/
│ │ ├── TicketsPage.tsx
│ │ └── TicketDetailPage.tsx
│ │
│ ├── redemption/ # 兌換功能
│ │ ├── components/
│ │ │ ├── RedemptionFlow.tsx
│ │ │ ├── QRScanner.tsx
│ │ │ └── RedemptionResult.tsx
│ │ ├── hooks/
│ │ │ ├── useQRScanner.ts
│ │ │ └── useRedemption.ts
│ │ ├── services/
│ │ │ └── RedemptionService.ts
│ │ └── pages/
│ │ └── RedemptionPage.tsx
│ │
│ └── store/ # 店家功能
│ ├── components/
│ │ ├── StoreDashboard.tsx
│ │ ├── RedemptionHistory.tsx
│ │ └── StoreAnalytics.tsx
│ ├── hooks/
│ │ └── useStoreData.ts
│ ├── services/
│ │ └── StoreService.ts
│ └── pages/
│ └── StoreDashboardPage.tsx
Tickets Feature的內部組織:
// features/tickets/domain/TicketAggregate.ts
export class TicketAggregate {
constructor(
private tickets: Ticket[],
private user: User,
private redemptionHistory: RedemptionHistory
) {}
// 聚合內的業務邏輯
validateRedemption(ticketId: string, storeId: string): ValidationResult {
const ticket = this.tickets.find(t => t.id === ticketId);
if (!ticket) {
return ValidationResult.failed("票券不存在");
}
if (!ticket.isRedeemable()) {
return ValidationResult.failed("票券無法使用");
}
if (!ticket.isValidForStore(storeId)) {
return ValidationResult.failed("票券不適用於此店家");
}
return ValidationResult.success();
}
// 聚合狀態變更
redeemTicket(ticketId: string, storeId: string): RedemptionResult {
const validation = this.validateRedemption(ticketId, storeId);
if (!validation.isValid) {
throw new Error(validation.errorMessage);
}
const ticket = this.tickets.find(t => t.id === ticketId);
ticket.markAsUsed();
const redemption = new Redemption(ticketId, storeId, new Date());
this.redemptionHistory.addRedemption(redemption);
return RedemptionResult.success(redemption);
}
}
// features/tickets/hooks/useTicketAggregate.ts
export const useTicketAggregate = (userId: string) => {
const [aggregate, setAggregate] = useState<TicketAggregate | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
// 載入聚合
useEffect(() => {
TicketService.loadUserTickets(userId)
.then(data => {
const aggregate = new TicketAggregate(
data.tickets,
data.user,
data.redemptionHistory
);
setAggregate(aggregate);
})
.catch(setError)
.finally(() => setLoading(false));
}, [userId]);
// 聚合操作方法
const redeemTicket = useCallback(async (ticketId: string, storeId: string) => {
if (!aggregate) return;
try {
// 樂觀更新
const newAggregate = aggregate.clone();
const result = newAggregate.redeemTicket(ticketId, storeId);
setAggregate(newAggregate);
// 同步到伺服器
await TicketService.redeemTicketOnServer(ticketId, storeId);
// 通知使用者
NotificationService.success('票券使用成功!');
return result;
} catch (error) {
// 回滾樂觀更新
setAggregate(aggregate);
NotificationService.error(error.message);
throw error;
}
}, [aggregate]);
return {
aggregate,
loading,
error,
redeemTicket
};
};
// features/tickets/components/TicketList.tsx
export const TicketList: React.FC<Props> = ({ userId }) => {
const {
aggregate,
loading,
error,
redeemTicket
} = useTicketAggregate(userId);
if (loading) return <TicketListSkeleton />;
if (error) return <ErrorDisplay error={error} />;
if (!aggregate?.tickets.length) return <EmptyTicketList />;
return (
<div className="ticket-list">
{aggregate.tickets.map(ticket => (
<TicketCard
key={ticket.id}
ticket={ticket}
onRedeem={(ticket) => redeemTicket(ticket.id, currentStoreId)}
/>
))}
</div>
);
};
設計令牌不應該只是視覺變數,而應該承載業務語義:
// design-tokens/semantic-tokens.ts
export const TicketSemanticTokens = {
// 顏色:反映票券狀態
colors: {
status: {
available: '#10B981', // 可用:綠色
pending: '#F59E0B', // 待處理:橙色
used: '#6B7280', // 已使用:灰色
expired: '#EF4444', // 過期:紅色
nearExpiry: '#DC2626' // 即將過期:深紅
},
value: {
high: '#7C3AED', // 高價值:紫色
medium: '#3B82F6', // 中價值:藍色
low: '#059669' // 低價值:綠色
},
category: {
coffee: '#92400E', // 咖啡:棕色
food: '#DC2626', // 食物:紅色
retail: '#7C3AED', // 零售:紫色
service: '#059669' // 服務:綠色
}
},
// 字體:反映信息層次
typography: {
price: {
major: { // 主要價格顯示
fontSize: '1.5rem',
fontWeight: '700',
fontFamily: '"SF Pro Display", sans-serif'
},
discount: { // 折扣價格
fontSize: '1rem',
fontWeight: '600',
color: '#DC2626'
}
},
status: {
active: { // 活躍狀態
fontSize: '0.875rem',
fontWeight: '600',
textTransform: 'uppercase'
}
}
},
// 間距:反映功能關係
spacing: {
card: {
padding: '1rem', // 卡片內間距
margin: '0.75rem' // 卡片間間距
},
section: {
padding: '1.5rem', // 區段內間距
margin: '2rem' // 區段間間距
}
},
// 陰影:反映層次和狀態
shadows: {
card: {
normal: '0 1px 3px rgba(0, 0, 0, 0.1)',
hover: '0 4px 6px rgba(0, 0, 0, 0.1)',
selected: '0 0 0 2px #3B82F6'
},
nearExpiry: '0 0 8px rgba(220, 38, 38, 0.3)'
}
};
// 業務組件中的使用
const TicketCard: React.FC<TicketCardProps> = ({ ticket }) => {
const getCardStyle = () => {
const tokens = TicketSemanticTokens;
const baseStyle = {
padding: tokens.spacing.card.padding,
margin: tokens.spacing.card.margin,
boxShadow: tokens.shadows.card.normal
};
if (ticket.isNearExpiry()) {
return {
...baseStyle,
boxShadow: tokens.shadows.nearExpiry,
borderColor: tokens.colors.status.nearExpiry
};
}
return baseStyle;
};
return (
<div style={getCardStyle()}>
<TicketInfo ticket={ticket} />
<RedemptionPanel ticket={ticket} />
</div>
);
};
組件測試應該驗證業務邏輯而非實現細節:
// TicketCard.test.tsx
describe('TicketCard Component', () => {
describe('業務邏輯顯示', () => {
it('應該根據票券狀態顯示正確的視覺提示', () => {
const availableTicket = TicketTestFactory.createAvailable();
render(<TicketCard ticket={availableTicket} />);
expect(screen.getByTestId('status-badge')).toHaveTextContent('可使用');
expect(screen.getByTestId('status-badge')).toHaveClass('status-available');
expect(screen.getByTestId('redeem-button')).toBeEnabled();
});
it('應該為即將過期的票券顯示警告', () => {
const nearExpiryTicket = TicketTestFactory.createNearExpiry();
render(<TicketCard ticket={nearExpiryTicket} />);
expect(screen.getByTestId('expiry-warning')).toBeInTheDocument();
expect(screen.getByTestId('ticket-card')).toHaveClass('near-expiry');
});
it('已使用的票券不應該顯示兌換按鈕', () => {
const usedTicket = TicketTestFactory.createUsed();
render(<TicketCard ticket={usedTicket} />);
expect(screen.queryByTestId('redeem-button')).not.toBeInTheDocument();
expect(screen.getByTestId('status-badge')).toHaveTextContent('已使用');
});
});
describe('使用者互動', () => {
it('點擊兌換按鈕應該觸發兌換流程', async () => {
const ticket = TicketTestFactory.createAvailable();
const onRedeem = jest.fn();
render(<TicketCard ticket={ticket} onRedeem={onRedeem} />);
fireEvent.click(screen.getByTestId('redeem-button'));
expect(onRedeem).toHaveBeenCalledWith(ticket);
});
});
});
// TicketList.test.tsx - 聚合層級的測試
describe('TicketList Component', () => {
describe('聚合狀態顯示', () => {
it('應該顯示使用者的所有可用票券', () => {
const tickets = [
TicketTestFactory.createAvailable({ productName: '經典美式咖啡' }),
TicketTestFactory.createAvailable({ productName: '拿鐵咖啡' })
];
render(<TicketList userId="user1" tickets={tickets} />);
expect(screen.getByText('經典美式咖啡')).toBeInTheDocument();
expect(screen.getByText('拿鐵咖啡')).toBeInTheDocument();
expect(screen.getAllByTestId('redeem-button')).toHaveLength(2);
});
});
describe('聚合操作', () => {
it('應該正確處理票券兌換流程', async () => {
const mockRedemption = jest.fn().mockResolvedValue({ success: true });
const tickets = [TicketTestFactory.createAvailable()];
render(
<TicketList
userId="user1"
tickets={tickets}
onTicketRedeem={mockRedemption}
/>
);
fireEvent.click(screen.getByTestId('redeem-button'));
await waitFor(() => {
expect(mockRedemption).toHaveBeenCalledWith(
expect.objectContaining({
id: tickets[0].id
})
);
});
});
});
});
不同設備上的功能應該基於業務重要性進行分層:
// responsive/BusinessPriority.ts
export const TicketSystemPriorities = {
mobile: {
primary: [
'ticket-redemption', // 票券兌換(核心功能)
'qr-code-display', // QR碼顯示
'ticket-status' // 票券狀態
],
secondary: [
'ticket-list', // 票券列表
'expiry-notifications', // 過期提醒
'store-locator' // 店家定位
],
hidden: [
'detailed-analytics', // 詳細分析
'bulk-operations', // 批量操作
'admin-functions' // 管理功能
]
},
tablet: {
primary: [
'ticket-overview', // 票券總覽
'redemption-history', // 兌換歷史
'store-integration' // 店家整合
],
secondary: [
'analytics-dashboard', // 分析面板
'user-management' // 用戶管理
]
},
desktop: {
primary: [
'full-dashboard', // 完整儀表板
'advanced-analytics', // 進階分析
'system-administration' // 系統管理
]
}
};
// 響應式組件實現
const ResponsiveTicketCard: React.FC<Props> = ({ ticket, device }) => {
const getPriorityLevel = () => {
return TicketSystemPriorities[device] || TicketSystemPriorities.mobile;
};
const shouldShowFeature = (feature: string) => {
const priorities = getPriorityLevel();
return priorities.primary.includes(feature) ||
priorities.secondary.includes(feature);
};
return (
<div className={`ticket-card ${device}`}>
<TicketInfo ticket={ticket} />
{/* 核心功能:所有設備都顯示 */}
<RedemptionPanel ticket={ticket} />
{/* 條件功能:根據設備優先級顯示 */}
{shouldShowFeature('detailed-info') && (
<TicketDetails ticket={ticket} />
)}
{shouldShowFeature('analytics') && (
<TicketAnalytics ticket={ticket} />
)}
</div>
);
};
// performance/LazyLoading.ts
export const TicketSystemLazyComponents = {
// 立即載入:核心業務功能
immediate: [
'TicketCard',
'StatusBadge',
'RedemptionButton'
],
// 延遲載入:次要功能
deferred: [
'TicketAnalytics',
'RedemptionHistory',
'StoreLocator'
],
// 按需載入:管理功能
onDemand: [
'AdminDashboard',
'BulkOperations',
'SystemSettings'
]
};
// 智能預載入策略
const useSmartPreloading = (userBehavior: UserBehavior) => {
useEffect(() => {
// 基於用戶行為預測需要的組件
if (userBehavior.frequentlyUsesAnalytics) {
import('./TicketAnalytics').then(component => {
// 預載入分析組件
});
}
if (userBehavior.isStoreManager) {
import('./StoreDashboard').then(component => {
// 預載入店家面板
});
}
}, [userBehavior]);
};
Day 7我們規劃了RESTful + GraphQL + WebSocket混合策略,現在針對三個核心案例分別實現最適合的API設計:
選型理由:毫秒級延遲要求、高併發、強一致性、實時數據流
// api/contracts/TradingContracts.ts
export interface TradingSystemAPI {
// WebSocket主導:實時市場數據和交易執行
websocket: {
// 市場數據流(最高優先級)
marketData: {
subscribe: { symbols: string[]; depth?: number };
events: {
'price:update': { symbol: string; price: number; volume: number; timestamp: number };
'orderbook:change': { symbol: string; bids: Order[]; asks: Order[] };
'trade:executed': { symbol: string; price: number; quantity: number };
};
};
// 交易操作(超低延遲)
trading: {
events: {
'order:submitted': { orderId: string; status: 'pending' | 'rejected'; reason?: string };
'order:filled': { orderId: string; fillPrice: number; fillQuantity: number };
'portfolio:updated': { portfolioId: string; totalValue: number; positions: Position[] };
};
};
};
// REST輔助:配置和歷史查詢
rest: {
'GET /api/portfolios/{id}': {
params: { id: string };
response: { portfolio: Portfolio; holdings: Holding[] };
cache: 'max-age=30'; // 30秒快取
};
'POST /api/orders': {
body: { portfolioId: string; symbol: string; quantity: number; orderType: 'MARKET' | 'LIMIT' };
response: { orderId: string; estimatedFillPrice?: number };
timeout: 500; // 500ms超時
};
'GET /api/analytics/risk/{portfolioId}': {
params: { portfolioId: string };
response: { var: number; beta: number; sharpeRatio: number };
cache: 'max-age=300'; // 5分鐘快取
};
};
// GraphQL不適用:延遲過高,不利於高頻交易
}
// 高性能API客戶端實現
export class TradingAPIClient {
private wsConnection: WebSocket;
private messageQueue: PriorityQueue<WSMessage>;
private reconnectStrategy: ExponentialBackoff;
constructor() {
// 建立多個WebSocket連線以降低延遲
this.wsConnection = new WebSocket(WS_ENDPOINT, {
// 關鍵配置:最小化延遲
perMessageDeflate: false, // 關閉壓縮以降低CPU開銷
maxPayload: 1024 * 16, // 限制payload大小
pingTimeout: 5000, // 快速心跳檢測
});
}
// 優先級訊息處理:交易>價格>其他
private processMessage(message: WSMessage): void {
switch (message.type) {
case 'order:filled':
this.handleOrderFill(message.data);
break;
case 'price:update':
this.updateMarketData(message.data);
break;
default:
this.handleGenericMessage(message);
}
}
// 批次訂單提交:減少網路往返
async submitOrderBatch(orders: OrderRequest[]): Promise<OrderResponse[]> {
const batchId = generateBatchId();
const promises = orders.map(order =>
this.submitSingleOrder({ ...order, batchId })
);
return Promise.all(promises);
}
}
選型理由:成本敏感、開發簡單、維護容易、協作功能
// api/contracts/FamilyFinanceContracts.ts
export interface FamilyFinanceAPI {
// REST主導:簡單CRUD,易於理解和維護
rest: {
// 家庭管理
'GET /api/families/{id}': {
params: { id: string };
response: { family: Family; members: FamilyMember[]; permissions: Permission[] };
cache: 'max-age=3600'; // 1小時快取
};
'POST /api/families/{id}/members': {
params: { id: string };
body: { email: string; role: 'ADMIN' | 'MEMBER' | 'CHILD' };
response: { member: FamilyMember; inviteToken: string };
};
// 支出記錄
'POST /api/expenses': {
body: {
familyId: string;
amount: number;
category: string;
description: string;
receiptImage?: string;
};
response: { expense: Expense; budgetRemaining: number };
};
'GET /api/expenses': {
params: {
familyId: string;
startDate?: string;
endDate?: string;
category?: string;
memberId?: string;
};
response: { expenses: Expense[]; summary: ExpenseSummary; pagination: Pagination };
cache: 'max-age=300'; // 5分鐘快取
};
// 預算管理
'PUT /api/budgets/{id}': {
params: { id: string };
body: { categoryLimits: CategoryLimit[]; alertThresholds: number[] };
response: { budget: Budget; effectiveDate: string };
};
};
// 基本WebSocket:協作通知
websocket: {
events: {
'expense:added': { expense: Expense; addedBy: FamilyMember };
'budget:exceeded': { category: string; currentAmount: number; limit: number };
'member:joined': { member: FamilyMember; invitedBy: FamilyMember };
};
};
// GraphQL不使用:增加複雜度,不符合簡化原則
}
// 簡化的API客戶端
export class FamilyFinanceAPIClient {
constructor(private baseURL: string, private authToken: string) {}
// 通用REST方法,減少程式碼重複
private async request<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
data?: any
): Promise<T> {
const response = await fetch(`${this.baseURL}${endpoint}`, {
method,
headers: {
'Authorization': `Bearer ${this.authToken}`,
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
});
if (!response.ok) {
throw new APIError(response.status, await response.text());
}
return response.json();
}
// 業務方法:語義清晰、易於理解
async addExpense(expense: ExpenseInput): Promise<ExpenseResult> {
return this.request('POST', '/api/expenses', expense);
}
async getFamilyExpenses(familyId: string, filters?: ExpenseFilters): Promise<ExpenseList> {
const params = new URLSearchParams({ familyId, ...filters });
return this.request('GET', `/api/expenses?${params}`);
}
async updateBudget(budgetId: string, budget: BudgetUpdate): Promise<Budget> {
return this.request('PUT', `/api/budgets/${budgetId}`, budget);
}
// 簡單的WebSocket連線管理
connectToFamilyUpdates(familyId: string, callback: (event: FamilyEvent) => void): void {
const ws = new WebSocket(`${WS_ENDPOINT}/families/${familyId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
callback(data);
};
}
}
選型理由:IoT數據流、長期存儲、異常檢測、時序分析
// api/contracts/HealthMonitorContracts.ts
export interface HealthMonitorAPI {
// REST: 配置和查詢
rest: {
// 使用者健康檔案
'GET /api/health-profiles/{userId}': {
params: { userId: string };
response: {
profile: HealthProfile;
devices: Device[];
latestMetrics: HealthMetric[]
};
cache: 'max-age=1800'; // 30分鐘快取
};
// 設備管理
'POST /api/devices': {
body: {
userId: string;
deviceType: 'HEART_RATE' | 'BLOOD_PRESSURE' | 'WEIGHT' | 'GLUCOSE';
deviceId: string;
calibrationData?: CalibrationData;
};
response: { device: Device; connectionToken: string };
};
// 歷史數據查詢
'GET /api/health-data/{userId}': {
params: {
userId: string;
startDate: string;
endDate: string;
metrics?: string[];
aggregation?: 'HOURLY' | 'DAILY' | 'WEEKLY';
};
response: {
data: TimeSeriesData[];
trends: TrendAnalysis;
anomalies: Anomaly[]
};
cache: 'max-age=7200'; // 2小時快取(歷史數據變化較少)
};
};
// GraphQL: 複雜關聯查詢和個性化分析
graphql: {
queries: {
healthDashboard: {
input: { userId: string; timeRange: TimeRange; preferences: DashboardPrefs };
output: {
user: {
profile: HealthProfile;
goals: HealthGoal[];
alerts: Alert[];
};
metrics: Array<{
type: MetricType;
current: number;
trend: 'IMPROVING' | 'STABLE' | 'DECLINING';
history: TimeSeriesPoint[];
benchmarks: Benchmark[];
}>;
insights: {
correlations: Correlation[];
recommendations: Recommendation[];
riskFactors: RiskFactor[];
};
};
};
};
subscriptions: {
healthAlerts: {
input: { userId: string };
output: {
alertType: 'CRITICAL' | 'WARNING' | 'INFO';
metric: MetricType;
value: number;
threshold: number;
timestamp: Date;
};
};
};
};
// WebSocket: IoT數據流和即時告警
websocket: {
// 設備數據流
deviceData: {
events: {
'device:reading': {
deviceId: string;
userId: string;
readings: DeviceReading[];
timestamp: number
};
'device:offline': { deviceId: string; lastSeen: number };
'device:battery_low': { deviceId: string; batteryLevel: number };
};
};
// 健康告警
healthAlerts: {
events: {
'health:anomaly_detected': {
userId: string;
metric: MetricType;
severity: AlertSeverity;
recommendations: string[];
};
'health:goal_achieved': { userId: string; goal: HealthGoal };
'health:trend_change': { userId: string; metric: MetricType; newTrend: TrendDirection };
};
};
};
}
// 混合式API客戶端:針對不同數據類型優化
export class HealthMonitorAPIClient {
constructor(
private restClient: RestClient,
private graphqlClient: GraphQLClient,
private wsClient: WebSocketManager
) {}
// REST: 簡單配置和歷史查詢
async getHealthProfile(userId: string): Promise<HealthProfile> {
return this.restClient.get(`/api/health-profiles/${userId}`);
}
async getHistoricalData(
userId: string,
timeRange: TimeRange,
options?: QueryOptions
): Promise<HealthHistoryResponse> {
const params = {
startDate: timeRange.start.toISOString(),
endDate: timeRange.end.toISOString(),
...options
};
return this.restClient.get(`/api/health-data/${userId}`, { params });
}
// GraphQL: 複雜關聯查詢
async getDashboardData(
userId: string,
preferences: DashboardPrefs
): Promise<HealthDashboard> {
const query = `
query HealthDashboard($userId: ID!, $timeRange: TimeRangeInput!, $preferences: DashboardPrefsInput!) {
healthDashboard(userId: $userId, timeRange: $timeRange, preferences: $preferences) {
user {
profile { id name age gender }
goals { type target current deadline }
alerts { type severity message timestamp }
}
metrics {
type
current
trend
history(limit: $preferences.historyPoints) {
timestamp
value
}
benchmarks {
label
value
isHealthy
}
}
insights {
correlations {
metrics
strength
description
}
recommendations {
category
priority
action
evidence
}
}
}
}
`;
return this.graphqlClient.query(query, {
userId,
timeRange: preferences.defaultTimeRange,
preferences
});
}
// WebSocket: 即時數據流處理
subscribeToDeviceReadings(
userId: string,
callback: (reading: DeviceReading) => void
): UnsubscribeFunction {
return this.wsClient.subscribe('device:reading',
{ filter: { userId } },
callback
);
}
subscribeToHealthAlerts(
userId: string,
callback: (alert: HealthAlert) => void
): UnsubscribeFunction {
return this.wsClient.subscribe('health:anomaly_detected',
{ filter: { userId } },
callback
);
}
// 智能數據同步:根據數據類型選擇最佳策略
async syncDeviceData(deviceId: string): Promise<SyncResult> {
// 1. 透過WebSocket接收即時數據
const realtimeData = await this.getRealtimeReadings(deviceId);
// 2. 透過REST獲取歷史數據補齊
const historicalData = await this.getHistoricalReadings(deviceId);
// 3. 合併並去重
const mergedData = this.mergeAndDeduplicateReadings(realtimeData, historicalData);
return { success: true, syncedReadings: mergedData.length };
}
}
責任邊界的具體劃分:
// 後端責任 (Backend Concerns)
interface BackendResponsibilities {
businessLogicValidation: {
// 票券使用資格驗證
validateTicketEligibility(ticketId: string, storeId: string): ValidationResult;
// 防重複兌換檢查
checkDuplicateRedemption(ticketId: string): boolean;
// 庫存與限額控制
enforceRedemptionLimits(storeId: string): LimitResult;
};
dataConsistency: {
// 跨系統資料同步
syncWithPOS(redemption: Redemption): Promise<void>;
// 交易記錄完整性
maintainAuditTrail(operation: Operation): Promise<void>;
// 並發安全控制
handleConcurrentRedemptions(ticketId: string): Promise<LockResult>;
};
integrationOrchestration: {
// 支付系統整合
processPaymentRefund(redemption: Redemption): Promise<PaymentResult>;
// 第三方API協調
notifyPartnerSystems(event: SystemEvent): Promise<void>;
// 合規性處理
handleComplianceRequirements(transaction: Transaction): Promise<void>;
};
}
// 前端責任 (Frontend Concerns)
interface FrontendResponsibilities {
userExperienceLogic: {
// 使用者互動流程
guideRedemptionProcess(ticket: Ticket): InteractionFlow;
// 視覺狀態反饋
provideFeedback(operation: Operation, result: Result): UIFeedback;
// 錯誤恢復指導
handleErrorRecovery(error: APIError): RecoveryAction;
};
clientSideOptimization: {
// 樂觀更新策略
optimisticallyUpdateUI(action: UserAction): void;
// 本地快取管理
manageCacheInvalidation(dataChange: DataChange): void;
// 離線行為處理
handleOfflineScenarios(operation: Operation): OfflineStrategy;
};
presentationLogic: {
// 數據格式化展示
formatDisplayData(rawData: APIResponse): DisplayData;
// 條件性UI渲染
renderConditionalElements(userContext: UserContext): ReactElement;
// 響應式佈局適配
adaptToDeviceConstraints(device: DeviceInfo): LayoutConfig;
};
}
// 共享責任 (Shared Concerns) - 需要前後端協調
interface SharedResponsibilities {
securityAndAuth: {
// 前端:Token管理和刷新
// 後端:Token驗證和權限檢查
authTokenManagement: TokenStrategy;
};
errorHandling: {
// 前端:使用者友善錯誤訊息
// 後端:結構化錯誤回應
errorResponseStrategy: ErrorHandlingStrategy;
};
performanceOptimization: {
// 前端:請求合併和快取
// 後端:批次處理和分頁
requestOptimization: PerformanceStrategy;
};
}
為了迎接Day 9的高併發挑戰,我們需要在前端架構中預先考慮:
併發安全的狀態管理:
// stores/ConcurrentSafeTicketStore.ts
export class ConcurrentSafeTicketStore {
private _tickets = new Map<string, Ticket>();
private _pendingOperations = new Map<string, Promise<any>>();
private _optimisticUpdates = new Map<string, OptimisticUpdate>();
// 防止重複請求的併發控制
async redeemTicket(ticketId: string, storeId: string): Promise<RedemptionResult> {
const operationKey = `redeem:${ticketId}:${storeId}`;
// 檢查是否已有相同操作在進行中
if (this._pendingOperations.has(operationKey)) {
return this._pendingOperations.get(operationKey);
}
// 建立新的操作Promise
const operation = this._executeRedemption(ticketId, storeId);
this._pendingOperations.set(operationKey, operation);
try {
const result = await operation;
return result;
} finally {
this._pendingOperations.delete(operationKey);
}
}
private async _executeRedemption(ticketId: string, storeId: string): Promise<RedemptionResult> {
// 1. 樂觀更新本地狀態
const originalTicket = this._tickets.get(ticketId);
const optimisticUpdate = {
ticketId,
originalState: originalTicket?.clone(),
newState: originalTicket?.clone().markAsRedeeming(),
timestamp: Date.now()
};
this._optimisticUpdates.set(ticketId, optimisticUpdate);
this._tickets.set(ticketId, optimisticUpdate.newState);
try {
// 2. 發送API請求
const result = await TicketAPI.redeemTicket(ticketId, storeId);
// 3. 更新為伺服器確認狀態
const confirmedTicket = originalTicket?.clone().markAsRedeemed(result);
this._tickets.set(ticketId, confirmedTicket);
this._optimisticUpdates.delete(ticketId);
return result;
} catch (error) {
// 4. 錯誤回滾
if (optimisticUpdate.originalState) {
this._tickets.set(ticketId, optimisticUpdate.originalState);
}
this._optimisticUpdates.delete(ticketId);
throw error;
}
}
// 處理網路重連後的狀態同步
async reconcileAfterReconnection(): Promise<void> {
const pendingUpdates = Array.from(this._optimisticUpdates.values());
for (const update of pendingUpdates) {
try {
// 向伺服器確認樂觀更新的實際狀態
const serverState = await TicketAPI.getTicketState(update.ticketId);
this._tickets.set(update.ticketId, serverState);
this._optimisticUpdates.delete(update.ticketId);
} catch (error) {
// 伺服器狀態不可用,保持樂觀更新或回滾
console.warn(`Failed to reconcile ticket ${update.ticketId}:`, error);
}
}
}
}
組件層的負載均衡策略:
// components/LoadBalancedTicketList.tsx
export const LoadBalancedTicketList: React.FC<Props> = ({ userId }) => {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [loadingStrategy, setLoadingStrategy] = useState<LoadingStrategy>('progressive');
// 漸進式載入策略:避免初始負載過重
useEffect(() => {
const loadTicketsProgressively = async () => {
// 1. 先載入關鍵票券(即將過期、高價值)
const criticalTickets = await TicketAPI.getCriticalTickets(userId);
setTickets(criticalTickets);
// 2. 後台載入其他票券
setTimeout(async () => {
const remainingTickets = await TicketAPI.getRemainingTickets(userId);
setTickets(prev => [...prev, ...remainingTickets]);
}, 100);
};
loadTicketsProgressively();
}, [userId]);
// 虛擬滾動:處理大量票券的渲染性能
const virtualizedRender = useVirtualization({
items: tickets,
itemHeight: 120,
overscan: 5, // 預渲染5個項目
renderItem: ({ item, index }) => (
<TicketCard
key={item.id}
ticket={item}
lazy={index > 10} // 超過10個項目後啟用懶載入
/>
)
});
return (
<div className="ticket-list-container">
{virtualizedRender}
</div>
);
};
// 智能快取策略:為高併發做準備
const useSmartCaching = (cacheKey: string) => {
const [cache] = useState(() => new Map<string, CacheEntry>());
const getCachedData = useCallback(<T,>(key: string): T | null => {
const entry = cache.get(key);
if (!entry) return null;
// 檢查快取是否過期
if (Date.now() - entry.timestamp > entry.ttl) {
cache.delete(key);
return null;
}
return entry.data as T;
}, [cache]);
const setCachedData = useCallback(<T,>(key: string, data: T, ttl: number = 300000) => {
cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}, [cache]);
return { getCachedData, setCachedData };
};
前端併發處理的核心策略:
// 請求去重和合併
export class RequestDeduplicator {
private pendingRequests = new Map<string, Promise<any>>();
async dedupe<T>(key: string, requestFn: () => Promise<T>): Promise<T> {
if (this.pendingRequests.has(key)) {
return this.pendingRequests.get(key) as Promise<T>;
}
const promise = requestFn();
this.pendingRequests.set(key, promise);
try {
const result = await promise;
return result;
} finally {
this.pendingRequests.delete(key);
}
}
}
// 批次請求優化
export class BatchRequestManager {
private batchQueue = new Map<string, BatchRequest>();
private batchTimeout = 50; // 50ms內的請求合併為一批
async batchRequest<T>(
batchKey: string,
request: BatchableRequest<T>
): Promise<T> {
return new Promise((resolve, reject) => {
let batch = this.batchQueue.get(batchKey);
if (!batch) {
batch = {
requests: [],
timer: setTimeout(() => this.executeBatch(batchKey), this.batchTimeout)
};
this.batchQueue.set(batchKey, batch);
}
batch.requests.push({ request, resolve, reject });
});
}
private async executeBatch(batchKey: string): Promise<void> {
const batch = this.batchQueue.get(batchKey);
if (!batch) return;
this.batchQueue.delete(batchKey);
clearTimeout(batch.timer);
try {
// 合併多個請求為單一批次請求
const batchedRequests = batch.requests.map(r => r.request);
const results = await API.executeBatch(batchKey, batchedRequests);
// 分發結果到各個Promise
batch.requests.forEach((r, index) => {
r.resolve(results[index]);
});
} catch (error) {
// 批次失敗時,讓所有請求都失敗
batch.requests.forEach(r => r.reject(error));
}
}
}
基於今天建立的前端架構,明天我們將深入探討:
記住:我們今天建立的不是UI框架,而是業務邏輯在視覺層的系統化表達。每個組件都承載著特定的業務語義,每個設計決策都應該服務於使用者的業務目標。
「組件不是UI的積木,而是業務概念的視覺化載體。我們設計的不是介面,而是使用者與業務領域對話的媒介。真正的設計系統,是讓複雜的業務邏輯變得直觀可感的翻譯藝術。」