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;
  }
}

安全增强措施

  1. 消息过滤
function sanitizeMessage(content) {
  const div = document.createElement('div');
  div.textContent = content;
  return div.innerHTML
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
}

// 在显示消息前调用
message.content = sanitizeMessage(message.content);
  1. 频率限制
// 使用令牌桶算法限制发送频率
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

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】