WebSocket 示例
使用 WebSocket 协议的实时通信示例,包括聊天、通知和实时数据流
💻 JavaScript WebSocket 客户端 javascript
🟢 simple
⭐⭐
具有重连、心跳和消息处理的完整 WebSocket 客户端实现
⏱️ 20 min
🏷️ websocket, javascript, real-time, client
Prerequisites:
JavaScript basics, WebSocket protocol
// WebSocket Client Implementation with Reconnection and Heartbeat
class WebSocketClient {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 3000,
maxReconnectAttempts: 5,
heartbeatInterval: 30000,
heartbeatTimeout: 5000,
debug: false,
...options
};
this.ws = null;
this.reconnectAttempts = 0;
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
this.messageQueue = [];
this.eventListeners = new Map();
this.isManualClose = false;
this.connect();
}
connect() {
try {
this.log('Connecting to WebSocket...');
this.ws = new WebSocket(this.url);
this.ws.onopen = (event) => {
this.log('WebSocket connected');
this.reconnectAttempts = 0;
this.startHeartbeat();
this.flushMessageQueue();
this.emit('open', event);
};
this.ws.onmessage = (event) => {
this.log('Message received:', event.data);
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (error) {
this.log('Error parsing message:', error);
this.emit('error', new Error('Invalid JSON message'));
}
// Handle heartbeat response
if (event.data === 'pong') {
this.handleHeartbeatResponse();
}
this.emit('message', event);
};
this.ws.onerror = (event) => {
this.log('WebSocket error:', event);
this.emit('error', event);
};
this.ws.onclose = (event) => {
this.log('WebSocket closed:', event.code, event.reason);
this.stopHeartbeat();
this.emit('close', event);
if (!this.isManualClose && this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.scheduleReconnect();
}
};
} catch (error) {
this.log('Connection error:', error);
this.emit('error', error);
this.scheduleReconnect();
}
}
disconnect() {
this.isManualClose = true;
this.stopHeartbeat();
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.close(1000, 'Manual disconnect');
}
}
send(data) {
const message = typeof data === 'string' ? data : JSON.stringify(data);
if (this.isConnected()) {
this.ws.send(message);
this.log('Message sent:', message);
} else {
this.log('WebSocket not connected, queuing message');
this.messageQueue.push(message);
}
}
isConnected() {
return this.ws && this.ws.readyState === WebSocket.OPEN;
}
on(event, callback) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, []);
}
this.eventListeners.get(event).push(callback);
}
off(event, callback) {
if (this.eventListeners.has(event)) {
const listeners = this.eventListeners.get(event);
const index = listeners.indexOf(callback);
if (index > -1) {
listeners.splice(index, 1);
}
}
}
emit(event, ...args) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach(callback => {
try {
callback(...args);
} catch (error) {
this.log('Error in event listener:', error);
}
});
}
}
// Heartbeat mechanism
startHeartbeat() {
this.heartbeatTimer = setInterval(() => {
if (this.isConnected()) {
this.ws.send('ping');
this.heartbeatTimeoutTimer = setTimeout(() => {
this.log('Heartbeat timeout, closing connection');
this.ws.close(1000, 'Heartbeat timeout');
}, this.options.heartbeatTimeout);
}
}, this.options.heartbeatInterval);
}
stopHeartbeat() {
if (this.heartbeatTimer) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
handleHeartbeatResponse() {
if (this.heartbeatTimeoutTimer) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
// Reconnection logic
scheduleReconnect() {
this.reconnectAttempts++;
const delay = Math.min(
this.options.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1),
30000
);
this.log(`Scheduling reconnect attempt ${this.reconnectAttempts} in ${delay}ms`);
setTimeout(() => {
this.connect();
}, delay);
}
// Message queue for offline messages
flushMessageQueue() {
while (this.messageQueue.length > 0 && this.isConnected()) {
const message = this.messageQueue.shift();
this.ws.send(message);
}
}
// Message handling with type routing
handleMessage(message) {
if (message.type && this.eventListeners.has(message.type)) {
this.emit(message.type, message.data);
}
}
log(...args) {
if (this.options.debug) {
console.log('[WebSocketClient]', ...args);
}
}
// Get connection state
getState() {
if (!this.ws) return 'DISCONNECTED';
switch (this.ws.readyState) {
case WebSocket.CONNECTING: return 'CONNECTING';
case WebSocket.OPEN: return 'CONNECTED';
case WebSocket.CLOSING: return 'CLOSING';
case WebSocket.CLOSED: return 'DISCONNECTED';
default: return 'UNKNOWN';
}
}
// Get statistics
getStats() {
return {
state: this.getState(),
reconnectAttempts: this.reconnectAttempts,
queuedMessages: this.messageQueue.length,
url: this.url
};
}
}
// Usage Examples
// Example 1: Basic WebSocket client
const basicClient = new WebSocketClient('wss://echo.websocket.org', {
debug: true,
reconnectInterval: 2000,
maxReconnectAttempts: 10
});
basicClient.on('open', () => {
console.log('Connected to echo server');
basicClient.send({
type: 'message',
data: 'Hello, WebSocket!'
});
});
basicClient.on('message', (event) => {
console.log('Received:', event.data);
});
basicClient.on('error', (error) => {
console.error('WebSocket error:', error);
});
// Example 2: Chat client
class ChatClient extends WebSocketClient {
constructor(url, username) {
super(url, { debug: true });
this.username = username;
this.on('open', () => {
this.send({
type: 'join',
data: { username: this.username }
});
});
this.on('chat_message', (data) => {
console.log(`${data.username}: ${data.message}`);
});
this.on('user_joined', (data) => {
console.log(`${data.username} joined the chat`);
});
this.on('user_left', (data) => {
console.log(`${data.username} left the chat`);
});
}
sendMessage(message) {
this.send({
type: 'chat_message',
data: {
username: this.username,
message: message,
timestamp: new Date().toISOString()
}
});
}
}
// Example 3: Real-time notifications client
class NotificationClient extends WebSocketClient {
constructor(url, userId) {
super(url, { debug: true });
this.userId = userId;
this.notifications = [];
this.on('open', () => {
this.send({
type: 'authenticate',
data: { userId: this.userId }
});
});
this.on('notification', (data) => {
this.notifications.push(data);
this.showNotification(data);
});
}
showNotification(notification) {
if (Notification.permission === 'granted') {
new Notification(notification.title, {
body: notification.body,
icon: notification.icon,
tag: notification.id
});
}
console.log('Notification:', notification);
}
getUnreadCount() {
return this.notifications.filter(n => !n.read).length;
}
markAsRead(notificationId) {
const notification = this.notifications.find(n => n.id === notificationId);
if (notification) {
notification.read = true;
}
}
}
// Request notification permission
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission();
}
// Export classes for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { WebSocketClient, ChatClient, NotificationClient };
}
💻 Node.js WebSocket 服务器 javascript
🟡 intermediate
⭐⭐⭐⭐
包含 Express、Socket.IO 和房间管理的完整 WebSocket 服务器实现
⏱️ 30 min
🏷️ websocket, nodejs, server, real-time
Prerequisites:
Node.js, Express, Socket.IO, JWT
// WebSocket Server Implementation with Socket.IO and Express
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');
const app = express();
const server = http.createServer(app);
// Configure Socket.IO with CORS and options
const io = socketIo(server, {
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
methods: ['GET', 'POST'],
credentials: true
},
pingTimeout: 60000,
pingInterval: 25000
});
// Rate limiting for HTTP endpoints
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use(limiter);
app.use(express.json());
// In-memory storage (replace with database in production)
const users = new Map();
const rooms = new Map();
const connectedSockets = new Map();
// JWT Secret
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// Middleware for Socket.IO authentication
io.use(async (socket, next) => {
try {
const token = socket.handshake.auth.token || socket.handshake.headers.authorization?.replace('Bearer ', '');
if (!token) {
return next(new Error('Authentication token required'));
}
const decoded = jwt.verify(token, JWT_SECRET);
socket.userId = decoded.userId;
socket.username = decoded.username;
next();
} catch (error) {
next(new Error('Invalid authentication token'));
}
});
// Socket.IO connection handling
io.on('connection', (socket) => {
console.log(`User connected: ${socket.username} (${socket.userId})`);
// Store socket connection
connectedSockets.set(socket.userId, socket);
// Join user to their personal room
const personalRoom = `user_${socket.userId}`;
socket.join(personalRoom);
// Initialize user data
users.set(socket.userId, {
id: socket.userId,
username: socket.username,
socketId: socket.id,
connectedAt: new Date(),
rooms: [personalRoom],
status: 'online'
});
// Notify others about user coming online
socket.broadcast.emit('user_status_change', {
userId: socket.userId,
username: socket.username,
status: 'online'
});
// Handle joining rooms
socket.on('join_room', (data) => {
const { roomId, password } = data;
if (!rooms.has(roomId)) {
return socket.emit('error', { message: 'Room not found' });
}
const room = rooms.get(roomId);
// Check password protection
if (room.password && room.password !== password) {
return socket.emit('error', { message: 'Invalid room password' });
}
// Join room
socket.join(roomId);
users.get(socket.userId).rooms.push(roomId);
// Update room participants
if (!room.participants.includes(socket.userId)) {
room.participants.push(socket.userId);
}
socket.emit('joined_room', { roomId, roomName: room.name });
// Notify other room participants
socket.to(roomId).emit('user_joined_room', {
userId: socket.userId,
username: socket.username,
roomId
});
// Send room info to the user
socket.emit('room_info', {
roomId,
name: room.name,
participants: room.participants.map(pid => users.get(pid)?.username).filter(Boolean)
});
});
// Handle leaving rooms
socket.on('leave_room', (data) => {
const { roomId } = data;
socket.leave(roomId);
const userData = users.get(socket.userId);
if (userData) {
userData.rooms = userData.rooms.filter(room => room !== roomId);
}
const room = rooms.get(roomId);
if (room) {
room.participants = room.participants.filter(pid => pid !== socket.userId);
// Notify other room participants
socket.to(roomId).emit('user_left_room', {
userId: socket.userId,
username: socket.username,
roomId
});
// Delete room if empty and not persistent
if (room.participants.length === 0 && !room.persistent) {
rooms.delete(roomId);
}
}
socket.emit('left_room', { roomId });
});
// Handle chat messages
socket.on('chat_message', (data) => {
const { roomId, message, type = 'text' } = data;
const messageData = {
id: generateId(),
userId: socket.userId,
username: socket.username,
message: sanitizeMessage(message),
type,
timestamp: new Date().toISOString(),
roomId
};
if (roomId) {
// Send to specific room
socket.to(roomId).emit('chat_message', messageData);
} else {
// Send to all connected users
socket.broadcast.emit('chat_message', messageData);
}
// Send confirmation to sender
socket.emit('message_sent', messageData);
// Store message in room history
if (roomId && rooms.has(roomId)) {
const room = rooms.get(roomId);
if (!room.messages) room.messages = [];
room.messages.push(messageData);
// Limit message history
if (room.messages.length > 1000) {
room.messages = room.messages.slice(-1000);
}
}
});
// Handle private messages
socket.on('private_message', (data) => {
const { targetUserId, message } = data;
if (!users.has(targetUserId)) {
return socket.emit('error', { message: 'User not found' });
}
const messageData = {
id: generateId(),
userId: socket.userId,
username: socket.username,
message: sanitizeMessage(message),
timestamp: new Date().toISOString(),
private: true
};
// Send to target user
const targetSocket = connectedSockets.get(targetUserId);
if (targetSocket) {
targetSocket.emit('private_message', messageData);
}
// Send confirmation to sender
socket.emit('message_sent', messageData);
});
// Handle typing indicators
socket.on('typing_start', (data) => {
const { roomId } = data;
if (roomId) {
socket.to(roomId).emit('user_typing', {
userId: socket.userId,
username: socket.username,
typing: true
});
}
});
socket.on('typing_stop', (data) => {
const { roomId } = data;
if (roomId) {
socket.to(roomId).emit('user_typing', {
userId: socket.userId,
username: socket.username,
typing: false
});
}
});
// Handle room creation
socket.on('create_room', (data) => {
const { name, password, persistent = false } = data;
const roomId = generateId();
rooms.set(roomId, {
id: roomId,
name: sanitizeInput(name),
password: password || null,
persistent,
createdBy: socket.userId,
createdAt: new Date(),
participants: [socket.userId],
messages: []
});
socket.emit('room_created', { roomId, name });
// Auto-join creator to the room
socket.emit('join_room', { roomId });
});
// Handle get room list
socket.on('get_rooms', () => {
const roomList = Array.from(rooms.values())
.filter(room => !room.password || room.participants.includes(socket.userId))
.map(room => ({
id: room.id,
name: room.name,
participantCount: room.participants.length,
hasPassword: !!room.password,
persistent: room.persistent
}));
socket.emit('rooms_list', roomList);
});
// Handle get online users
socket.on('get_online_users', () => {
const onlineUsers = Array.from(users.values())
.filter(user => user.id !== socket.userId)
.map(user => ({
id: user.id,
username: user.username,
status: user.status
}));
socket.emit('online_users', onlineUsers);
});
// Handle disconnection
socket.on('disconnect', (reason) => {
console.log(`User disconnected: ${socket.username} (${reason})`);
// Clean up
connectedSockets.delete(socket.userId);
const userData = users.get(socket.userId);
if (userData) {
// Leave all rooms
userData.rooms.forEach(roomId => {
if (roomId !== `user_${socket.userId}`) {
const room = rooms.get(roomId);
if (room) {
room.participants = room.participants.filter(pid => pid !== socket.userId);
// Notify room participants
socket.to(roomId).emit('user_left_room', {
userId: socket.userId,
username: socket.username,
roomId
});
// Delete empty non-persistent rooms
if (room.participants.length === 0 && !room.persistent) {
rooms.delete(roomId);
}
}
}
});
users.delete(socket.userId);
}
// Notify others about user going offline
socket.broadcast.emit('user_status_change', {
userId: socket.userId,
username: socket.username,
status: 'offline'
});
});
});
// HTTP Routes
app.get('/health', (req, res) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
connectedUsers: users.size,
activeRooms: rooms.size
});
});
app.get('/api/rooms', (req, res) => {
const roomList = Array.from(rooms.values()).map(room => ({
id: room.id,
name: room.name,
participantCount: room.participants.length,
hasPassword: !!room.password,
persistent: room.persistent,
createdAt: room.createdAt
}));
res.json(roomList);
});
app.get('/api/users/online', (req, res) => {
const onlineUsers = Array.from(users.values()).map(user => ({
id: user.id,
username: user.username,
status: user.status,
connectedAt: user.connectedAt
}));
res.json(onlineUsers);
});
// Utility functions
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
function sanitizeInput(input) {
return input.replace(/<[^>]*>/g, '').trim();
}
function sanitizeMessage(message) {
// Basic sanitization - in production, use a proper HTML sanitizer
return sanitizeInput(message);
}
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
process.on('SIGINT', () => {
console.log('SIGINT received, shutting down gracefully');
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});
// Start server
const PORT = process.env.PORT || 3001;
server.listen(PORT, () => {
console.log(`WebSocket server running on port ${PORT}`);
});
module.exports = { app, server, io };
💻 TypeScript WebSocket 类型和接口 typescript
🟡 intermediate
⭐⭐⭐
WebSocket 应用程序的完整 TypeScript 类型定义和接口
⏱️ 25 min
🏷️ typescript, websocket, types, definitions
Prerequisites:
TypeScript, WebSocket concepts
// WebSocket Type Definitions and Interfaces for TypeScript
// Base WebSocket message types
export interface BaseMessage {
id: string;
timestamp: string;
type: string;
}
// Chat message types
export interface ChatMessage extends BaseMessage {
type: 'chat_message';
data: {
userId: string;
username: string;
message: string;
roomId?: string;
private?: boolean;
messageType: 'text' | 'image' | 'file' | 'emoji';
metadata?: {
fileName?: string;
fileSize?: number;
fileUrl?: string;
imageUrl?: string;
};
};
}
export interface PrivateMessage extends BaseMessage {
type: 'private_message';
data: {
userId: string;
username: string;
targetUserId: string;
targetUsername: string;
message: string;
messageType: 'text' | 'image' | 'file' | 'emoji';
};
}
// System notification types
export interface SystemNotification extends BaseMessage {
type: 'system_notification';
data: {
title: string;
message: string;
level: 'info' | 'warning' | 'error' | 'success';
icon?: string;
actionUrl?: string;
actionText?: string;
};
}
// User management types
export interface UserStatusChange extends BaseMessage {
type: 'user_status_change';
data: {
userId: string;
username: string;
status: 'online' | 'offline' | 'away' | 'busy';
lastSeen?: string;
};
}
export interface UserJoinedRoom extends BaseMessage {
type: 'user_joined_room';
data: {
userId: string;
username: string;
roomId: string;
roomName: string;
};
}
export interface UserLeftRoom extends BaseMessage {
type: 'user_left_room';
data: {
userId: string;
username: string;
roomId: string;
roomName: string;
};
}
// Room management types
export interface RoomInfo {
id: string;
name: string;
description?: string;
participantCount: number;
maxParticipants?: number;
hasPassword: boolean;
persistent: boolean;
isPrivate: boolean;
createdBy: string;
createdAt: string;
lastActivity: string;
tags: string[];
}
export interface RoomInfoResponse extends BaseMessage {
type: 'room_info';
data: RoomInfo & {
participants: Array<{
userId: string;
username: string;
role: 'owner' | 'admin' | 'moderator' | 'member';
joinedAt: string;
}>;
permissions: {
canSendMessages: boolean;
canSendFiles: boolean;
canInviteUsers: boolean;
canManageRoom: boolean;
};
};
}
export interface RoomListResponse extends BaseMessage {
type: 'rooms_list';
data: {
rooms: RoomInfo[];
totalCount: number;
hasMore: boolean;
};
}
// Typing indicators
export interface UserTyping extends BaseMessage {
type: 'user_typing';
data: {
userId: string;
username: string;
roomId?: string;
typing: boolean;
};
}
// File sharing types
export interface FileShareMessage extends BaseMessage {
type: 'file_share';
data: {
userId: string;
username: string;
fileName: string;
fileSize: number;
fileUrl: string;
mimeType: string;
roomId?: string;
private?: boolean;
thumbnailUrl?: string;
downloadUrl: string;
previewUrl?: string;
};
}
// Voice/Video call types
export interface CallInvitation extends BaseMessage {
type: 'call_invitation';
data: {
callId: string;
callerId: string;
callerUsername: string;
targetUserId: string;
targetUsername: string;
callType: 'audio' | 'video';
roomId?: string;
};
}
export interface CallResponse extends BaseMessage {
type: 'call_response';
data: {
callId: string;
response: 'accept' | 'decline' | 'busy';
userId: string;
username: string;
};
}
export interface CallEnd extends BaseMessage {
type: 'call_end';
data: {
callId: string;
endedBy: string;
endedByUsername: string;
duration: number; // in seconds
reason: 'ended' | 'rejected' | 'failed' | 'timeout';
};
}
// Authentication and authorization types
export interface AuthRequest {
type: 'authenticate';
data: {
token: string;
userId?: string;
deviceId?: string;
};
}
export interface AuthResponse extends BaseMessage {
type: 'auth_response';
data: {
success: boolean;
userId: string;
username: string;
permissions: string[];
sessionExpiresAt: string;
};
}
// Error handling types
export interface ErrorMessage extends BaseMessage {
type: 'error';
data: {
code: string;
message: string;
details?: any;
retryable: boolean;
};
}
// Presence and activity types
export interface UserActivity extends BaseMessage {
type: 'user_activity';
data: {
userId: string;
username: string;
activity: {
type: 'listening' | 'watching' | 'playing' | 'working';
details: string;
expiresAt?: string;
};
};
}
// Reaction types
export interface MessageReaction extends BaseMessage {
type: 'message_reaction';
data: {
messageId: string;
userId: string;
username: string;
reaction: string; // emoji
action: 'add' | 'remove';
};
}
// Moderation types
export interface MessageModeration extends BaseMessage {
type: 'message_moderation';
data: {
messageId: string;
moderatedBy: string;
moderatedByUsername: string;
action: 'delete' | 'edit' | 'warn';
reason?: string;
originalContent?: string;
};
}
// Union type for all possible messages
export type WebSocketMessage =
| ChatMessage
| PrivateMessage
| SystemNotification
| UserStatusChange
| UserJoinedRoom
| UserLeftRoom
| RoomInfoResponse
| RoomListResponse
| UserTyping
| FileShareMessage
| CallInvitation
| CallResponse
| CallEnd
| AuthResponse
| ErrorMessage
| UserActivity
| MessageReaction
| MessageModeration;
// Client configuration
export interface WebSocketClientConfig {
url: string;
protocols?: string[];
reconnectInterval?: number;
maxReconnectAttempts?: number;
heartbeatInterval?: number;
heartbeatTimeout?: number;
debug?: boolean;
authentication?: {
token?: string;
type?: 'bearer' | 'custom';
};
}
// Room creation data
export interface CreateRoomData {
name: string;
description?: string;
password?: string;
maxParticipants?: number;
isPrivate?: boolean;
persistent?: boolean;
tags?: string[];
}
// Room join data
export interface JoinRoomData {
roomId: string;
password?: string;
}
// Chat message data
export interface SendMessageData {
message: string;
roomId?: string;
targetUserId?: string;
messageType?: 'text' | 'image' | 'file' | 'emoji';
metadata?: {
fileName?: string;
fileSize?: number;
fileUrl?: string;
imageUrl?: string;
};
}
// File upload data
export interface FileUploadData {
file: File | Blob;
fileName?: string;
roomId?: string;
targetUserId?: string;
description?: string;
}
// Statistics and monitoring types
export interface WebSocketStats {
connectionState: 'connecting' | 'connected' | 'disconnecting' | 'disconnected';
reconnectAttempts: number;
messagesReceived: number;
messagesSent: number;
bytesReceived: number;
bytesSent: number;
latency: number; // in milliseconds
connectedAt?: string;
lastMessageAt?: string;
}
export interface RoomStats {
roomId: string;
participantCount: number;
messageCount: number;
fileCount: number;
lastActivity: string;
createdAt: string;
}
// Event handler types
export type WebSocketEventHandler<T = WebSocketMessage> = (data: T) => void;
export type WebSocketErrorEventHandler = (error: Error) => void;
export type WebSocketCloseEventHandler = (event: CloseEvent) => void;
export type WebSocketOpenEventHandler = (event: Event) => void;
// Advanced client interface
export interface IWebSocketClient {
connect(): Promise<void>;
disconnect(): void;
send<T extends WebSocketMessage>(message: T): void;
on<T extends WebSocketMessage['type']>(
eventType: T,
handler: (data: Extract<WebSocketMessage, { type: T }>) => void
): void;
off<T extends WebSocketMessage['type']>(
eventType: T,
handler: (data: Extract<WebSocketMessage, { type: T }>) => void
): void;
once<T extends WebSocketMessage['type']>(
eventType: T,
handler: (data: Extract<WebSocketMessage, { type: T }>) => void
): void;
getStats(): WebSocketStats;
getState(): string;
isConnected(): boolean;
}
// Room management interface
export interface IRoomManager {
createRoom(data: CreateRoomData): Promise<string>;
joinRoom(data: JoinRoomData): Promise<void>;
leaveRoom(roomId: string): Promise<void>;
getRoomList(): Promise<RoomInfo[]>;
getRoomInfo(roomId: string): Promise<RoomInfoResponse['data']>;
inviteToRoom(roomId: string, userIds: string[]): Promise<void>;
kickFromRoom(roomId: string, userIds: string[]): Promise<void>;
}
// Message persistence interface
export interface IMessagePersistence {
saveMessage(message: ChatMessage): Promise<void>;
getMessageHistory(roomId?: string, limit?: number, offset?: number): Promise<ChatMessage[]>;
searchMessages(query: string, roomId?: string): Promise<ChatMessage[]>;
deleteMessage(messageId: string): Promise<void>;
editMessage(messageId: string, newContent: string): Promise<void>;
}
// User presence interface
export interface IUserPresence {
updateStatus(status: 'online' | 'offline' | 'away' | 'busy'): void;
setActivity(activity: UserActivity['data']['activity']): void;
getOnlineUsers(): Promise<Array<{ id: string; username: string; status: string }>>;
subscribeToUserStatus(userId: string): void;
unsubscribeFromUserStatus(userId: string): void;
}
// File sharing interface
export interface IFileSharing {
uploadFile(data: FileUploadData): Promise<string>; // Returns file URL
downloadFile(fileId: string): Promise<Blob>;
deleteFile(fileId: string): Promise<void>;
getFileMetadata(fileId: string): Promise<FileShareMessage['data']>;
}
// Type guards for message type discrimination
export function isChatMessage(message: WebSocketMessage): message is ChatMessage {
return message.type === 'chat_message';
}
export function isPrivateMessage(message: WebSocketMessage): message is PrivateMessage {
return message.type === 'private_message';
}
export function isSystemNotification(message: WebSocketMessage): message is SystemNotification {
return message.type === 'system_notification';
}
export function isUserStatusChange(message: WebSocketMessage): message is UserStatusChange {
return message.type === 'user_status_change';
}
export function isErrorMessage(message: WebSocketMessage): message is ErrorMessage {
return message.type === 'error';
}
export function isCallInvitation(message: WebSocketMessage): message is CallInvitation {
return message.type === 'call_invitation';
}
// Utility functions
export function createChatMessage(
data: ChatMessage['data']
): ChatMessage {
return {
id: generateId(),
timestamp: new Date().toISOString(),
type: 'chat_message',
data
};
}
export function createPrivateMessage(
data: PrivateMessage['data']
): PrivateMessage {
return {
id: generateId(),
timestamp: new Date().toISOString(),
type: 'private_message',
data
};
}
export function createSystemNotification(
title: string,
message: string,
level: SystemNotification['data']['level'] = 'info'
): SystemNotification {
return {
id: generateId(),
timestamp: new Date().toISOString(),
type: 'system_notification',
data: { title, message, level }
};
}
function generateId(): string {
return Math.random().toString(36).substr(2, 9) + Date.now().toString(36);
}
// Export all types for easy importing
export * from './socket-io-types'; // If using Socket.IO specific types
💻 React WebSocket 聊天组件 typescript
🟡 intermediate
⭐⭐⭐⭐
具有 WebSocket 集成和实时消息传递的完整 React 聊天应用程序
⏱️ 35 min
🏷️ react, websocket, chat, typescript, ui
Prerequisites:
React, TypeScript, WebSocket
// React WebSocket Chat Application
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { WebSocketClient } from './websocket-client';
import type { ChatMessage, UserStatusChange, UserTyping } from './websocket-types';
interface ChatRoom {
id: string;
name: string;
participantCount: number;
hasPassword: boolean;
}
interface User {
id: string;
username: string;
status: 'online' | 'offline' | 'away' | 'busy';
lastSeen?: string;
}
interface ChatState {
messages: ChatMessage[];
users: User[];
rooms: ChatRoom[];
currentRoom: string | null;
isConnected: boolean;
typingUsers: Set<string>;
}
// Chat component
export const ChatApp: React.FC<{ wsUrl: string; authToken: string; username: string }> = ({
wsUrl,
authToken,
username
}) => {
const [state, setState] = useState<ChatState>({
messages: [],
users: [],
rooms: [],
currentRoom: null,
isConnected: false,
typingUsers: new Set()
});
const [inputMessage, setInputMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [showRoomList, setShowRoomList] = useState(false);
const wsClientRef = useRef<WebSocketClient | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const typingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Initialize WebSocket connection
useEffect(() => {
if (!wsUrl || !authToken) return;
const client = new WebSocketClient(wsUrl, {
debug: process.env.NODE_ENV === 'development',
authentication: {
token: authToken,
type: 'bearer'
}
});
wsClientRef.current = client;
// Event handlers
const handleOpen = () => {
setState(prev => ({ ...prev, isConnected: true }));
};
const handleClose = () => {
setState(prev => ({ ...prev, isConnected: false }));
};
const handleError = (error: Error) => {
console.error('WebSocket error:', error);
// Could add toast notification here
};
const handleChatMessage = (message: ChatMessage) => {
setState(prev => ({
...prev,
messages: [...prev.messages, message]
}));
scrollToBottom();
};
const handleUserStatusChange = (data: UserStatusChange['data']) => {
setState(prev => ({
...prev,
users: prev.users.map(user =>
user.id === data.userId
? { ...user, status: data.status, lastSeen: data.lastSeen }
: user
)
}));
};
const handleUserTyping = (data: UserTyping['data']) => {
setState(prev => {
const newTypingUsers = new Set(prev.typingUsers);
if (data.typing) {
newTypingUsers.add(data.username);
} else {
newTypingUsers.delete(data.username);
}
return { ...prev, typingUsers: newTypingUsers };
});
};
const handleRoomsList = (rooms: ChatRoom[]) => {
setState(prev => ({ ...prev, rooms }));
};
const handleOnlineUsers = (users: User[]) => {
setState(prev => ({ ...prev, users }));
};
// Register event listeners
client.on('open', handleOpen);
client.on('close', handleClose);
client.on('error', handleError);
client.on('chat_message', handleChatMessage);
client.on('user_status_change', (msg: any) => handleUserStatusChange(msg.data));
client.on('user_typing', (msg: any) => handleUserTyping(msg.data));
client.on('rooms_list', (msg: any) => handleRoomsList(msg.data.rooms));
client.on('online_users', (msg: any) => handleOnlineUsers(msg.data));
// Request initial data
client.on('open', () => {
client.send({ type: 'get_rooms' });
client.send({ type: 'get_online_users' });
});
return () => {
client.off('open', handleOpen);
client.off('close', handleClose);
client.off('error', handleError);
client.off('chat_message', handleChatMessage);
client.off('user_status_change', (msg: any) => handleUserStatusChange(msg.data));
client.off('user_typing', (msg: any) => handleUserTyping(msg.data));
client.off('rooms_list', (msg: any) => handleRoomsList(msg.data.rooms));
client.off('online_users', (msg: any) => handleOnlineUsers(msg.data));
client.disconnect();
};
}, [wsUrl, authToken]);
// Auto-scroll to bottom on new messages
const scrollToBottom = useCallback(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, []);
// Send message
const sendMessage = useCallback(() => {
if (!inputMessage.trim() || !wsClientRef.current) return;
const messageData = {
type: 'chat_message',
data: {
userId: '', // Will be filled by server
username,
message: inputMessage.trim(),
roomId: state.currentRoom || undefined,
messageType: 'text' as const
}
};
wsClientRef.current.send(messageData);
setInputMessage('');
stopTyping();
}, [inputMessage, username, state.currentRoom]);
// Handle typing indicators
const startTyping = useCallback(() => {
if (isTyping || !wsClientRef.current) return;
setIsTyping(true);
wsClientRef.current.send({
type: 'typing_start',
data: { roomId: state.currentRoom }
});
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
}
typingTimeoutRef.current = setTimeout(() => {
stopTyping();
}, 3000);
}, [isTyping, state.currentRoom]);
const stopTyping = useCallback(() => {
if (!isTyping || !wsClientRef.current) return;
setIsTyping(false);
wsClientRef.current.send({
type: 'typing_stop',
data: { roomId: state.currentRoom }
});
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = null;
}
}, [isTyping, state.currentRoom]);
// Join room
const joinRoom = useCallback((roomId: string) => {
if (!wsClientRef.current) return;
wsClientRef.current.send({
type: 'join_room',
data: { roomId }
});
setState(prev => ({ ...prev, currentRoom: roomId }));
setShowRoomList(false);
}, []);
// Leave room
const leaveRoom = useCallback(() => {
if (!wsClientRef.current || !state.currentRoom) return;
wsClientRef.current.send({
type: 'leave_room',
data: { roomId: state.currentRoom }
});
setState(prev => ({ ...prev, currentRoom: null }));
}, [state.currentRoom]);
// Handle input change
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setInputMessage(e.target.value);
if (e.target.value.trim()) {
startTyping();
} else {
stopTyping();
}
}, [startTyping, stopTyping]);
// Handle key press
const handleKeyPress = useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}, [sendMessage]);
// Get current room info
const currentRoomInfo = state.rooms.find(room => room.id === state.currentRoom);
return (
<div className="chat-app">
{/* Header */}
<div className="chat-header">
<div className="connection-status">
<span className={`status-indicator ${state.isConnected ? 'connected' : 'disconnected'}`} />
{state.isConnected ? 'Connected' : 'Disconnected'}
</div>
<div className="current-room">
{currentRoomInfo ? currentRoomInfo.name : 'General Chat'}
{state.currentRoom && (
<button onClick={leaveRoom} className="leave-room-btn">
Leave Room
</button>
)}
</div>
<div className="user-info">
{username}
</div>
</div>
<div className="chat-body">
{/* Sidebar */}
<div className="chat-sidebar">
<div className="sidebar-section">
<h3>Rooms</h3>
<button onClick={() => setShowRoomList(!showRoomList)}>
{showRoomList ? 'Hide' : 'Show'} Rooms
</button>
{showRoomList && (
<div className="room-list">
<div
className={`room-item ${!state.currentRoom ? 'active' : ''}`}
onClick={() => setState(prev => ({ ...prev, currentRoom: null }))}
>
General Chat
</div>
{state.rooms.map(room => (
<div
key={room.id}
className={`room-item ${state.currentRoom === room.id ? 'active' : ''}`}
onClick={() => joinRoom(room.id)}
>
<span>{room.name}</span>
<span className="participant-count">
{room.participantCount}
</span>
</div>
))}
</div>
)}
</div>
<div className="sidebar-section">
<h3>Online Users</h3>
<div className="user-list">
{state.users.map(user => (
<div key={user.id} className="user-item">
<span className={`user-status ${user.status}`} />
<span>{user.username}</span>
</div>
))}
</div>
</div>
</div>
{/* Main chat area */}
<div className="chat-main">
{/* Messages */}
<div className="messages-container">
{state.messages.map((message, index) => (
<div key={message.id || index} className="message">
<div className="message-header">
<span className="message-username">
{message.data.username}
</span>
<span className="message-timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="message-content">
{message.data.message}
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Typing indicator */}
{state.typingUsers.size > 0 && (
<div className="typing-indicator">
{Array.from(state.typingUsers).join(', ')} {state.typingUsers.size === 1 ? 'is' : 'are'} typing...
</div>
)}
{/* Message input */}
<div className="message-input-container">
<input
type="text"
className="message-input"
placeholder="Type a message..."
value={inputMessage}
onChange={handleInputChange}
onKeyPress={handleKeyPress}
disabled={!state.isConnected}
/>
<button
className="send-button"
onClick={sendMessage}
disabled={!inputMessage.trim() || !state.isConnected}
>
Send
</button>
</div>
</div>
</div>
<style jsx>{`
.chat-app {
display: flex;
flex-direction: column;
height: 100vh;
max-width: 1200px;
margin: 0 auto;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.connection-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.status-indicator {
width: 10px;
height: 10px;
border-radius: 50%;
}
.status-indicator.connected {
background: #4caf50;
}
.status-indicator.disconnected {
background: #f44336;
}
.chat-body {
display: flex;
flex: 1;
overflow: hidden;
}
.chat-sidebar {
width: 250px;
background: #fafafa;
border-right: 1px solid #ddd;
overflow-y: auto;
}
.sidebar-section {
padding: 1rem;
border-bottom: 1px solid #eee;
}
.room-list, .user-list {
margin-top: 0.5rem;
}
.room-item, .user-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem;
cursor: pointer;
border-radius: 4px;
margin-bottom: 0.25rem;
}
.room-item:hover, .user-item:hover {
background: #e3f2fd;
}
.room-item.active {
background: #2196f3;
color: white;
}
.user-status {
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 0.5rem;
}
.user-status.online { background: #4caf50; }
.user-status.offline { background: #9e9e9e; }
.user-status.away { background: #ff9800; }
.user-status.busy { background: #f44336; }
.chat-main {
flex: 1;
display: flex;
flex-direction: column;
}
.messages-container {
flex: 1;
padding: 1rem;
overflow-y: auto;
background: white;
}
.message {
margin-bottom: 1rem;
padding: 0.5rem;
border-radius: 8px;
background: #f9f9f9;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.25rem;
font-size: 0.875rem;
}
.message-username {
font-weight: bold;
color: #2196f3;
}
.message-timestamp {
color: #666;
}
.message-content {
word-wrap: break-word;
}
.typing-indicator {
padding: 0.5rem 1rem;
color: #666;
font-style: italic;
font-size: 0.875rem;
}
.message-input-container {
display: flex;
padding: 1rem;
border-top: 1px solid #ddd;
background: #fafafa;
}
.message-input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
.send-button {
margin-left: 0.5rem;
padding: 0.5rem 1rem;
background: #2196f3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.send-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.leave-room-btn {
margin-left: 0.5rem;
padding: 0.25rem 0.5rem;
background: #f44336;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.75rem;
}
`}</style>
</div>
);
};