TypeScript设计模式:仲裁者模式

仲裁者模式(Mediator Pattern)是一种行为型设计模式,通过一个中介者对象(Mediator)管理一组对象(Colleague)之间的交互。Colleague对象不直接通信,而是通过Mediator发送和接收消息,从而实现松耦合。

模式结构

  • Mediator(仲裁者接口) :定义Colleague之间通信的接口。
  • ConcreteMediator(具体仲裁者) :实现Mediator接口,协调Colleague的交互。
  • Colleague(同事接口) :定义与Mediator交互的方法。
  • ConcreteColleague(具体同事) :实现Colleague接口,持有Mediator引用。

优点

  • 松耦合:组件无需直接引用彼此,仅与Mediator交互。
  • 集中管理:交互逻辑集中在Mediator,便于维护。
  • 可扩展:添加新组件只需与Mediator对接。

缺点

  • Mediator复杂性:交互逻辑过多可能导致Mediator成为"上帝类"。
  • 性能开销:所有通信通过Mediator,可能引入瓶颈。

应用场景

仲裁者模式适用于多组件复杂交互的场景,例如:

  • 聊天室:用户通过服务器中转消息。
  • GUI系统:控件通过对话框协调事件。
  • 微服务架构:服务通过消息队列交互。

在我们的示例中,聊天室包含以下组件:

  • ChatClient:处理用户消息发送和接收。
  • MessageManager:管理消息记录。
  • UserManager:维护在线用户列表。
  • NotificationManager:处理系统通知。

代码实现

项目结构

css 复制代码
chat-app/
├── backend/
│   ├── src/
│   │   ├── mediator.ts
│   │   ├── colleague.ts
│   │   ├── chat-mediator.ts
│   │   ├── components/
│   │   │   ├── chat-client.ts
│   │   │   ├── message-manager.ts
│   │   │   ├── user-manager.ts
│   │   │   ├── notification-manager.ts
│   │   ├── server.ts
│   ├── tsconfig.json
├── frontend/
│   ├── src/
│   │   ├── ChatApp.tsx
│   │   ├── index.css
│   ├── tailwind.config.js
│   ├── package.json

