iT邦幫忙

2025 iThome 鐵人賽

DAY 17
0
Modern Web

30 天製作工作室 SaaS 產品 (前端篇)系列 第 17

Day 17: 30天打造SaaS產品前端篇-微服務架構升級與 WebSocket 通信最佳化實作

  • 分享至 

  • xImage
  •  

前情提要

在 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;
}

微服務通信管理器

統一的 API 客戶端

// 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 {
    // 清理定時器等資源
  }
}

WebSocket 微服務客戶端

協作服務專用客戶端

// 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';

微服務管理 Hook

統一的服務管理

// 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;
}

今日總結

今天我們完成了重要的架構升級,將系統從單體轉向微服務架構:

核心改進

  1. 微服務架構

    • WebSocket 協作引擎獨立為專門服務
    • 統一的 API 客戶端管理
    • 服務間通信最佳化
  2. 前端通信架構

    • 熔斷器模式防止級聯故障
    • 智能重試機制提升可靠性
    • WebSocket 連線管理最佳化
  3. 服務監控體系

    • 即時服務狀態監控
    • 性能指標追蹤
    • 可視化服務健康度
  4. 可靠性增強

    • 優雅的錯誤處理和恢復
    • 訊息佇列確保資料不丟失
    • 自動重連和故障轉移

技術特色

  • 可擴展性: 各服務可獨立擴展
  • 容錯性: 單一服務故障不影響整體
  • 可維護性: 服務邊界清晰,職責分明
  • 可觀測性: 完整的監控和追蹤體系

上一篇
Day 16: 30天打造SaaS產品前端篇-即時協作課程排程系統與進階拖拽互動實作
下一篇
Day 18: 30天打造SaaS產品前端篇-前端效能優化方法
系列文
30 天製作工作室 SaaS 產品 (前端篇)18
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言