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则为用户提供了流畅的交互体验。

相关推荐
AlienZHOU2 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
狗头大军之江苏分军4 小时前
《潮水漫过十七岁》开学了
后端
苏三说技术5 小时前
如何看待GPT-6在UP主众测中碾压夺冠?它是现在最强大模型吗?
后端
mldong5 小时前
一份 JSON,一条能跑的审批流:把报销流程送上工作流引擎
后端·架构
wno7045 小时前
Spring Boot WebFlux增删改查
java·spring boot·后端
Captaincc5 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
aramae6 小时前
模拟实现strlen()函数 (C语言)
c语言·开发语言·后端
计算机魔术师7 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen7 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒7 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端