后端实现(Node.js + TypeScript + Socket.io

1. 安装依赖

kotlin 复制代码
npm init -y
npm install socket.io typescript @types/socket.io @types/node
npx tsc --init

2. 定义接口(mediator.ts)

typescript 复制代码
type ChatEvent = 
  | 'message_sent'
  | 'message_received'
  | 'user_joined'
  | 'user_left'
  | 'user_typing'
  | 'connection_status_changed'
  | 'notification_created';

interface MessageData {
  id: string;
  content: string;
  sender: string;
  timestamp: number;
  type?: 'text' | 'system' | 'notification';
}

interface UserData {
  id: string;
  name: string;
  avatar?: string;
  status: 'online' | 'offline' | 'typing';
}

interface ChatMediator {
  notify(sender: ChatComponent, event: ChatEvent, data?: any): void;
  registerComponent(name: string, component: ChatComponent): void;
  getComponent(name: string): ChatComponent | undefined;
}

3. 定义抽象组件(colleague.ts)

typescript 复制代码
interface ChatComponent {
  notify(event: ChatEvent, data?: any): void;
  receive(event: ChatEvent, data?: any): void;
  initialize(): void;
  cleanup(): void;
}

4. 具体仲裁者(chat-mediator.ts)

typescript 复制代码
import { Server as SocketIOServer, Socket } from 'socket.io';
import { ChatComponent } from './colleague';
import { ChatMediator, MessageData, UserData, ChatEvent } from './mediator';

class ConcreteChatMediator implements ChatMediator {
  private components: Map<string, ChatComponent> = new Map();
  private io: SocketIOServer;
  private connectedUsers: Map<string, UserData> = new Map();
  private messages: MessageData[] = [];

  constructor(io: SocketIOServer) {
    this.io = io;
    this.setupSocketListeners();
  }

  registerComponent(name: string, component: ChatComponent): void {
    this.components.set(name, component);
    console.log(`Component ${name} registered`);
  }

  getComponent(name: string): ChatComponent | undefined {
    return this.components.get(name);
  }

  notify(sender: ChatComponent, event: ChatEvent, data?: any): void {
    console.log(`Event ${event} from ${sender.constructor.name}:`, data);

    switch (event) {
      case 'message_sent':
        this.handleMessageSent(data);
        break;
      case 'user_joined':
        this.handleUserJoined(data);
        break;
      case 'user_left':
        this.handleUserLeft(data);
        break;
      case 'user_typing':
        this.handleUserTyping(data);
        break;
      case 'connection_status_changed':
        this.handleConnectionStatusChanged(data);
        break;
      default:
        this.broadcastToComponents(event, data, sender);
    }
  }

  private setupSocketListeners(): void {
    this.io.on('connection', (socket: Socket) => {
      console.log('User connected:', socket.id);

      socket.on('user_join', (userData: UserData) => {
        this.notify(this.getSocketComponent(), 'user_joined', {
          ...userData,
          socketId: socket.id
        });
      });

      socket.on('send_message', (messageData: Partial<MessageData>) => {
        this.notify(this.getSocketComponent(), 'message_sent', {
          ...messageData,
          id: this.generateMessageId(),
          timestamp: Date.now(),
          socketId: socket.id
        });
      });

      socket.on('typing', (data: { userId: string; isTyping: boolean }) => {
        this.notify(this.getSocketComponent(), 'user_typing', {
          ...data,
          socketId: socket.id
        });
      });

      socket.on('disconnect', () => {
        const user = Array.from(this.connectedUsers.values())
          .find(u => u.id === socket.id);
        if (user) {
          this.notify(this.getSocketComponent(), 'user_left', {
            userId: user.id,
            socketId: socket.id
          });
        }
      });
    });
  }

  private handleMessageSent(data: MessageData & { socketId: string }): void {
    const message: MessageData = {
      id: data.id,
      content: data.content,
      sender: data.sender,
      timestamp: data.timestamp,
      type: data.type || 'text'
    };

    this.messages.push(message);
    this.io.emit('message_received', message);
    this.broadcastToComponents('message_received', message);
  }

  private handleUserJoined(data: UserData & { socketId: string }): void {
    const userData: UserData = {
      id: data.socketId,
      name: data.name,
      avatar: data.avatar,
      status: 'online'
    };

    this.connectedUsers.set(data.socketId, userData);
    this.io.to(data.socketId).emit('user_list', Array.from(this.connectedUsers.values()));
    this.io.emit('user_joined', userData);
    this.broadcastToComponents('user_joined', userData);
  }

  private handleUserLeft(data: { userId: string; socketId: string }): void {
    const user = this.connectedUsers.get(data.socketId);
    if (user) {
      this.connectedUsers.delete(data.socketId);
      this.io.emit('user_left', { userId: user.id, name: user.name });
      this.broadcastToComponents('user_left', user);
    }
  }

  private handleUserTyping(data: { userId: string; isTyping: boolean; socketId: string }): void {
    this.io.except(data.socketId).emit('user_typing', {
      userId: data.userId,
      isTyping: data.isTyping
    });
    this.broadcastToComponents('user_typing', data);
  }

  private handleConnectionStatusChanged(data: { status: 'connected' | 'disconnected' | 'error' }): void {
    this.broadcastToComponents('connection_status_changed', data);
  }

  private broadcastToComponents(event: ChatEvent, data: any, exclude?: ChatComponent): void {
    this.components.forEach(component => {
      if (component !== exclude) {
        component.receive(event, data);
      }
    });
  }

  private generateMessageId(): string {
    return `msg_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }

  private getSocketComponent(): ChatComponent {
    return this.components.get('socketManager') as ChatComponent;
  }

  getMessages(): MessageData[] {
    return [...this.messages];
  }

  getOnlineUsers(): UserData[] {
    return Array.from(this.connectedUsers.values());
  }
}

5. 具体组件(components/socket-manager.ts)

typescript 复制代码
import { Socket } from 'socket.io';
import { ChatComponent } from '../colleague';
import { ChatMediator, ChatEvent } from '../mediator';

class SocketManager extends ChatComponent {
  private socket: Socket | null = null;

  constructor(mediator: ChatMediator) {
    super(mediator, 'socketManager');
  }

  initialize(): void {
    console.log('SocketManager initialized');
  }

  receive(event: ChatEvent, data?: any): void {
    if (event === 'connection_status_changed') {
      console.log('Connection status changed:', data.status);
    }
  }

  cleanup(): void {
    if (this.socket) {
      this.socket.disconnect();
    }
  }
}

6. 消息管理(components/message-manager.ts)

typescript 复制代码
import { ChatComponent } from '../colleague';
import { ChatMediator, ChatEvent, MessageData } from '../mediator';

class MessageManager extends ChatComponent {
  private messages: MessageData[] = [];

  constructor(mediator: ChatMediator) {
    super(mediator, 'messageManager');
  }

  initialize(): void {
    console.log('MessageManager initialized');
  }

  receive(event: ChatEvent, data?: any): void {
    switch (event) {
      case 'message_received':
        this.addMessage(data as MessageData);
        break;
      case 'user_joined':
        this.addSystemMessage(`${data.name} 加入了聊天室`);
        break;
      case 'user_left':
        this.addSystemMessage(`${data.name} 离开了聊天室`);
        break;
    }
  }

  private addMessage(message: MessageData): void {
    this.messages.push(message);
    console.log('New message added:', message);
  }

  private addSystemMessage(content: string): void {
    const systemMessage: MessageData = {
      id: this.generateId(),
      content,
      sender: 'system',
      timestamp: Date.now(),
      type: 'system'
    };
    this.addMessage(systemMessage);
  }

  sendMessage(content: string, sender: string): void {
    this.notify('message_sent', {
      content,
      sender,
      type: 'text'
    });
  }

  getMessages(): MessageData[] {
    return [...this.messages];
  }

  cleanup(): void {
    this.messages = [];
  }

  private generateId(): string {
    return `sys_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

7. 用户管理(components/user-manager.ts)

typescript 复制代码
import { ChatComponent } from '../colleague';
import { ChatMediator, ChatEvent, UserData } from '../mediator';

class UserManager extends ChatComponent {
  private users: Map<string, UserData> = new Map();
  private typingUsers: Set<string> = new Set();

  constructor(mediator: ChatMediator) {
    super(mediator, 'userManager');
  }

  initialize(): void {
    console.log('UserManager initialized');
  }

  receive(event: ChatEvent, data?: any): void {
    switch (event) {
      case 'user_joined':
        this.addUser(data as UserData);
        break;
      case 'user_left':
        this.removeUser(data.id || data.userId);
        break;
      case 'user_typing':
        this.updateUserTypingStatus(data.userId, data.isTyping);
        break;
    }
  }

  private addUser(user: UserData): void {
    this.users.set(user.id, user);
    console.log('User added:', user);
  }

  private removeUser(userId: string): void {
    this.users.delete(userId);
    this.typingUsers.delete(userId);
    console.log('User removed:', userId);
  }

  private updateUserTypingStatus(userId: string, isTyping: boolean): void {
    if (isTyping) {
      this.typingUsers.add(userId);
    } else {
      this.typingUsers.delete(userId);
    }
  }

  joinUser(userData: UserData): void {
    this.notify('user_joined', userData);
  }

  setUserTyping(userId: string, isTyping: boolean): void {
    this.notify('user_typing', { userId, isTyping });
  }

  getUsers(): UserData[] {
    return Array.from(this.users.values());
  }

  getTypingUsers(): string[] {
    return Array.from(this.typingUsers);
  }

  cleanup(): void {
    this.users.clear();
    this.typingUsers.clear();
  }
}

8. 通知管理(components/notification-manager.ts)

typescript 复制代码
import { ChatComponent } from '../colleague';
import { ChatMediator, ChatEvent } from '../mediator';

class NotificationManager extends ChatComponent {
  private notifications: Array<{
    id: string;
    message: string;
    type: 'info' | 'warning' | 'error';
    timestamp: number;
  }> = [];

  constructor(mediator: ChatMediator) {
    super(mediator, 'notificationManager');
  }

  initialize(): void {
    console.log('NotificationManager initialized');
  }

  receive(event: ChatEvent, data?: any): void {
    switch (event) {
      case 'user_joined':
        this.createNotification(`${data.name} 加入了聊天室`, 'info');
        break;
      case 'user_left':
        this.createNotification(`${data.name} 离开了聊天室`, 'info');
        break;
      case 'connection_status_changed':
        const message = data.status === 'connected' ? '已连接到服务器' : '与服务器连接断开';
        const type = data.status === 'connected' ? 'info' : 'error';
        this.createNotification(message, type);
        break;
    }
  }

  private createNotification(message: string, type: 'info' | 'warning' | 'error'): void {
    const notification = {
      id: this.generateId(),
      message,
      type,
      timestamp: Date.now()
    };
    
    this.notifications.push(notification);
    console.log('Notification created:', notification);
    this.notify('notification_created', notification);
  }

  getNotifications(): typeof this.notifications {
    return [...this.notifications];
  }

  clearNotifications(): void {
    this.notifications = [];
  }

  cleanup(): void {
    this.clearNotifications();
  }

  private generateId(): string {
    return `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

9. 服务器启动(server.ts)

typescript 复制代码
import { createServer } from 'http';
import { Server } from 'socket.io';
import { ConcreteChatMediator } from './chatMediator';
import { SocketManager } from './components/socketManager';
import { MessageManager } from './components/messageManager';
import { UserManager } from './components/userManager';
import { NotificationManager } from './components/notificationManager';

const httpServer = createServer();
const io = new Server(httpServer, { cors: { origin: '*' } });
const mediator = new ConcreteChatMediator(io);

// 注册组件
new SocketManager(mediator);
new MessageManager(mediator);
new UserManager(mediator);
new NotificationManager(mediator);

httpServer.listen(3001, () => console.log('Server running on port 3001'));

10. 编译并运行

bash 复制代码
npx tsc
node dist/server.js

前端实现(React + Tailwind CSS + Socket.io-client)

1. 创建React项目

lua 复制代码
npx create-react-app chat-frontend --template typescript
cd chat-frontend
npm install socket.io-client tailwindcss postcss autoprefixer
npx tailwindcss init -p

2. 配置tailwind.config.js

css 复制代码
module.exports = {
  content: ['./src/**/*.{js,jsx,ts,tsx}'],
  theme: { extend: {} },
  plugins: [],
};

3. 配置src/index.css

less 复制代码
@tailwind base;
@tailwind components;
@tailwind utilities;

4. 前端主组件(ChatApp.tsx)

tsx 复制代码
import React, { useState, useEffect, useRef } from 'react';
import io, { Socket } from 'socket.io-client';

type ChatEvent = 
  | 'message_sent'
  | 'message_received'
  | 'user_joined'
  | 'user_left'
  | 'user_typing'
  | 'connection_status_changed'
  | 'notification_created'
  | 'user_list';

interface MessageData {
  id: string;
  content: string;
  sender: string;
  timestamp: number;
  type?: 'text' | 'system' | 'notification';
}

interface UserData {
  id: string;
  name: string;
  avatar?: string;
  status: 'online' | 'offline' | 'typing';
}

interface ChatMediator {
  notify(sender: ChatComponent, event: ChatEvent, data?: any): void;
  registerComponent(name: string, component: ChatComponent): void;
  getComponent(name: string): ChatComponent | undefined;
}

interface ChatComponent {
  notify(event: ChatEvent, data?: any): void;
  receive(event: ChatEvent, data?: any): void;
  initialize(): void;
  cleanup(): void;
}

class ClientChatMediator implements ChatMediator {
  private components: Map<string, ChatComponent> = new Map();
  private socket: Socket;

  constructor(serverUrl: string) {
    this.socket = io(serverUrl);
    this.setupSocketListeners();
  }

  registerComponent(name: string, component: ChatComponent): void {
    this.components.set(name, component);
  }

  getComponent(name: string): ChatComponent | undefined {
    return this.components.get(name);
  }

  notify(sender: ChatComponent, event: ChatEvent, data?: any): void {
    switch (event) {
      case 'message_sent':
        this.socket.emit('send_message', data);
        break;
      case 'user_joined':
        this.socket.emit('user_join', data);
        break;
      case 'user_typing':
        this.socket.emit('typing', data);
        break;
      default:
        this.broadcastToComponents(event, data, sender);
    }
  }

  private setupSocketListeners(): void {
    this.socket.on('message_received', (data) => {
      this.broadcastToComponents('message_received', data);
    });

    this.socket.on('user_joined', (data) => {
      this.broadcastToComponents('user_joined', data);
    });

    this.socket.on('user_left', (data) => {
      this.broadcastToComponents('user_left', data);
    });

    this.socket.on('user_typing', (data) => {
      this.broadcastToComponents('user_typing', data);
    });

    this.socket.on('user_list', (data) => {
      this.broadcastToComponents('user_list', data);
    });

    this.socket.on('connect', () => {
      this.broadcastToComponents('connection_status_changed', { status: 'connected' });
    });

    this.socket.on('disconnect', () => {
      this.broadcastToComponents('connection_status_changed', { status: 'disconnected' });
    });
  }

  private broadcastToComponents(event: ChatEvent, data: any, exclude?: ChatComponent): void {
    this.components.forEach(component => {
      if (component !== exclude) {
        component.receive(event, data);
      }
    });
  }

  getSocket(): Socket {
    return this.socket;
  }
}

const useChatMediator = (serverUrl: string) => {
  const [mediator] = useState(() => new ClientChatMediator(serverUrl));
  return mediator;
};

const ChatApp: React.FC = () => {
  const mediator = useChatMediator('http://localhost:3001');
  const [currentUser, setCurrentUser] = useState<UserData | null>(null);

  useEffect(() => {
    const userData: UserData = {
      id: `user_${Date.now()}`,
      name: `用户${Math.floor(Math.random() * 1000)}`,
      status: 'online'
    };
    setCurrentUser(userData);
  }, []);

  return (
    <div className="min-h-screen bg-gray-100 flex">
      <div className="w-1/4 bg-white border-r">
        <UserListComponent mediator={mediator} currentUser={currentUser} />
      </div>
      <div className="flex-1 flex flex-col">
        <NotificationComponent mediator={mediator} />
        <div className="flex-1">
          <MessageListComponent mediator={mediator} currentUser={currentUser} />
        </div>
        <MessageInputComponent mediator={mediator} currentUser={currentUser} />
      </div>
    </div>
  );
};

const MessageListComponent: React.FC<{
  mediator: ClientChatMediator;
  currentUser: UserData | null;
}> = ({ mediator, currentUser }) => {
  const [messages, setMessages] = useState<MessageData[]>([]);
  const messagesEndRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    class MessageListManager implements ChatComponent {
      constructor(mediator: ChatMediator) {
        this.mediator = mediator;
        this.componentName = 'messageList';
        this.mediator.registerComponent(this.componentName, this);
      }
      protected mediator: ChatMediator;
      protected componentName: string;
      initialize(): void {}
      notify(event: ChatEvent, data?: any): void {
        this.mediator.notify(this, event, data);
      }
      receive(event: ChatEvent, data?: any): void {
        if (event === 'message_received') {
          setMessages(prev => [...prev, data]);
        }
      }
      cleanup(): void {}
    }

    const manager = new MessageListManager(mediator);
    return () => manager.cleanup();
  }, [mediator]);

  useEffect(() => {
    messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
  }, [messages]);

  const formatTime = (timestamp: number) => {
    return new Date(timestamp).toLocaleTimeString();
  };

  return (
    <div className="flex-1 overflow-y-auto p-4 space-y-4">
      {messages.map((message) => (
        <div
          key={message.id}
          className={`flex ${
            message.sender === currentUser?.name ? 'justify-end' : 'justify-start'
          }`}
        >
          <div
            className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${
              message.type === 'system'
                ? 'bg-gray-200 text-gray-600 mx-auto text-sm'
                : message.sender === currentUser?.name
                ? 'bg-blue-500 text-white'
                : 'bg-white border'
            }`}
          >
            {message.type !== 'system' && message.sender !== currentUser?.name && (
              <div className="text-xs text-gray-500 mb-1">{message.sender}</div>
            )}
            <div>{message.content}</div>
            <div className="text-xs opacity-70 mt-1">
              {formatTime(message.timestamp)}
            </div>
          </div>
        </div>
      ))}
      <div ref={messagesEndRef} />
    </div>
  );
};

