实时通信与WebSocket专题
掌握现代实时通信技术,构建高性能的实时应用
📚 专题目标
通过本专题学习,你将掌握:
- WebSocket 连接管理与消息处理
- Server-Sent Events (SSE) 实时数据推送
- Socket.io 高级应用
- WebRTC 音视频通信
- 实时应用架构设计
🔌 WebSocket 基础应用
WebSocket 连接管理
// websocket.ts
export class WebSocketManager {
private ws: WebSocket | null = null
private url: string
private reconnectAttempts = 0
private maxReconnectAttempts = 5
private reconnectInterval = 1000
private messageQueue: string[] = []
private isConnecting = false
constructor(url: string) {
this.url = url
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.isConnecting) return
this.isConnecting = true
try {
this.ws = new WebSocket(this.url)
this.ws.onopen = () => {
console.log('WebSocket connected')
this.isConnecting = false
this.reconnectAttempts = 0
// 发送队列中的消息
this.flushMessageQueue()
resolve()
}
this.ws.onmessage = (event) => {
this.handleMessage(event.data)
}
this.ws.onclose = (event) => {
console.log('WebSocket closed:', event.code, event.reason)
this.isConnecting = false
this.handleReconnect()
}
this.ws.onerror = (error) => {
console.error('WebSocket error:', error)
this.isConnecting = false
reject(error)
}
} catch (error) {
this.isConnecting = false
reject(error)
}
})
}
send(message: string | object): void {
const data = typeof message === 'string' ? message : JSON.stringify(message)
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(data)
} else {
// 连接未就绪,加入队列
this.messageQueue.push(data)
}
}
private handleMessage(data: string): void {
try {
const message = JSON.parse(data)
this.emit('message', message)
} catch {
this.emit('message', data)
}
}
private handleReconnect(): void {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++
const delay = this.reconnectInterval * Math.pow(2, this.reconnectAttempts - 1)
setTimeout(() => {
console.log(`Reconnecting... (${this.reconnectAttempts}/${this.maxReconnectAttempts})`)
this.connect().catch(console.error)
}, delay)
}
}
private flushMessageQueue(): void {
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift()
if (message && this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(message)
}
}
}
disconnect(): void {
if (this.ws) {
this.ws.close(1000, 'Client disconnect')
this.ws = null
}
}
private emit(event: string, data: any): void {
// 事件发射器逻辑
window.dispatchEvent(new CustomEvent(`websocket:${event}`, { detail: data }))
}
}
Vue3 WebSocket Composable
// useWebSocket.ts
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { WebSocketManager } from './websocket'
export function useWebSocket(url: string) {
const isConnected = ref(false)
const messages = ref<any[]>([])
const error = ref<string | null>(null)
const wsManager = new WebSocketManager(url)
const connect = async () => {
try {
await wsManager.connect()
isConnected.value = true
error.value = null
} catch (err) {
error.value = err instanceof Error ? err.message : 'Connection failed'
}
}
const disconnect = () => {
wsManager.disconnect()
isConnected.value = false
}
const send = (message: string | object) => {
if (isConnected.value) {
wsManager.send(message)
}
}
const handleMessage = (event: CustomEvent) => {
messages.value.push({
data: event.detail,
timestamp: new Date()
})
}
onMounted(() => {
window.addEventListener('websocket:message', handleMessage as EventListener)
connect()
})
onUnmounted(() => {
window.removeEventListener('websocket:message', handleMessage as EventListener)
disconnect()
})
return {
isConnected,
messages,
error,
connect,
disconnect,
send
}
}
实时聊天组件
<template>
<div class="chat-container">
<div class="chat-header">
<h3>实时聊天</h3>
<div class="connection-status" :class="{ 'connected': isConnected }">
{{ isConnected ? '已连接' : '连接中...' }}
</div>
</div>
<div class="chat-messages" ref="messagesContainer">
<div
v-for="message in messages"
:key="message.id"
class="message"
:class="{ 'own-message': message.isOwn }"
>
<div class="message-avatar">
<img :src="message.avatar" :alt="message.username" />
</div>
<div class="message-content">
<div class="message-header">
<span class="username">{{ message.username }}</span>
<span class="timestamp">{{ formatTime(message.timestamp) }}</span>
</div>
<div class="message-text">{{ message.text }}</div>
</div>
</div>
</div>
<div class="chat-input">
<input
v-model="inputMessage"
type="text"
placeholder="输入消息..."
@keyup.enter="sendMessage"
:disabled="!isConnected"
/>
<button @click="sendMessage" :disabled="!isConnected || !inputMessage.trim()">
发送
</button>
</div>
</div>
</template>
<script setup lang="ts">
interface ChatMessage {
id: string
username: string
avatar: string
text: string
timestamp: Date
isOwn: boolean
}
const { isConnected, messages, send } = useWebSocket('ws://localhost:8080/chat')
const inputMessage = ref('')
const messagesContainer = ref<HTMLElement>()
const sendMessage = () => {
if (inputMessage.value.trim() && isConnected.value) {
const message = {
type: 'chat',
text: inputMessage.value.trim(),
username: '当前用户',
timestamp: new Date()
}
send(message)
inputMessage.value = ''
}
}
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 自动滚动到底部
watch(messages, () => {
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}, { deep: true })
</script>
<style scoped>
.chat-container {
display: flex;
flex-direction: column;
height: 500px;
border: 1px solid #e5e5e5;
border-radius: 8px;
overflow: hidden;
}
.chat-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e5e5e5;
}
.connection-status {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
background: #ffc107;
color: #000;
}
.connection-status.connected {
background: #28a745;
color: #fff;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.message {
display: flex;
margin-bottom: 16px;
gap: 12px;
}
.message.own-message {
flex-direction: row-reverse;
}
.message-avatar img {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.message-content {
flex: 1;
max-width: 70%;
}
.message-header {
display: flex;
gap: 8px;
margin-bottom: 4px;
}
.username {
font-weight: 600;
font-size: 14px;
}
.timestamp {
font-size: 12px;
color: #666;
}
.message-text {
background: #f1f3f4;
padding: 8px 12px;
border-radius: 12px;
word-wrap: break-word;
}
.own-message .message-text {
background: #007bff;
color: white;
}
.chat-input {
display: flex;
padding: 16px;
border-top: 1px solid #e5e5e5;
gap: 8px;
}
.chat-input input {
flex: 1;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 20px;
outline: none;
}
.chat-input input:focus {
border-color: #007bff;
}
.chat-input button {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 20px;
cursor: pointer;
}
.chat-input button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
📡 Server-Sent Events (SSE)
SSE 连接管理
// sse.ts
export class SSEManager {
private eventSource: EventSource | null = null
private url: string
private reconnectInterval = 3000
private isConnecting = false
constructor(url: string) {
this.url = url
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.isConnecting) return
this.isConnecting = true
try {
this.eventSource = new EventSource(this.url)
this.eventSource.onopen = () => {
console.log('SSE connected')
this.isConnecting = false
resolve()
}
this.eventSource.onmessage = (event) => {
this.handleMessage(event.data)
}
this.eventSource.onerror = (error) => {
console.error('SSE error:', error)
this.isConnecting = false
reject(error)
}
// 监听自定义事件
this.eventSource.addEventListener('notification', (event) => {
this.handleNotification(JSON.parse(event.data))
})
this.eventSource.addEventListener('update', (event) => {
this.handleUpdate(JSON.parse(event.data))
})
} catch (error) {
this.isConnecting = false
reject(error)
}
})
}
private handleMessage(data: string): void {
try {
const message = JSON.parse(data)
this.emit('message', message)
} catch {
this.emit('message', data)
}
}
private handleNotification(data: any): void {
this.emit('notification', data)
}
private handleUpdate(data: any): void {
this.emit('update', data)
}
disconnect(): void {
if (this.eventSource) {
this.eventSource.close()
this.eventSource = null
}
}
private emit(event: string, data: any): void {
window.dispatchEvent(new CustomEvent(`sse:${event}`, { detail: data }))
}
}
实时通知组件
<template>
<div class="notification-container">
<div class="notification-header">
<h3>实时通知</h3>
<button @click="clearAll" class="clear-btn">清空</button>
</div>
<div class="notification-list">
<div
v-for="notification in notifications"
:key="notification.id"
class="notification-item"
:class="`notification-${notification.type}`"
>
<div class="notification-icon">
<Icon :name="getIcon(notification.type)" />
</div>
<div class="notification-content">
<div class="notification-title">{{ notification.title }}</div>
<div class="notification-message">{{ notification.message }}</div>
<div class="notification-time">{{ formatTime(notification.timestamp) }}</div>
</div>
<button @click="removeNotification(notification.id)" class="close-btn">
<Icon name="x" />
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Notification {
id: string
type: 'info' | 'success' | 'warning' | 'error'
title: string
message: string
timestamp: Date
}
const notifications = ref<Notification[]>([])
const { connect, disconnect } = useSSE('http://localhost:8080/events')
const handleNotification = (event: CustomEvent) => {
const notification: Notification = {
id: Date.now().toString(),
...event.detail,
timestamp: new Date()
}
notifications.value.unshift(notification)
// 自动移除通知
setTimeout(() => {
removeNotification(notification.id)
}, 5000)
}
const removeNotification = (id: string) => {
const index = notifications.value.findIndex(n => n.id === id)
if (index > -1) {
notifications.value.splice(index, 1)
}
}
const clearAll = () => {
notifications.value = []
}
const getIcon = (type: string) => {
const icons = {
info: 'info',
success: 'check-circle',
warning: 'alert-triangle',
error: 'x-circle'
}
return icons[type] || 'info'
}
const formatTime = (date: Date) => {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
onMounted(() => {
window.addEventListener('sse:notification', handleNotification as EventListener)
connect()
})
onUnmounted(() => {
window.removeEventListener('sse:notification', handleNotification as EventListener)
disconnect()
})
</script>
<style scoped>
.notification-container {
width: 400px;
height: 500px;
border: 1px solid #e5e5e5;
border-radius: 8px;
overflow: hidden;
background: white;
}
.notification-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e5e5e5;
}
.clear-btn {
padding: 4px 8px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.notification-list {
height: calc(100% - 60px);
overflow-y: auto;
}
.notification-item {
display: flex;
padding: 12px;
border-bottom: 1px solid #f0f0f0;
gap: 12px;
transition: background-color 0.2s ease;
}
.notification-item:hover {
background: #f8f9fa;
}
.notification-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.notification-info .notification-icon {
color: #007bff;
}
.notification-success .notification-icon {
color: #28a745;
}
.notification-warning .notification-icon {
color: #ffc107;
}
.notification-error .notification-icon {
color: #dc3545;
}
.notification-content {
flex: 1;
}
.notification-title {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
}
.notification-message {
font-size: 13px;
color: #666;
margin-bottom: 4px;
}
.notification-time {
font-size: 11px;
color: #999;
}
.close-btn {
width: 20px;
height: 20px;
border: none;
background: none;
cursor: pointer;
color: #999;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
color: #666;
}
</style>
🎮 Socket.io 高级应用
Socket.io 客户端管理
// socket.ts
import { io, Socket } from 'socket.io-client'
export class SocketManager {
private socket: Socket | null = null
private url: string
private options: any
constructor(url: string, options: any = {}) {
this.url = url
this.options = {
autoConnect: false,
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
...options
}
}
connect(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.socket?.connected) {
resolve()
return
}
this.socket = io(this.url, this.options)
this.socket.on('connect', () => {
console.log('Socket.io connected')
resolve()
})
this.socket.on('connect_error', (error) => {
console.error('Socket.io connection error:', error)
reject(error)
})
this.socket.on('disconnect', (reason) => {
console.log('Socket.io disconnected:', reason)
})
this.socket.on('reconnect', (attemptNumber) => {
console.log('Socket.io reconnected after', attemptNumber, 'attempts')
})
})
}
emit(event: string, data?: any): void {
if (this.socket?.connected) {
this.socket.emit(event, data)
}
}
on(event: string, callback: (data: any) => void): void {
if (this.socket) {
this.socket.on(event, callback)
}
}
off(event: string, callback?: (data: any) => void): void {
if (this.socket) {
this.socket.off(event, callback)
}
}
disconnect(): void {
if (this.socket) {
this.socket.disconnect()
this.socket = null
}
}
get connected(): boolean {
return this.socket?.connected || false
}
}
实时协作编辑器
<template>
<div class="collaborative-editor">
<div class="editor-header">
<h3>协作编辑器</h3>
<div class="collaborators">
<div
v-for="user in collaborators"
:key="user.id"
class="collaborator"
:style="{ backgroundColor: user.color }"
>
{{ user.name }}
</div>
</div>
</div>
<div class="editor-content">
<textarea
v-model="content"
@input="handleInput"
@selectionchange="handleSelectionChange"
class="editor-textarea"
placeholder="开始协作编辑..."
/>
</div>
<div class="editor-status">
<span>连接状态: {{ isConnected ? '已连接' : '连接中...' }}</span>
<span>在线用户: {{ collaborators.length }}</span>
</div>
</div>
</template>
<script setup lang="ts">
interface Collaborator {
id: string
name: string
color: string
cursor?: { start: number; end: number }
}
const content = ref('')
const collaborators = ref<Collaborator[]>([])
const isConnected = ref(false)
const socketManager = new SocketManager('http://localhost:8080')
const handleInput = (event: Event) => {
const target = event.target as HTMLTextAreaElement
const newContent = target.value
// 发送内容更新
socketManager.emit('content-change', {
content: newContent,
timestamp: Date.now()
})
}
const handleSelectionChange = () => {
const textarea = document.querySelector('.editor-textarea') as HTMLTextAreaElement
if (textarea) {
socketManager.emit('cursor-change', {
start: textarea.selectionStart,
end: textarea.selectionEnd
})
}
}
const handleContentChange = (data: any) => {
if (data.content !== content.value) {
content.value = data.content
}
}
const handleUserJoin = (data: any) => {
collaborators.value.push(data.user)
}
const handleUserLeave = (data: any) => {
const index = collaborators.value.findIndex(u => u.id === data.userId)
if (index > -1) {
collaborators.value.splice(index, 1)
}
}
const handleCursorChange = (data: any) => {
const user = collaborators.value.find(u => u.id === data.userId)
if (user) {
user.cursor = { start: data.start, end: data.end }
}
}
onMounted(async () => {
try {
await socketManager.connect()
isConnected.value = true
// 监听事件
socketManager.on('content-change', handleContentChange)
socketManager.on('user-join', handleUserJoin)
socketManager.on('user-leave', handleUserLeave)
socketManager.on('cursor-change', handleCursorChange)
// 加入房间
socketManager.emit('join-room', { roomId: 'editor-1' })
} catch (error) {
console.error('Failed to connect:', error)
}
})
onUnmounted(() => {
socketManager.disconnect()
})
</script>
<style scoped>
.collaborative-editor {
width: 100%;
height: 500px;
border: 1px solid #e5e5e5;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #f8f9fa;
border-bottom: 1px solid #e5e5e5;
}
.collaborators {
display: flex;
gap: 8px;
}
.collaborator {
padding: 4px 8px;
border-radius: 12px;
color: white;
font-size: 12px;
font-weight: 500;
}
.editor-content {
flex: 1;
position: relative;
}
.editor-textarea {
width: 100%;
height: 100%;
border: none;
outline: none;
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 14px;
line-height: 1.5;
resize: none;
}
.editor-status {
display: flex;
justify-content: space-between;
padding: 8px 16px;
background: #f8f9fa;
border-top: 1px solid #e5e5e5;
font-size: 12px;
color: #666;
}
</style>
🎥 WebRTC 音视频通信
WebRTC 连接管理
// webrtc.ts
export class WebRTCManager {
private localStream: MediaStream | null = null
private peerConnection: RTCPeerConnection | null = null
private dataChannel: RTCDataChannel | null = null
private iceServers = [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
async initializeLocalStream(): Promise<MediaStream> {
try {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
})
return this.localStream
} catch (error) {
console.error('Error accessing media devices:', error)
throw error
}
}
createPeerConnection(): RTCPeerConnection {
this.peerConnection = new RTCPeerConnection({
iceServers: this.iceServers
})
// 添加本地流
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
this.peerConnection!.addTrack(track, this.localStream!)
})
}
// 创建数据通道
this.dataChannel = this.peerConnection.createDataChannel('chat', {
ordered: true
})
// 监听ICE候选
this.peerConnection.onicecandidate = (event) => {
if (event.candidate) {
this.emit('ice-candidate', event.candidate)
}
}
// 监听远程流
this.peerConnection.ontrack = (event) => {
this.emit('remote-stream', event.streams[0])
}
// 监听数据通道
this.peerConnection.ondatachannel = (event) => {
const channel = event.channel
channel.onmessage = (event) => {
this.emit('data-message', JSON.parse(event.data))
}
}
return this.peerConnection
}
async createOffer(): Promise<RTCSessionDescriptionInit> {
if (!this.peerConnection) {
throw new Error('Peer connection not initialized')
}
const offer = await this.peerConnection.createOffer()
await this.peerConnection.setLocalDescription(offer)
return offer
}
async createAnswer(offer: RTCSessionDescriptionInit): Promise<RTCSessionDescriptionInit> {
if (!this.peerConnection) {
throw new Error('Peer connection not initialized')
}
await this.peerConnection.setRemoteDescription(offer)
const answer = await this.peerConnection.createAnswer()
await this.peerConnection.setLocalDescription(answer)
return answer
}
async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> {
if (!this.peerConnection) {
throw new Error('Peer connection not initialized')
}
await this.peerConnection.setRemoteDescription(description)
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
if (!this.peerConnection) {
throw new Error('Peer connection not initialized')
}
await this.peerConnection.addIceCandidate(candidate)
}
sendData(data: any): void {
if (this.dataChannel && this.dataChannel.readyState === 'open') {
this.dataChannel.send(JSON.stringify(data))
}
}
close(): void {
if (this.localStream) {
this.localStream.getTracks().forEach(track => track.stop())
this.localStream = null
}
if (this.peerConnection) {
this.peerConnection.close()
this.peerConnection = null
}
if (this.dataChannel) {
this.dataChannel.close()
this.dataChannel = null
}
}
private emit(event: string, data: any): void {
window.dispatchEvent(new CustomEvent(`webrtc:${event}`, { detail: data }))
}
}
视频通话组件
<template>
<div class="video-call">
<div class="video-container">
<div class="local-video">
<video ref="localVideoRef" autoplay muted playsinline />
<div class="video-controls">
<button @click="toggleMute" :class="{ active: !isMuted }">
<Icon :name="isMuted ? 'mic-off' : 'mic'" />
</button>
<button @click="toggleVideo" :class="{ active: !isVideoOff }">
<Icon :name="isVideoOff ? 'video-off' : 'video'" />
</button>
<button @click="endCall" class="end-call">
<Icon name="phone" />
</button>
</div>
</div>
<div class="remote-video">
<video ref="remoteVideoRef" autoplay playsinline />
<div v-if="!remoteStream" class="waiting">
<Icon name="user" />
<p>等待对方加入...</p>
</div>
</div>
</div>
<div class="call-info">
<div class="connection-status" :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '连接中...' }}
</div>
<div class="call-duration">{{ formatDuration(callDuration) }}</div>
</div>
</div>
</template>
<script setup lang="ts">
const localVideoRef = ref<HTMLVideoElement>()
const remoteVideoRef = ref<HTMLVideoElement>()
const isConnected = ref(false)
const isMuted = ref(false)
const isVideoOff = ref(false)
const remoteStream = ref<MediaStream | null>(null)
const callDuration = ref(0)
const webrtcManager = new WebRTCManager()
let callTimer: NodeJS.Timeout | null = null
const initializeCall = async () => {
try {
// 获取本地流
const stream = await webrtcManager.initializeLocalStream()
if (localVideoRef.value) {
localVideoRef.value.srcObject = stream
}
// 创建对等连接
webrtcManager.createPeerConnection()
// 监听事件
window.addEventListener('webrtc:remote-stream', handleRemoteStream)
window.addEventListener('webrtc:ice-candidate', handleIceCandidate)
// 开始计时
startCallTimer()
} catch (error) {
console.error('Failed to initialize call:', error)
}
}
const handleRemoteStream = (event: CustomEvent) => {
remoteStream.value = event.detail
if (remoteVideoRef.value) {
remoteVideoRef.value.srcObject = event.detail
}
isConnected.value = true
}
const handleIceCandidate = (event: CustomEvent) => {
// 发送ICE候选到对方
console.log('ICE candidate:', event.detail)
}
const toggleMute = () => {
isMuted.value = !isMuted.value
if (webrtcManager.localStream) {
webrtcManager.localStream.getAudioTracks().forEach(track => {
track.enabled = !isMuted.value
})
}
}
const toggleVideo = () => {
isVideoOff.value = !isVideoOff.value
if (webrtcManager.localStream) {
webrtcManager.localStream.getVideoTracks().forEach(track => {
track.enabled = !isVideoOff.value
})
}
}
const endCall = () => {
webrtcManager.close()
if (callTimer) {
clearInterval(callTimer)
}
// 返回上一页或关闭弹窗
}
const startCallTimer = () => {
callTimer = setInterval(() => {
callDuration.value++
}, 1000)
}
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
onMounted(() => {
initializeCall()
})
onUnmounted(() => {
webrtcManager.close()
if (callTimer) {
clearInterval(callTimer)
}
})
</script>
<style scoped>
.video-call {
width: 100%;
height: 100vh;
background: #000;
display: flex;
flex-direction: column;
}
.video-container {
flex: 1;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
.local-video {
position: absolute;
top: 20px;
right: 20px;
width: 200px;
height: 150px;
border-radius: 8px;
overflow: hidden;
z-index: 10;
}
.remote-video {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.remote-video video {
width: 100%;
height: 100%;
object-fit: cover;
}
.local-video video {
width: 100%;
height: 100%;
object-fit: cover;
}
.video-controls {
position: absolute;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
}
.video-controls button {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
background: rgba(255, 255, 255, 0.2);
color: white;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s ease;
}
.video-controls button:hover {
background: rgba(255, 255, 255, 0.3);
}
.video-controls button.active {
background: #28a745;
}
.video-controls .end-call {
background: #dc3545;
}
.waiting {
display: flex;
flex-direction: column;
align-items: center;
color: white;
gap: 16px;
}
.waiting .icon {
width: 64px;
height: 64px;
opacity: 0.5;
}
.call-info {
position: absolute;
top: 20px;
left: 20px;
display: flex;
gap: 20px;
color: white;
z-index: 10;
}
.connection-status {
padding: 4px 8px;
border-radius: 4px;
background: rgba(255, 193, 7, 0.8);
font-size: 12px;
}
.connection-status.connected {
background: rgba(40, 167, 69, 0.8);
}
.call-duration {
padding: 4px 8px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.5);
font-size: 12px;
font-family: monospace;
}
</style>
🎯 专题总结
通过本专题学习,你掌握了:
- WebSocket 应用:连接管理、消息处理、重连机制
- SSE 实时推送:事件流、通知系统、自动重连
- Socket.io 高级功能:房间管理、协作编辑、实时同步
- WebRTC 音视频:媒体流、对等连接、数据通道
- 实时应用架构:状态管理、错误处理、性能优化
📝 练习题
- 实现一个完整的实时聊天应用
- 开发一个协作文档编辑器
- 创建一个视频会议系统
- 构建实时数据监控面板
- 实现实时游戏功能