19.2 使用 React/Vue 前端框架
19.2 使用 React/Vue 前端框架
现代前端框架可以大幅提升聊天应用的开发效率,本节将分别展示React和Vue的实现方案,并提供最佳实践建议。
React 实现方案
1. 组件架构设计
chat-app/
├── components/
│ ├── MessageList.jsx
│ ├── MessageInput.jsx
│ ├── StatusIndicator.jsx
│ └── UserList.jsx
├── hooks/
│ └── useChat.js
├── contexts/
│ └── ChatContext.js
└── App.jsx
2. 核心聊天Hook增强版
// hooks/useChat.js
import { useCallback, useEffect, useReducer } from 'react';
function chatReducer(state, action) {
switch (action.type) {
case 'CONNECTED':
return { ...state, isConnected: true };
case 'DISCONNECTED':
return { ...state, isConnected: false };
case 'NEW_MESSAGE':
return {
...state,
messages: [...state.messages, action.payload],
unread: !document.hasFocus()
? state.unread + 1
: 0
};
case 'CLEAR_UNREAD':
return { ...state, unread: 0 };
default:
return state;
}
}
export function useChat(wsUrl) {
const [state, dispatch] = useReducer(chatReducer, {
isConnected: false,
messages: [],
unread: 0
});
const wsRef = useRef(null);
const handleVisibilityChange = useCallback(() => {
if (document.visibilityState === 'visible') {
dispatch({ type: 'CLEAR_UNREAD' });
}
}, []);
useEffect(() => {
document.addEventListener('visibilitychange', handleVisibilityChange);
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange);
};
}, [handleVisibilityChange]);
const sendMessage = useCallback((content) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(JSON.stringify({
type: 'chat_message',
content,
timestamp: Date.now()
}));
}
}, []);
useEffect(() => {
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = () => dispatch({ type: 'CONNECTED' });
ws.onclose = () => dispatch({ type: 'DISCONNECTED' });
ws.onmessage = (e) => {
try {
const message = JSON.parse(e.data);
dispatch({ type: 'NEW_MESSAGE', payload: message });
} catch (err) {
console.error('消息解析失败:', err);
}
};
return () => {
ws.close();
};
}, [wsUrl]);
return { ...state, sendMessage };
}
3. 优化版消息列表组件
// components/MessageList.jsx
import React, { useEffect, useRef } from 'react';
import PropTypes from 'prop-types';
export function MessageList({ messages }) {
const listRef = useRef(null);
useEffect(() => {
// 自动滚动到底部
const { current } = listRef;
if (current) {
current.scrollTop = current.scrollHeight;
}
}, [messages]);
return (
<div ref={listRef} className="message-list">
{messages.map((message, index) => {
const isContinuous = index > 0 &&
messages[index - 1].userId === message.userId;
return (
<div
key={`${message.id}-${index}`}
className={`message ${isContinuous ? 'continuous' : ''}`}
>
{!isContinuous && (
<div className="message-header">
<span className="username">{message.username}</span>
<span className="timestamp">
{new Date(message.timestamp).toLocaleTimeString()}
</span>
</div>
)}
<div className="message-content">{message.content}</div>
</div>
);
})}
</div>
);
}
MessageList.propTypes = {
messages: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
userId: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
timestamp: PropTypes.number.isRequired
})
).isRequired
};
Vue 3 实现方案
1. 组合式API实现
<!-- src/components/ChatRoom.vue -->
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue';
const props = defineProps({
roomId: {
type: String,
required: true
}
});
const messages = ref([]);
const isConnected = ref(false);
const inputMessage = ref('');
const ws = ref(null);
const formattedMessages = computed(() => {
return messages.value.map(msg => ({
...msg,
time: new Date(msg.timestamp).toLocaleTimeString()
}));
});
function connect() {
ws.value = new WebSocket(`wss://api.example.com/chat/${props.roomId}`);
ws.value.onopen = () => {
isConnected.value = true;
};
ws.value.onmessage = (e) => {
try {
const message = JSON.parse(e.data);
messages.value.push(message);
} catch (err) {
console.error('消息解析失败:', err);
}
};
ws.value.onclose = () => {
isConnected.value = false;
setTimeout(connect, 3000); // 3秒后重连
};
}
function sendMessage() {
if (inputMessage.value.trim() && ws.value?.readyState === WebSocket.OPEN) {
const message = {
content: inputMessage.value,
timestamp: Date.now()
};
ws.value.send(JSON.stringify(message));
inputMessage.value = '';
}
}
onMounted(connect);
onUnmounted(() => {
ws.value?.close();
});
</script>
<template>
<div class="chat-container">
<div class="status-indicator" :class="{ connected: isConnected }">
{{ isConnected ? '已连接' : '连接中...' }}
</div>
<div class="message-list">
<div
v-for="(msg, index) in formattedMessages"
:key="index"
class="message"
>
<div class="message-time">{{ msg.time }}</div>
<div class="message-content">{{ msg.content }}</div>
</div>
</div>
<div class="input-area">
<input
v-model="inputMessage"
@keyup.enter="sendMessage"
:disabled="!isConnected"
/>
<button
@click="sendMessage"
:disabled="!isConnected || !inputMessage.trim()"
>
发送
</button>
</div>
</div>
</template>
2. Pinia状态管理
// stores/chat.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useChatStore = defineStore('chat', () => {
const messages = ref([]);
const isConnected = ref(false);
const unreadCount = ref(0);
const ws = ref(null);
const lastMessage = computed(() =>
messages.value[messages.value.length - 1]
);
function connect(url) {
ws.value = new WebSocket(url);
ws.value.onopen = () => {
isConnected.value = true;
};
ws.value.onmessage = (e) => {
try {
const message = JSON.parse(e.data);
messages.value.push(message);
if (document.visibilityState !== 'visible') {
unreadCount.value++;
}
} catch (err) {
console.error('消息解析失败:', err);
}
};
ws.value.onclose = () => {
isConnected.value = false;
};
}
function send(content) {
if (ws.value?.readyState === WebSocket.OPEN) {
ws.value.send(JSON.stringify({
type: 'chat_message',
content,
timestamp: Date.now()
}));
}
}
function clearUnread() {
unreadCount.value = 0;
}
return {
messages,
isConnected,
unreadCount,
lastMessage,
connect,
send,
clearUnread
};
});
跨框架最佳实践
1. 性能优化方案
虚拟滚动 (适用于大型消息列表):
// React 示例使用 react-window
import { FixedSizeList as List } from 'react-window';
function VirtualizedMessageList({ messages }) {
const Row = ({ index, style }) => (
<div style={style}>
<Message message={messages[index]} />
</div>
);
return (
<List
height={500}
itemCount={messages.length}
itemSize={80}
width="100%"
>
{Row}
</List>
);
}
// Vue 示例使用 vue-virtual-scroller
<template>
<RecycleScroller
class="scroller"
:items="messages"
:item-size="80"
key-field="id"
>
<template #default="{ item }">
<Message :message="item" />
</template>
</RecycleScroller>
</template>
2. 消息持久化策略
// 使用IndexedDB缓存消息
async function cacheMessages(messages) {
const db = await openDB('ChatDB', 1, {
upgrade(db) {
db.createObjectStore('messages', { keyPath: 'id' });
}
});
await db.clear('messages');
await Promise.all(
messages.map(msg =>
db.put('messages', msg)
)
);
}
// 组件卸载时保存
useEffect(() => {
return () => {
cacheMessages(messages);
};
}, [messages]);
3. 音视频通知集成
// 新消息通知
function playNotificationSound() {
const audio = new Audio('/notification.mp3');
audio.volume = 0.3;
audio.play().catch(e => console.log('播放失败:', e));
}
// 在收到消息时调用
useEffect(() => {
if (messages.length > 0 && !document.hasFocus()) {
playNotificationSound();
// 浏览器通知
if (Notification.permission === 'granted') {
new Notification('新消息', {
body: lastMessage.content,
icon: '/icon.png'
});
}
}
}, [messages.length]);
响应式设计技巧
/* 移动端适配 */
.chat-container {
display: flex;
flex-direction: column;
height: 100vh;
}
.message-list {
flex: 1;
overflow-y: auto;
padding: 10px;
}
.input-area {
padding: 10px;
display: flex;
position: sticky;
bottom: 0;
background: white;
}
@media (max-width: 768px) {
.message {
font-size: 14px;
}
.input-area {
flex-direction: column;
}
}
安全增强措施
- 消息过滤:
function sanitizeMessage(content) {
const div = document.createElement('div');
div.textContent = content;
return div.innerHTML
.replace(/</g, '<')
.replace(/>/g, '>');
}
// 在显示消息前调用
message.content = sanitizeMessage(message.content);
- 频率限制:
// 使用令牌桶算法限制发送频率
const messageQueue = [];
let tokens = 5; // 初始令牌数
const MAX_TOKENS = 5;
setInterval(() => {
tokens = Math.min(tokens + 1, MAX_TOKENS);
if (tokens > 0 && messageQueue.length > 0) {
const message = messageQueue.shift();
ws.send(message);
tokens--;
}
}, 1000); // 每秒补充1个令牌
function sendWithThrottle(content) {
if (tokens > 0) {
ws.send(JSON.stringify({ content }));
tokens--;
} else {
messageQueue.push(JSON.stringify({ content }));
}
}
框架特定优化
React 优化技巧
// 使用React.memo优化消息项
const MessageItem = React.memo(({ message }) => {
return (
<div className="message">
{message.content}
</div>
);
}, (prev, next) => {
return prev.message.id === next.message.id;
});
// 使用useCallback避免不必要的重渲染
const sendMessage = useCallback((content) => {
// ...发送逻辑
}, [ws]);
Vue 优化技巧
<!-- 使用v-memo优化渲染 -->
<div
v-for="msg in messages"
:key="msg.id"
v-memo="[msg.id, msg.content]"
class="message"
>
{{ msg.content }}
</div>
<!-- 使用computed计算属性 -->
<script setup>
const unreadMessages = computed(() =>
messages.value.filter(msg => !msg.read)
);
</script>
通过以上实现,你可以基于React或Vue构建高性能的实时聊天界面。接下来我们将学习如何实现JWT身份验证来保护聊天系统。
#前端开发
分享于 2025-03-25
上一篇:19.1 WebSocket 通信
下一篇:19.3 JWT 身份验证