const MessageInputComponent: React.FC<{
  mediator: ClientChatMediator;
  currentUser: UserData | null;
}> = ({ mediator, currentUser }) => {
  const [message, setMessage] = useState('');
  const [isTyping, setIsTyping] = useState(false);
  const typingTimeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    class MessageInputManager implements ChatComponent {
      constructor(mediator: ChatMediator) {
        this.mediator = mediator;
        this.componentName = 'messageInput';
        this.mediator.registerComponent(this.componentName, this);
      }
      protected mediator: ChatMediator;
      protected componentName: string;
      initialize(): void {}
      notify(event: ChatEvent, data?: any): void {
        this.mediator.notify(this, event, data);
      }
      receive(event: ChatEvent, data?: any): void {}
      cleanup(): void {}
    }

    const manager = new MessageInputManager(mediator);
    return () => manager.cleanup();
  }, [mediator]);

  const handleTyping = (value: string) => {
    setMessage(value);

    if (!isTyping && value.trim() && currentUser) {
      setIsTyping(true);
      mediator.notify(mediator.getComponent('messageInput')!, 'user_typing', {
        userId: currentUser.id,
        isTyping: true
      });
    }

    if (typingTimeoutRef.current) {
      clearTimeout(typingTimeoutRef.current);
    }

    typingTimeoutRef.current = setTimeout(() => {
      if (isTyping && currentUser) {
        setIsTyping(false);
        mediator.notify(mediator.getComponent('messageInput')!, 'user_typing', {
          userId: currentUser.id,
          isTyping: false
        });
      }
    }, 1000);
  };

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    
    if (message.trim() && currentUser) {
      mediator.notify(mediator.getComponent('messageInput')!, 'message_sent', {
        content: message.trim(),
        sender: currentUser.name,
        type: 'text'
      });
      setMessage('');
      if (isTyping) {
        setIsTyping(false);
        mediator.notify(mediator.getComponent('messageInput')!, 'user_typing', {
          userId: currentUser.id,
          isTyping: false
        });
      }
    }
  };

  return (
    <div className="border-t bg-white p-4">
      <form onSubmit={handleSubmit} className="flex space-x-2">
        <input
          type="text"
          value={message}
          onChange={(e) => handleTyping(e.target.value)}
          placeholder="输入消息..."
          className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        />
        <button
          type="submit"
          disabled={!message.trim()}
          className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
        >
          发送
        </button>
      </form>
    </div>
  );
};

