仲裁者模式(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
运行项目
-
启动后端:
- 确保后端目录下运行
npx tsc && node dist/server.js
。 - 服务器将在
http://localhost:3001
运行。
- 确保后端目录下运行
-
启动前端:
- 在前端目录运行
npm start
。 - 浏览器将自动打开
http://localhost:3000
。
- 在前端目录运行
实现效果
- 消息发送与接收:用户发送的消息通过服务器广播给所有在线用户。
- 用户状态:显示在线用户列表,支持"正在输入"状态提示。
- 通知系统:用户加入/离开、连接状态变化会触发通知。
- UI体验:Tailwind CSS提供响应式、现代化的界面,消息自动滚动,通知自动消失。
总结
仲裁者模式通过集中管理组件交互,显著降低了聊天室应用的耦合度。Socket.io的实时通信能力与TypeScript的类型安全结合,使得代码结构清晰、可维护。React和Tailwind CSS则为用户提供了流畅的交互体验。