在 Day 16 我們實作了即時協作系統,今天我們將進行重要的架構升級:將 WebSocket 協作引擎獨立為專門的微服務。這個架構調整將帶來更好的可擴展性、故障隔離和維護性。我們將實作前端的微服務通信架構、WebSocket 連線管理最佳化、以及跨服務的資料同步機制。
// src/types/microservices.ts
export interface MicroserviceArchitecture {
// 核心業務服務
otpService: {
endpoint: string;
responsibilities: ['authentication', 'user-management', 'basic-crud'];
};
// 協作引擎服務 (新增)
collaborationService: {
endpoint: string;
websocketUrl: string;
responsibilities: ['real-time-collaboration', 'session-management', 'conflict-resolution'];
};
// 分析報表服務 (預計 Day 18)
analyticsService: {
endpoint: string;
responsibilities: ['data-analysis', 'reporting', 'metrics'];
};
// 通知服務
notificationService: {
endpoint: string;
websocketUrl: string;
responsibilities: ['push-notifications', 'email', 'sms'];
};
}
export interface ServiceConfig {
baseUrl: string;
timeout: number;
retryPolicy: RetryPolicy;
circuitBreaker: CircuitBreakerConfig;
authentication: AuthConfig;
}
export interface RetryPolicy {
maxRetries: number;
backoffMultiplier: number;
initialDelay: number;
maxDelay: number;
retryableErrors: string[];
}
export interface CircuitBreakerConfig {
threshold: number;
timeout: number;
monitor: boolean;
}
// src/services/microservices/ApiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { CircuitBreaker } from './CircuitBreaker';
import { RetryManager } from './RetryManager';
import { AuthTokenManager } from './AuthTokenManager';
export class ApiClient {
private client: AxiosInstance;
private circuitBreaker: CircuitBreaker;
private retryManager: RetryManager;
private authManager: AuthTokenManager;
private requestInterceptors: Map<string, number> = new Map();
private responseInterceptors: Map<string, number> = new Map();
constructor(
private serviceName: string,
private config: ServiceConfig
) {
this.authManager = new AuthTokenManager();
this.circuitBreaker = new CircuitBreaker(config.circuitBreaker);
this.retryManager = new RetryManager(config.retryPolicy);
this.client = axios.create({
baseURL: config.baseUrl,
timeout: config.timeout,
headers: {
'Content-Type': 'application/json',
'X-Service-Client': serviceName,
'X-Client-Version': process.env.REACT_APP_VERSION || '1.0.0'
}
});
this.setupInterceptors();
this.setupErrorHandling();
}
/**
* 設置請求/回應攔截器
*/
private setupInterceptors(): void {
// 請求攔截器 - 認證和追蹤
const requestInterceptor = this.client.interceptors.request.use(
async (config) => {
// 添加認證 token
const token = await this.authManager.getValidToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// 添加請求 ID 追蹤
const requestId = this.generateRequestId();
config.headers['X-Request-ID'] = requestId;
config.metadata = { requestId, startTime: Date.now() };
// 記錄請求
this.logRequest(config);
return config;
},
(error) => {
this.logError('Request interceptor error:', error);
return Promise.reject(error);
}
);
this.requestInterceptors.set('auth', requestInterceptor);
// 回應攔截器 - 錯誤處理和重試
const responseInterceptor = this.client.interceptors.response.use(
(response) => {
// 記錄成功回應
this.logResponse(response);
this.circuitBreaker.recordSuccess();
return response;
},
async (error) => {
// 記錄錯誤
this.logError('Response error:', error);
// 檢查是否需要重試
if (this.shouldRetry(error)) {
return this.retryManager.retry(() =>
this.client.request(error.config)
);
}
// 處理認證錯誤
if (error.response?.status === 401) {
await this.handleAuthError();
}
// 記錄熔斷器失敗
this.circuitBreaker.recordFailure();
return Promise.reject(this.enhanceError(error));
}
);
this.responseInterceptors.set('retry', responseInterceptor);
}
/**
* GET 請求
*/
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.executeWithCircuitBreaker(async () => {
const response = await this.client.get(url, config);
return response.data;
});
}
/**
* POST 請求
*/
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.executeWithCircuitBreaker(async () => {
const response = await this.client.post(url, data, config);
return response.data;
});
}
/**
* PUT 請求
*/
async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.executeWithCircuitBreaker(async () => {
const response = await this.client.put(url, data, config);
return response.data;
});
}
/**
* DELETE 請求
*/
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.executeWithCircuitBreaker(async () => {
const response = await this.client.delete(url, config);
return response.data;
});
}
/**
* 熔斷器執行包裝
*/
private async executeWithCircuitBreaker<T>(operation: () => Promise<T>): Promise<T> {
if (this.circuitBreaker.isOpen()) {
throw new ServiceUnavailableError(
`Service ${this.serviceName} is currently unavailable (circuit breaker open)`
);
}
try {
return await operation();
} catch (error) {
this.circuitBreaker.recordFailure();
throw error;
}
}
/**
* 檢查是否應該重試
*/
private shouldRetry(error: any): boolean {
// 網路錯誤
if (!error.response) return true;
// 5xx 服務器錯誤
if (error.response.status >= 500) return true;
// 429 限流錯誤
if (error.response.status === 429) return true;
// 特定的可重試錯誤
const retryableErrors = this.config.retryPolicy.retryableErrors;
if (retryableErrors.includes(error.code)) return true;
return false;
}
/**
* 處理認證錯誤
*/
private async handleAuthError(): Promise<void> {
try {
await this.authManager.refreshToken();
} catch (refreshError) {
// 刷新失敗,清除認證並重定向登入
this.authManager.clearAuth();
window.location.href = '/login';
}
}
/**
* 增強錯誤資訊
*/
private enhanceError(error: any): ServiceError {
return new ServiceError(
error.message || 'Unknown service error',
{
serviceName: this.serviceName,
status: error.response?.status,
code: error.code,
requestId: error.config?.headers?.['X-Request-ID'],
timestamp: new Date().toISOString(),
originalError: error
}
);
}
/**
* 記錄請求
*/
private logRequest(config: AxiosRequestConfig): void {
if (process.env.NODE_ENV === 'development') {
console.log(`🚀 [${this.serviceName}] ${config.method?.toUpperCase()} ${config.url}`, {
requestId: config.headers?.['X-Request-ID'],
data: config.data
});
}
}
/**
* 記錄回應
*/
private logResponse(response: any): void {
if (process.env.NODE_ENV === 'development') {
const duration = Date.now() - (response.config.metadata?.startTime || 0);
console.log(`✅ [${this.serviceName}] ${response.status} ${response.config.url} (${duration}ms)`, {
requestId: response.config.headers?.['X-Request-ID']
});
}
}
/**
* 記錄錯誤
*/
private logError(message: string, error: any): void {
console.error(`❌ [${this.serviceName}] ${message}`, {
error: error.message,
status: error.response?.status,
requestId: error.config?.headers?.['X-Request-ID']
});
}
/**
* 生成請求 ID
*/
private generateRequestId(): string {
return `${this.serviceName}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 清理資源
*/
destroy(): void {
this.requestInterceptors.forEach(id => {
this.client.interceptors.request.eject(id);
});
this.responseInterceptors.forEach(id => {
this.client.interceptors.response.eject(id);
});
this.circuitBreaker.destroy();
this.retryManager.destroy();
}
}
// 自定義錯誤類別
export class ServiceError extends Error {
constructor(message: string, public metadata: any) {
super(message);
this.name = 'ServiceError';
}
}
export class ServiceUnavailableError extends Error {
constructor(message: string) {
super(message);
this.name = 'ServiceUnavailableError';
}
}
// src/services/microservices/CircuitBreaker.ts
export class CircuitBreaker {
private state: 'CLOSED' | 'OPEN' | 'HALF_OPEN' = 'CLOSED';
private failureCount = 0;
private lastFailureTime?: Date;
private successCount = 0;
constructor(private config: CircuitBreakerConfig) {}
/**
* 檢查熔斷器是否開啟
*/
isOpen(): boolean {
if (this.state === 'OPEN') {
// 檢查是否應該進入半開狀態
if (this.shouldAttemptReset()) {
this.state = 'HALF_OPEN';
this.successCount = 0;
return false;
}
return true;
}
return false;
}
/**
* 記錄成功
*/
recordSuccess(): void {
this.failureCount = 0;
if (this.state === 'HALF_OPEN') {
this.successCount++;
// 半開狀態下連續成功,重置為關閉狀態
if (this.successCount >= this.config.threshold) {
this.state = 'CLOSED';
}
}
}
/**
* 記錄失敗
*/
recordFailure(): void {
this.failureCount++;
this.lastFailureTime = new Date();
if (this.failureCount >= this.config.threshold) {
this.state = 'OPEN';
}
}
/**
* 檢查是否應該嘗試重置
*/
private shouldAttemptReset(): boolean {
if (!this.lastFailureTime) return false;
const timeSinceLastFailure = Date.now() - this.lastFailureTime.getTime();
return timeSinceLastFailure >= this.config.timeout;
}
/**
* 獲取當前狀態
*/
getState(): { state: string; failureCount: number; lastFailureTime?: Date } {
return {
state: this.state,
failureCount: this.failureCount,
lastFailureTime: this.lastFailureTime
};
}
/**
* 清理資源
*/
destroy(): void {
// 清理定時器等資源
}
}
// src/services/microservices/CollaborationServiceClient.ts
import { io, Socket } from 'socket.io-client';
import { ApiClient } from './ApiClient';
import { EventEmitter } from 'events';
export class CollaborationServiceClient extends EventEmitter {
private apiClient: ApiClient;
private socket: Socket | null = null;
private connectionState: ConnectionState = 'disconnected';
private reconnectionAttempts = 0;
private maxReconnectionAttempts = 5;
private heartbeatInterval: NodeJS.Timeout | null = null;
private sessionId: string | null = null;
private messageQueue: QueuedMessage[] = [];
constructor(private config: CollaborationServiceConfig) {
super();
this.apiClient = new ApiClient('collaboration-service', {
baseUrl: config.apiEndpoint,
timeout: 10000,
retryPolicy: {
maxRetries: 3,
backoffMultiplier: 2,
initialDelay: 1000,
maxDelay: 10000,
retryableErrors: ['ECONNRESET', 'ETIMEDOUT']
},
circuitBreaker: {
threshold: 5,
timeout: 30000,
monitor: true
},
authentication: config.auth
});
}
/**
* 連接到協作服務
*/
async connect(userInfo: UserInfo): Promise<void> {
try {
this.connectionState = 'connecting';
this.emit('connection_state_change', 'connecting');
// 首先透過 REST API 驗證和獲取 session token
const sessionToken = await this.apiClient.post<string>('/sessions/create', {
userInfo,
capabilities: ['real-time-collaboration', 'cursor-tracking', 'selection-sync']
});
// 建立 WebSocket 連接
this.socket = io(this.config.websocketUrl, {
auth: {
token: sessionToken,
userId: userInfo.userId,
tenantId: userInfo.tenantId
},
transports: ['websocket'],
upgrade: false,
rememberUpgrade: false,
timeout: 20000,
forceNew: true
});
this.setupSocketEventHandlers();
// 等待連接建立
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('WebSocket connection timeout'));
}, 20000);
this.socket!.once('connect', () => {
clearTimeout(timeout);
resolve();
});
this.socket!.once('connect_error', (error) => {
clearTimeout(timeout);
reject(error);
});
});
this.connectionState = 'connected';
this.reconnectionAttempts = 0;
this.emit('connection_state_change', 'connected');
// 開始心跳檢測
this.startHeartbeat();
// 處理等待中的訊息
await this.flushMessageQueue();
} catch (error) {
this.connectionState = 'disconnected';
this.emit('connection_state_change', 'disconnected');
this.emit('connection_error', error);
throw error;
}
}
/**
* 設置 Socket 事件處理器
*/
private setupSocketEventHandlers(): void {
if (!this.socket) return;
// 連接事件
this.socket.on('connect', () => {
console.log('🔗 Connected to collaboration service');
this.connectionState = 'connected';
this.emit('connection_state_change', 'connected');
});
this.socket.on('disconnect', (reason) => {
console.log('🔌 Disconnected from collaboration service:', reason);
this.connectionState = 'disconnected';
this.emit('connection_state_change', 'disconnected');
if (reason === 'io server disconnect') {
// 服務器主動斷開,不自動重連
return;
}
this.attemptReconnection();
});
this.socket.on('connect_error', (error) => {
console.error('❌ Collaboration service connection error:', error);
this.emit('connection_error', error);
this.attemptReconnection();
});
// 會話事件
this.socket.on('session_joined', (data) => {
this.sessionId = data.sessionId;
this.emit('session_joined', data);
});
this.socket.on('user_joined', (data) => {
this.emit('user_joined', data);
});
this.socket.on('user_left', (data) => {
this.emit('user_left', data);
});
// 協作事件
this.socket.on('collaboration_event', (event) => {
this.emit('collaboration_event', event);
});
this.socket.on('cursor_moved', (data) => {
this.emit('cursor_moved', data);
});
this.socket.on('selection_changed', (data) => {
this.emit('selection_changed', data);
});
// 衝突處理
this.socket.on('conflict_detected', (data) => {
this.emit('conflict_detected', data);
});
this.socket.on('operation_rejected', (data) => {
this.emit('operation_rejected', data);
});
// 心跳
this.socket.on('pong', () => {
// 心跳回應,連接正常
});
// 錯誤處理
this.socket.on('error', (error) => {
console.error('🚨 Collaboration service error:', error);
this.emit('service_error', error);
});
}
/**
* 加入協作會話
*/
async joinSession(sessionId: string, sessionInfo: SessionJoinInfo): Promise<void> {
if (!this.isConnected()) {
throw new Error('Not connected to collaboration service');
}
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Join session timeout'));
}, 10000);
this.socket!.emit('join_session', { sessionId, ...sessionInfo }, (response: any) => {
clearTimeout(timeout);
if (response.success) {
this.sessionId = sessionId;
resolve();
} else {
reject(new Error(response.error || 'Failed to join session'));
}
});
});
}
/**
* 發送協作事件
*/
async sendCollaborationEvent(event: CollaborationEvent): Promise<void> {
if (!this.isConnected() || !this.sessionId) {
// 將事件加入佇列
this.messageQueue.push({
type: 'collaboration_event',
data: event,
timestamp: new Date(),
retries: 0
});
return;
}
try {
await this.emitWithAck('collaboration_event', event);
} catch (error) {
// 發送失敗,加入重試佇列
this.messageQueue.push({
type: 'collaboration_event',
data: event,
timestamp: new Date(),
retries: 0
});
throw error;
}
}
/**
* 更新游標位置
*/
updateCursorPosition(position: CursorPosition): void {
if (!this.isConnected()) return;
// 節流發送,避免過於頻繁
this.throttle(() => {
this.socket!.emit('cursor_move', {
sessionId: this.sessionId,
position
});
}, 50, 'cursor_move');
}
/**
* 更新選擇範圍
*/
updateSelection(selection: SelectionRange): void {
if (!this.isConnected()) return;
this.socket!.emit('selection_change', {
sessionId: this.sessionId,
selection
});
}
/**
* 帶確認的事件發送
*/
private emitWithAck(event: string, data: any, timeout: number = 5000): Promise<any> {
return new Promise((resolve, reject) => {
if (!this.socket) {
reject(new Error('Socket not connected'));
return;
}
const timer = setTimeout(() => {
reject(new Error(`Emit ${event} timeout`));
}, timeout);
this.socket.emit(event, data, (response: any) => {
clearTimeout(timer);
if (response.success) {
resolve(response.data);
} else {
reject(new Error(response.error || 'Unknown error'));
}
});
});
}
/**
* 重連機制
*/
private attemptReconnection(): void {
if (this.reconnectionAttempts >= this.maxReconnectionAttempts) {
this.emit('max_reconnection_attempts_reached');
return;
}
this.reconnectionAttempts++;
const delay = Math.min(1000 * Math.pow(2, this.reconnectionAttempts), 30000);
console.log(`🔄 Attempting reconnection ${this.reconnectionAttempts}/${this.maxReconnectionAttempts} in ${delay}ms`);
setTimeout(() => {
if (this.connectionState === 'disconnected') {
this.socket?.connect();
}
}, delay);
}
/**
* 心跳檢測
*/
private startHeartbeat(): void {
this.heartbeatInterval = setInterval(() => {
if (this.socket?.connected) {
this.socket.emit('ping');
}
}, 30000);
}
/**
* 停止心跳
*/
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
/**
* 處理等待中的訊息
*/
private async flushMessageQueue(): Promise<void> {
while (this.messageQueue.length > 0 && this.isConnected()) {
const message = this.messageQueue.shift()!;
try {
switch (message.type) {
case 'collaboration_event':
await this.emitWithAck('collaboration_event', message.data);
break;
// 其他訊息類型...
}
} catch (error) {
// 重試邏輯
if (message.retries < 3) {
message.retries++;
this.messageQueue.unshift(message);
} else {
console.error('Failed to send queued message after retries:', error);
this.emit('message_send_failed', { message, error });
}
break;
}
}
}
/**
* 節流函數
*/
private throttleTimers: Map<string, NodeJS.Timeout> = new Map();
private throttle(fn: Function, delay: number, key: string): void {
const existingTimer = this.throttleTimers.get(key);
if (existingTimer) {
clearTimeout(existingTimer);
}
const timer = setTimeout(() => {
fn();
this.throttleTimers.delete(key);
}, delay);
this.throttleTimers.set(key, timer);
}
/**
* 檢查連接狀態
*/
isConnected(): boolean {
return this.connectionState === 'connected' && this.socket?.connected === true;
}
/**
* 獲取連接統計
*/
getConnectionStats(): ConnectionStats {
return {
state: this.connectionState,
sessionId: this.sessionId,
reconnectionAttempts: this.reconnectionAttempts,
queuedMessages: this.messageQueue.length,
connected: this.isConnected()
};
}
/**
* 斷開連接
*/
disconnect(): void {
this.stopHeartbeat();
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
this.connectionState = 'disconnected';
this.sessionId = null;
this.messageQueue = [];
this.emit('connection_state_change', 'disconnected');
}
/**
* 清理資源
*/
destroy(): void {
this.disconnect();
this.apiClient.destroy();
this.removeAllListeners();
// 清理節流計時器
this.throttleTimers.forEach(timer => clearTimeout(timer));
this.throttleTimers.clear();
}
}
// 輔助類型定義
interface QueuedMessage {
type: string;
data: any;
timestamp: Date;
retries: number;
}
interface ConnectionStats {
state: string;
sessionId: string | null;
reconnectionAttempts: number;
queuedMessages: number;
connected: boolean;
}
type ConnectionState = 'disconnected' | 'connecting' | 'connected';
// src/hooks/useMicroservices.ts
import { useState, useEffect, useCallback, useRef } from 'react';
import { ApiClient } from '../services/microservices/ApiClient';
import { CollaborationServiceClient } from '../services/microservices/CollaborationServiceClient';
import { useAuth } from './useAuth';
interface MicroservicesState {
otpService: ServiceState;
collaborationService: ServiceState;
notificationService: ServiceState;
}
interface ServiceState {
status: 'disconnected' | 'connecting' | 'connected' | 'error';
lastError?: string;
metrics: ServiceMetrics;
}
interface ServiceMetrics {
requestCount: number;
errorCount: number;
averageResponseTime: number;
lastRequestTime?: Date;
}
export const useMicroservices = () => {
const { user, token } = useAuth();
const [servicesState, setServicesState] = useState<MicroservicesState>({
otpService: { status: 'disconnected', metrics: { requestCount: 0, errorCount: 0, averageResponseTime: 0 } },
collaborationService: { status: 'disconnected', metrics: { requestCount: 0, errorCount: 0, averageResponseTime: 0 } },
notificationService: { status: 'disconnected', metrics: { requestCount: 0, errorCount: 0, averageResponseTime: 0 } }
});
// 服務客戶端實例
const otpServiceRef = useRef<ApiClient | null>(null);
const collaborationServiceRef = useRef<CollaborationServiceClient | null>(null);
const notificationServiceRef = useRef<ApiClient | null>(null);
/**
* 初始化服務連接
*/
const initializeServices = useCallback(async () => {
if (!user || !token) return;
try {
// 初始化 OTP 服務
if (!otpServiceRef.current) {
otpServiceRef.current = new ApiClient('otp-service', {
baseUrl: process.env.REACT_APP_OTP_SERVICE_URL || 'https://api.kyong-saas.com',
timeout: 10000,
retryPolicy: {
maxRetries: 3,
backoffMultiplier: 2,
initialDelay: 1000,
maxDelay: 10000,
retryableErrors: ['ECONNRESET', 'ETIMEDOUT']
},
circuitBreaker: {
threshold: 5,
timeout: 30000,
monitor: true
},
authentication: { token }
});
setServicesState(prev => ({
...prev,
otpService: { ...prev.otpService, status: 'connected' }
}));
}
// 初始化協作服務
if (!collaborationServiceRef.current) {
collaborationServiceRef.current = new CollaborationServiceClient({
apiEndpoint: process.env.REACT_APP_COLLABORATION_API_URL || 'https://collaboration.kyong-saas.com',
websocketUrl: process.env.REACT_APP_COLLABORATION_WS_URL || 'wss://collaboration.kyong-saas.com',
auth: { token }
});
// 設置協作服務事件監聽
setupCollaborationServiceListeners();
// 連接到協作服務
await collaborationServiceRef.current.connect({
userId: user.id,
userName: user.name,
tenantId: user.tenantId,
role: user.role
});
setServicesState(prev => ({
...prev,
collaborationService: { ...prev.collaborationService, status: 'connected' }
}));
}
// 初始化通知服務
if (!notificationServiceRef.current) {
notificationServiceRef.current = new ApiClient('notification-service', {
baseUrl: process.env.REACT_APP_NOTIFICATION_SERVICE_URL || 'https://notifications.kyong-saas.com',
timeout: 5000,
retryPolicy: {
maxRetries: 2,
backoffMultiplier: 1.5,
initialDelay: 500,
maxDelay: 5000,
retryableErrors: ['ECONNRESET']
},
circuitBreaker: {
threshold: 3,
timeout: 15000,
monitor: true
},
authentication: { token }
});
setServicesState(prev => ({
...prev,
notificationService: { ...prev.notificationService, status: 'connected' }
}));
}
} catch (error) {
console.error('Failed to initialize microservices:', error);
setServicesState(prev => ({
otpService: { ...prev.otpService, status: 'error', lastError: error.message },
collaborationService: { ...prev.collaborationService, status: 'error', lastError: error.message },
notificationService: { ...prev.notificationService, status: 'error', lastError: error.message }
}));
}
}, [user, token]);
/**
* 設置協作服務事件監聽
*/
const setupCollaborationServiceListeners = useCallback(() => {
const collaborationService = collaborationServiceRef.current;
if (!collaborationService) return;
collaborationService.on('connection_state_change', (state) => {
setServicesState(prev => ({
...prev,
collaborationService: { ...prev.collaborationService, status: state }
}));
});
collaborationService.on('connection_error', (error) => {
setServicesState(prev => ({
...prev,
collaborationService: {
...prev.collaborationService,
status: 'error',
lastError: error.message
}
}));
});
collaborationService.on('service_error', (error) => {
updateServiceMetrics('collaborationService', 'error');
});
}, []);
/**
* 更新服務指標
*/
const updateServiceMetrics = useCallback((
serviceName: keyof MicroservicesState,
type: 'request' | 'error'
) => {
setServicesState(prev => {
const service = prev[serviceName];
const newMetrics = { ...service.metrics };
if (type === 'request') {
newMetrics.requestCount++;
newMetrics.lastRequestTime = new Date();
} else if (type === 'error') {
newMetrics.errorCount++;
}
return {
...prev,
[serviceName]: {
...service,
metrics: newMetrics
}
};
});
}, []);
/**
* OTP 服務 API 包裝
*/
const otpService = {
get: async <T>(url: string, config?: any): Promise<T> => {
updateServiceMetrics('otpService', 'request');
try {
return await otpServiceRef.current!.get<T>(url, config);
} catch (error) {
updateServiceMetrics('otpService', 'error');
throw error;
}
},
post: async <T>(url: string, data?: any, config?: any): Promise<T> => {
updateServiceMetrics('otpService', 'request');
try {
return await otpServiceRef.current!.post<T>(url, data, config);
} catch (error) {
updateServiceMetrics('otpService', 'error');
throw error;
}
},
put: async <T>(url: string, data?: any, config?: any): Promise<T> => {
updateServiceMetrics('otpService', 'request');
try {
return await otpServiceRef.current!.put<T>(url, data, config);
} catch (error) {
updateServiceMetrics('otpService', 'error');
throw error;
}
},
delete: async <T>(url: string, config?: any): Promise<T> => {
updateServiceMetrics('otpService', 'request');
try {
return await otpServiceRef.current!.delete<T>(url, config);
} catch (error) {
updateServiceMetrics('otpService', 'error');
throw error;
}
}
};
/**
* 協作服務 API
*/
const collaborationService = {
joinSession: async (sessionId: string, sessionInfo: any) => {
return collaborationServiceRef.current?.joinSession(sessionId, sessionInfo);
},
sendCollaborationEvent: async (event: any) => {
return collaborationServiceRef.current?.sendCollaborationEvent(event);
},
updateCursorPosition: (position: any) => {
collaborationServiceRef.current?.updateCursorPosition(position);
},
updateSelection: (selection: any) => {
collaborationServiceRef.current?.updateSelection(selection);
},
on: (event: string, listener: (...args: any[]) => void) => {
collaborationServiceRef.current?.on(event, listener);
},
off: (event: string, listener: (...args: any[]) => void) => {
collaborationServiceRef.current?.off(event, listener);
},
getConnectionStats: () => {
return collaborationServiceRef.current?.getConnectionStats();
}
};
/**
* 通知服務 API
*/
const notificationService = {
sendNotification: async (notification: any) => {
updateServiceMetrics('notificationService', 'request');
try {
return await notificationServiceRef.current!.post('/notifications', notification);
} catch (error) {
updateServiceMetrics('notificationService', 'error');
throw error;
}
},
getNotifications: async (params?: any) => {
updateServiceMetrics('notificationService', 'request');
try {
return await notificationServiceRef.current!.get('/notifications', { params });
} catch (error) {
updateServiceMetrics('notificationService', 'error');
throw error;
}
}
};
/**
* 健康檢查
*/
const performHealthCheck = useCallback(async () => {
const healthResults = {
otpService: false,
collaborationService: false,
notificationService: false
};
try {
await Promise.allSettled([
otpServiceRef.current?.get('/health').then(() => { healthResults.otpService = true; }),
collaborationServiceRef.current?.isConnected() ? Promise.resolve().then(() => { healthResults.collaborationService = true; }) : Promise.reject(),
notificationServiceRef.current?.get('/health').then(() => { healthResults.notificationService = true; })
]);
} catch (error) {
console.warn('Health check completed with some failures');
}
return healthResults;
}, []);
/**
* 清理資源
*/
const cleanup = useCallback(() => {
otpServiceRef.current?.destroy();
collaborationServiceRef.current?.destroy();
notificationServiceRef.current?.destroy();
otpServiceRef.current = null;
collaborationServiceRef.current = null;
notificationServiceRef.current = null;
}, []);
// 初始化服務
useEffect(() => {
initializeServices();
return cleanup;
}, [initializeServices, cleanup]);
// 定期健康檢查
useEffect(() => {
const healthCheckInterval = setInterval(performHealthCheck, 60000); // 每分鐘檢查一次
return () => clearInterval(healthCheckInterval);
}, [performHealthCheck]);
return {
servicesState,
otpService,
collaborationService,
notificationService,
healthCheck: performHealthCheck,
cleanup
};
};
// src/components/ServiceMonitor/MicroservicesDashboard.tsx
import React, { useState, useEffect } from 'react';
import {
Card,
Group,
Text,
Badge,
Stack,
Progress,
ActionIcon,
Tooltip,
Grid,
Alert,
Button,
Modal,
ScrollArea,
Indicator
} from '@mantine/core';
import {
IconRefresh,
IconSettings,
IconAlertTriangle,
IconCheck,
IconX,
IconClock,
IconTrendingUp
} from '@tabler/icons-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useMicroservices } from '../../hooks/useMicroservices';
export const MicroservicesDashboard: React.FC = () => {
const { servicesState, healthCheck } = useMicroservices();
const [isRefreshing, setIsRefreshing] = useState(false);
const [detailsModalOpened, setDetailsModalOpened] = useState(false);
const [selectedService, setSelectedService] = useState<string | null>(null);
/**
* 執行健康檢查
*/
const handleRefresh = async () => {
setIsRefreshing(true);
try {
await healthCheck();
} finally {
setIsRefreshing(false);
}
};
/**
* 獲取狀態顏色
*/
const getStatusColor = (status: string) => {
switch (status) {
case 'connected': return 'green';
case 'connecting': return 'yellow';
case 'error': return 'red';
default: return 'gray';
}
};
/**
* 獲取狀態圖示
*/
const getStatusIcon = (status: string) => {
switch (status) {
case 'connected': return <IconCheck size={16} />;
case 'connecting': return <IconClock size={16} />;
case 'error': return <IconX size={16} />;
default: return <IconAlertTriangle size={16} />;
}
};
/**
* 計算錯誤率
*/
const getErrorRate = (metrics: any) => {
if (metrics.requestCount === 0) return 0;
return (metrics.errorCount / metrics.requestCount) * 100;
};
/**
* 獲取整體健康度
*/
const getOverallHealth = () => {
const services = Object.values(servicesState);
const connectedCount = services.filter(s => s.status === 'connected').length;
return (connectedCount / services.length) * 100;
};
const overallHealth = getOverallHealth();
return (
<Card shadow="sm" padding="lg" radius="md" withBorder>
<Stack gap="md">
{/* 標題列 */}
<Group justify="space-between">
<Group>
<Text size="lg" fw={600}>微服務狀態監控</Text>
<Indicator
color={overallHealth > 80 ? 'green' : overallHealth > 50 ? 'yellow' : 'red'}
size={8}
/>
</Group>
<Group>
<Tooltip label="刷新狀態">
<ActionIcon
variant="light"
onClick={handleRefresh}
loading={isRefreshing}
>
<IconRefresh size={16} />
</ActionIcon>
</Tooltip>
<Tooltip label="設置">
<ActionIcon variant="light">
<IconSettings size={16} />
</ActionIcon>
</Tooltip>
</Group>
</Group>
{/* 整體健康度 */}
<Card withBorder padding="sm">
<Group justify="space-between" mb="xs">
<Text size="sm" fw={500}>整體健康度</Text>
<Text size="sm" c="dimmed">{Math.round(overallHealth)}%</Text>
</Group>
<Progress
value={overallHealth}
color={overallHealth > 80 ? 'green' : overallHealth > 50 ? 'yellow' : 'red'}
size="lg"
radius="xl"
/>
</Card>
{/* 服務狀態卡片 */}
<Grid>
{Object.entries(servicesState).map(([serviceName, serviceState]) => (
<Grid.Col key={serviceName} span={{ base: 12, md: 4 }}>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<ServiceStatusCard
serviceName={serviceName}
serviceState={serviceState}
onDetailsClick={() => {
setSelectedService(serviceName);
setDetailsModalOpened(true);
}}
/>
</motion.div>
</Grid.Col>
))}
</Grid>
{/* 警告和錯誤 */}
<AnimatePresence>
{Object.entries(servicesState).some(([_, state]) => state.status === 'error') && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
>
<Alert
icon={<IconAlertTriangle size={16} />}
color="red"
title="服務異常"
>
<Stack gap="xs">
{Object.entries(servicesState)
.filter(([_, state]) => state.status === 'error')
.map(([serviceName, state]) => (
<Text key={serviceName} size="sm">
{serviceName}: {state.lastError}
</Text>
))
}
</Stack>
</Alert>
</motion.div>
)}
</AnimatePresence>
{/* 服務詳情模態框 */}
<Modal
opened={detailsModalOpened}
onClose={() => setDetailsModalOpened(false)}
title={`${selectedService} 詳細資訊`}
size="lg"
>
{selectedService && (
<ServiceDetailsModal
serviceName={selectedService}
serviceState={servicesState[selectedService as keyof typeof servicesState]}
/>
)}
</Modal>
</Stack>
</Card>
);
};
// 服務狀態卡片
const ServiceStatusCard: React.FC<{
serviceName: string;
serviceState: any;
onDetailsClick: () => void;
}> = ({ serviceName, serviceState, onDetailsClick }) => {
const statusColor = getStatusColor(serviceState.status);
const errorRate = getErrorRate(serviceState.metrics);
return (
<Card
withBorder
padding="md"
style={{
cursor: 'pointer',
transition: 'all 0.2s ease',
}}
onClick={onDetailsClick}
className="hover:shadow-md"
>
<Stack gap="sm">
{/* 服務名稱和狀態 */}
<Group justify="space-between">
<Text size="sm" fw={600} tt="capitalize">
{serviceName.replace('Service', '')}
</Text>
<Badge
color={statusColor}
variant="light"
leftSection={getStatusIcon(serviceState.status)}
>
{serviceState.status}
</Badge>
</Group>
{/* 指標 */}
<Group justify="space-between">
<div>
<Text size="xs" c="dimmed">請求數</Text>
<Text size="sm" fw={500}>{serviceState.metrics.requestCount}</Text>
</div>
<div>
<Text size="xs" c="dimmed">錯誤率</Text>
<Text size="sm" fw={500} c={errorRate > 5 ? 'red' : 'green'}>
{errorRate.toFixed(1)}%
</Text>
</div>
<div>
<Text size="xs" c="dimmed">回應時間</Text>
<Text size="sm" fw={500}>
{serviceState.metrics.averageResponseTime || 0}ms
</Text>
</div>
</Group>
{/* 錯誤率進度條 */}
{errorRate > 0 && (
<Progress
value={Math.min(errorRate, 100)}
color={errorRate > 10 ? 'red' : errorRate > 5 ? 'yellow' : 'green'}
size="xs"
/>
)}
</Stack>
</Card>
);
};
// 服務詳情模態框
const ServiceDetailsModal: React.FC<{
serviceName: string;
serviceState: any;
}> = ({ serviceName, serviceState }) => {
return (
<ScrollArea.Autosize mah={400}>
<Stack gap="md">
{/* 基本資訊 */}
<Card withBorder padding="sm">
<Text size="sm" fw={500} mb="xs">基本資訊</Text>
<Stack gap="xs">
<Group justify="space-between">
<Text size="xs" c="dimmed">狀態</Text>
<Badge color={getStatusColor(serviceState.status)}>
{serviceState.status}
</Badge>
</Group>
{serviceState.lastError && (
<Group justify="space-between">
<Text size="xs" c="dimmed">最後錯誤</Text>
<Text size="xs" c="red">{serviceState.lastError}</Text>
</Group>
)}
</Stack>
</Card>
{/* 性能指標 */}
<Card withBorder padding="sm">
<Text size="sm" fw={500} mb="xs">性能指標</Text>
<Stack gap="xs">
<Group justify="space-between">
<Text size="xs" c="dimmed">總請求數</Text>
<Text size="xs">{serviceState.metrics.requestCount}</Text>
</Group>
<Group justify="space-between">
<Text size="xs" c="dimmed">錯誤數</Text>
<Text size="xs" c={serviceState.metrics.errorCount > 0 ? 'red' : 'green'}>
{serviceState.metrics.errorCount}
</Text>
</Group>
<Group justify="space-between">
<Text size="xs" c="dimmed">平均回應時間</Text>
<Text size="xs">{serviceState.metrics.averageResponseTime || 0}ms</Text>
</Group>
{serviceState.metrics.lastRequestTime && (
<Group justify="space-between">
<Text size="xs" c="dimmed">最後請求時間</Text>
<Text size="xs">
{new Date(serviceState.metrics.lastRequestTime).toLocaleString()}
</Text>
</Group>
)}
</Stack>
</Card>
</Stack>
</ScrollArea.Autosize>
);
};
function getStatusColor(status: string): string {
switch (status) {
case 'connected': return 'green';
case 'connecting': return 'yellow';
case 'error': return 'red';
default: return 'gray';
}
}
function getStatusIcon(status: string) {
switch (status) {
case 'connected': return <IconCheck size={16} />;
case 'connecting': return <IconClock size={16} />;
case 'error': return <IconX size={16} />;
default: return <IconAlertTriangle size={16} />;
}
}
function getErrorRate(metrics: any): number {
if (metrics.requestCount === 0) return 0;
return (metrics.errorCount / metrics.requestCount) * 100;
}
今天我們完成了重要的架構升級,將系統從單體轉向微服務架構:
微服務架構
前端通信架構
服務監控體系
可靠性增強