const UserListComponent: React.FC<{
  mediator: ClientChatMediator;
  currentUser: UserData | null;
}> = ({ mediator, currentUser }) => {
  const [users, setUsers] = useState<UserData[]>([]);
  const [typingUsers, setTypingUsers] = useState<Set<string>>(new Set());

  useEffect(() => {
    class UserListManager implements ChatComponent {
      constructor(mediator: ChatMediator) {
        this.mediator = mediator;
        this.componentName = 'userList';
        this.mediator.registerComponent(this.componentName, this);
      }
      protected mediator: ChatMediator;
      protected componentName: string;
      initialize(): void {}
      notify(event: ChatEvent, data?: any): void {
        this.mediator.notify(this, event, data);
      }
      receive(event: ChatEvent, data?: any): void {
        switch (event) {
          case 'user_list':
            setUsers(data);
            break;
          case 'user_joined':
            setUsers(prev => [...prev.filter(u => u.id !== data.id), data]);
            break;
          case 'user_left':
            setUsers(prev => prev.filter(u => u.id !== data.id));
            setTypingUsers(prev => {
              const newSet = new Set(prev);
              newSet.delete(data.id);
              return newSet;
            });
            break;
          case 'user_typing':
            setTypingUsers(prev => {
              const newSet = new Set(prev);
              if (data.isTyping) {
                newSet.add(data.userId);
              } else {
                newSet.delete(data.userId);
              }
              return newSet;
            });
            break;
        }
      }
      cleanup(): void {}
    }

    const manager = new UserListManager(mediator);
    if (currentUser) {
      mediator.notify(manager, 'user_joined', currentUser);
    }
    return () => manager.cleanup();
  }, [mediator, currentUser]);

  return (
    <div className="h-full flex flex-col">
      <div className="p-4 border-b bg-gray-50">
        <h3 className="font-semibold text-gray-800">在线用户 ({users.length})</h3>
      </div>
      <div className="flex-1 overflow-y-auto p-2">
        {users.map((user) => (
          <div
            key={user.id}
            className={`flex items-center space-x-2 p-2 rounded-lg mb-1 ${
              user.id === currentUser?.id ? 'bg-blue-50' : 'hover:bg-gray-50'
            }`}
          >
            <div className="w-8 h-8 bg-blue-500 rounded-full flex items-center justify-center text-white text-sm">
              {user.name.charAt(0).toUpperCase()}
            </div>
            <div className="flex-1">
              <div className="text-sm font-medium text-gray-800">
                {user.name}
                {user.id === currentUser?.id && ' (你)'}
              </div>
              {typingUsers.has(user.id) && (
                <div className="text-xs text-blue-500">正在输入...</div>
              )}
            </div>
            <div className={`w-2 h-2 rounded-full ${
              user.status === 'online' ? 'bg-green-500' : 'bg-gray-400'
            }`} />
          </div>
        ))}
      </div>
    </div>
  );
};

const NotificationComponent: React.FC<{
  mediator: ClientChatMediator;
}> = ({ mediator }) => {
  const [notifications, setNotifications] = useState<Array<{
    id: string;
    message: string;
    type: 'info' | 'warning' | 'error';
    timestamp: number;
  }>>([]);
  const [connectionStatus, setConnectionStatus] = useState<'connected' | 'disconnected' | 'connecting'>('connecting');

  useEffect(() => {
    class NotificationManager implements ChatComponent {
      constructor(mediator: ChatMediator) {
        this.mediator = mediator;
        this.componentName = 'notification';
        this.mediator.registerComponent(this.componentName, this);
      }
      protected mediator: ChatMediator;
      protected componentName: string;
      initialize(): void {}
      notify(event: ChatEvent, data?: any): void {
        this.mediator.notify(this, event, data);
      }
      receive(event: ChatEvent, data?: any): void {
        switch (event) {
          case 'connection_status_changed':
            setConnectionStatus(data.status === 'connected' ? 'connected' : 'disconnected');
            break;
          case 'user_joined':
            if (data.name) {
              this.addNotification(`${data.name} 加入了聊天室`, 'info');
            }
            break;
          case 'user_left':
            if (data.name) {
              this.addNotification(`${data.name} 离开了聊天室`, 'info');
            }
            break;
        }
      }
      private addNotification(message: string, type: 'info' | 'warning' | 'error'): void {
        const notification = {
          id: `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
          message,
          type,
          timestamp: Date.now()
        };
        setNotifications(prev => [...prev, notification]);
        setTimeout(() => {
          setNotifications(prev => prev.filter(n => n.id !== notification.id));
        }, 3000);
      }
      cleanup(): void {}
    }

    const manager = new NotificationManager(mediator);
    return () => manager.cleanup();
  }, [mediator]);

  const getStatusColor = () => {
    switch (connectionStatus) {
      case 'connected': return 'bg-green-500';
      case 'disconnected': return 'bg-red-500';
      case 'connecting': return 'bg-yellow-500';
      default: return 'bg-gray-500';
    }
  };

  const getStatusText = () => {
    switch (connectionStatus) {
      case 'connected': return '已连接';
      case 'disconnected': return '连接断开';
      case 'connecting': return '连接中...';
      default: return '未知状态';
    }
  };

  return (
    <div className="bg-white border-b">
      <div className="flex items-center justify-between px-4 py-2 bg-gray-50">
        <div className="flex items-center space-x-2">
          <div className={`w-2 h-2 rounded-full ${getStatusColor()}`} />
          <span className="text-sm text-gray-600">{getStatusText()}</span>
        </div>
        <div className="text-xs text-gray-500">聊天室</div>
      </div>
      {notifications.length > 0 && (
        <div className="p-2 space-y-1">
          {notifications.map((notification) => (
            <div
              key={notification.id}
              className={`px-3 py-2 rounded text-sm ${
                notification.type === 'info'
                  ? 'bg-blue-50 text-blue-800 border border-blue-200'
                  : notification.type === 'warning'
                  ? 'bg-yellow-50 text-yellow-800 border border-yellow-200'
                  : 'bg-red-50 text-red-800 border border-red-200'
              }`}
            >
              {notification.message}
            </div>
          ))}
        </div>
      )}
    </div>
  );
};

export default ChatApp;

5. 运行前端

sql 复制代码
npm start

运行项目

  1. 启动后端

    • 确保后端目录下运行npx tsc && node dist/server.js
    • 服务器将在http://localhost:3001运行。
  2. 启动前端

    • 在前端目录运行npm start
    • 浏览器将自动打开http://localhost:3000

实现效果

  • 消息发送与接收:用户发送的消息通过服务器广播给所有在线用户。
  • 用户状态:显示在线用户列表,支持"正在输入"状态提示。
  • 通知系统:用户加入/离开、连接状态变化会触发通知。
  • UI体验:Tailwind CSS提供响应式、现代化的界面,消息自动滚动,通知自动消失。

总结

仲裁者模式通过集中管理组件交互,显著降低了聊天室应用的耦合度。Socket.io的实时通信能力与TypeScript的类型安全结合,使得代码结构清晰、可维护。React和Tailwind CSS则为用户提供了流畅的交互体验。

相关推荐
天天摸鱼的java工程师2 小时前
SpringBoot + RabbitMQ + MySQL + XXL-Job:物流系统运单状态定时同步与异常订单重试
后端
粘豆煮包2 小时前
掀起你的盖头来之《数据库揭秘》-3-SQL 核心技能速成笔记-查询、过滤、排序、分组等
后端·mysql
子兮曰2 小时前
🚀前端依赖配置避坑指南:深度解析package.json中devDependencies的常见误解
前端·javascript·npm
瑶琴AI前端2 小时前
【零成本高效编程】VS Code必装的5款免费AI插件,开发效率飙升!
前端·ai编程·visual studio code
forever_Mamba2 小时前
实现一个高性能倒计时:从踩坑到最佳实践
前端·javascript
_AaronWong2 小时前
实现一个鼠标滚轮横向滚动需求
前端·electron
子兮曰2 小时前
浏览器与 Node.js 全局变量体系详解:从 window 到 global 的核心差异
前端·javascript·node.js
Olrookie2 小时前
ruoyi-vue(十五)——布局设置,导航栏,侧边栏,顶部栏
前端·vue.js·笔记
召摇2 小时前
API 设计最佳实践 Javascript 篇
前端·javascript·vue.js