React从基础入门到高级实战:React 实战项目 - 项目三:实时聊天应用

React 实战项目:实时聊天应用

欢迎来到本 React 开发教程专栏 的第 28 篇!在前 27 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件设计、状态管理、路由配置、性能优化和架构模式等核心知识。这一次,我们将通过一个完整的实战项目------实时聊天应用,将这些知识融会贯通,帮助您从理论走向实践。

本项目的目标是为中高级开发者提供一个全面的 React 开发体验。通过这个类似 Slack 的实时聊天应用,您将学习如何分析需求、选择技术栈、实现复杂功能、优化性能并最终部署上线。无论您是希望积累项目经验的中级开发者,还是追求架构优化的高级开发者,这篇文章都将为您提供清晰的指引、丰富的代码示例和深入的场景分析。

引言

实时聊天应用是现代 Web 开发中最具挑战性和实用性的项目之一。它不仅需要处理复杂的用户交互和数据流,还要求高性能和出色的用户体验。在本项目中,我们将构建一个功能完善的聊天应用,支持实时消息传递、用户在线状态显示、消息历史记录、动画效果和状态同步等特性。通过这个项目,您将掌握 React 在实际场景中的高级应用,理解实时通信的实现原理,并学习如何优化和部署一个生产级应用。

这个应用的目标非常明确:为用户提供流畅的聊天体验,同时为开发者提供一个学习和实践 React 高级特性的平台。我们将从需求分析开始,逐步完成技术选型、功能实现、性能优化和上线部署,并在最后提供一个练习,帮助您进一步巩固所学内容。

通过本项目,您将体验到:

  • 需求分析:如何将业务需求转化为技术实现。
  • 技术栈选择:如何根据项目需求选择合适的工具和库。
  • 状态管理:如何使用 React Query 和 Redux Toolkit 管理复杂状态。
  • 实时通信:如何通过 WebSocket 实现消息实时传递。
  • 性能优化:如何通过消息缓存和断线重连提升用户体验。
  • 部署上线:如何将应用部署到 AWS 并确保其稳定运行。

准备好了吗?让我们开始吧!


需求分析

在动手编码之前,我们需要明确项目的功能需求。一个清晰的需求清单不仅能指导开发过程,还能帮助我们理解每个功能的意义。以下是实时聊天应用的核心需求:

  1. 实时消息
    • 用户可以发送和接收消息,消息需实时更新。
    • 支持文本消息和表情输入。
  2. 用户在线状态
    • 显示用户的在线状态(如在线、离线、忙碌)。
    • 支持用户手动设置状态。
  3. 消息历史
    • 用户可以查看历史消息记录。
    • 支持消息搜索和过滤功能。
  4. 动画效果
    • 消息发送和接收时具有动画效果,提升用户体验。
    • 支持消息列表的平滑滚动。
  5. 状态同步
    • 确保不同用户之间的状态同步(如消息已读状态)。
    • 支持多设备登录和状态一致性。

需求背后的意义

这些功能覆盖了实时聊天应用的核心场景,同时为学习 React 提供了丰富的实践机会:

  • 实时消息和在线状态 需要 WebSocket 和实时通信技术的支持。
  • 消息历史和状态同步 涉及数据请求、缓存和一致性管理。
  • 动画效果 展示了如何使用现代库提升用户体验。
  • 多设备支持 引入了状态管理的复杂性,考验架构设计能力。

这些需求还为性能优化(如消息缓存和断线重连)提供了实际场景,确保应用在高负载下依然流畅。


技术栈选择

在实现功能之前,我们需要选择合适的技术栈。以下是本项目使用的工具和技术,以及选择它们的理由:

  • React
    核心前端框架,用于构建用户界面。React 的组件化和声明式编程让开发过程更高效。
  • WebSocket (via Socket.IO)
    用于实现实时通信,确保消息的实时传递。Socket.IO 提供了便捷的 API 和断线重连支持。
  • React Query
    用于管理数据请求和缓存,简化与后端交互并提升性能。
  • Framer Motion
    用于实现动画效果,提升用户体验。其简单而强大的 API 非常适合 React 项目。
  • Redux Toolkit
    用于管理全局状态,确保状态的可预测性和一致性。
  • AWS
    用于部署应用,提供高可用性和可扩展性。

技术栈的优势

  • React:生态丰富,社区活跃,是现代 Web 开发的首选框架。
  • Socket.IO:封装了 WebSocket,简化了实时通信的实现。
  • React Query:自动管理数据获取、缓存和同步,大幅提升开发效率。
  • Framer Motion:提供流畅的动画效果,易于集成到 React 组件中。
  • Redux Toolkit:简化 Redux 的使用,适合复杂状态管理。
  • AWS:支持自动扩展和负载均衡,确保应用的稳定性。

这些工具的组合不仅易于上手,还能帮助您掌握 2025 年 React 开发的最佳实践。


项目实现

现在,我们进入核心部分------代码实现。我们将从项目搭建开始,逐步完成组件设计、WebSocket 集成、状态管理、动画效果和状态同步。

1. 项目搭建

我们使用 Vite 快速创建一个 React 项目,因其构建速度快且配置简单。

bash 复制代码
npm create vite@latest chat-app -- --template react
cd chat-app
npm install
npm run dev

安装必要的依赖:

bash 复制代码
npm install react-router-dom @reduxjs/toolkit react-redux @tanstack/react-query framer-motion socket.io-client tailwindcss postcss autoprefixer

初始化 Tailwind CSS:

bash 复制代码
npx tailwindcss init -p

编辑 tailwind.config.js

js 复制代码
/** @type {import('tailwindcss').Config} */
export default {
  content: [
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

src/index.css 中引入 Tailwind:

css 复制代码
@tailwind base;
@tailwind components;
@tailwind utilities;

这将启动一个基础项目,接下来我们将实现具体功能。

2. 组件拆分

组件化是 React 的核心思想。通过将应用拆分为小组件,我们提高代码的可读性和复用性。

组件结构
  • App:根组件,负责路由和布局。
  • Header:导航栏,包含用户菜单。
  • ChatList:聊天列表,支持搜索。
  • ChatItem:单个聊天项。
  • ChatWindow:聊天窗口,显示消息和输入框。
  • MessageList:消息列表,支持动画。
  • MessageItem:单个消息。
  • InputBox:消息输入框。
  • UserList:用户列表,显示在线状态。
文件结构
复制代码
src/
├── components/
│   ├── Header.jsx
│   ├── ChatList.jsx
│   ├── ChatItem.jsx
│   ├── ChatWindow.jsx
│   ├── MessageList.jsx
│   ├── MessageItem.jsx
│   ├── InputBox.jsx
│   └── UserList.jsx
├── features/
│   ├── auth/
│   │   └── authSlice.js
│   ├── chat/
│   │   └── chatSlice.js
│   └── users/
│       └── usersSlice.js
├── pages/
│   ├── Home.jsx
│   ├── Chat.jsx
│   └── Profile.jsx
├── App.jsx
├── main.jsx
└── index.css

3. 路由设计

我们使用 React Router 实现多页面导航。

路由配置
  • /:首页,显示聊天列表。
  • /chat/:id:聊天页面,显示指定聊天。
  • /profile:用户资料页面。

App.jsx

js 复制代码
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Header from './components/Header';
import Home from './pages/Home';
import Chat from './pages/Chat';
import Profile from './pages/Profile';

function App({ socket }) {
  return (
    <Router>
      <div className="min-h-screen bg-gray-100">
        <Header />
        <main className="container mx-auto p-4">
          <Routes>
            <Route path="/" element={<Home socket={socket} />} />
            <Route path="/chat/:id" element={<Chat socket={socket} />} />
            <Route path="/profile" element={<Profile />} />
          </Routes>
        </main>
      </div>
    </Router>
  );
}

export default App;
导航栏

Header.jsx

js 复制代码
import { Link } from 'react-router-dom';

function Header() {
  return (
    <header className="bg-blue-600 text-white p-4 shadow-md">
      <nav className="flex justify-between items-center max-w-6xl mx-auto">
        <Link to="/" className="text-xl font-bold">实时聊天</Link>
        <div className="space-x-4">
          <Link to="/profile" className="hover:underline">个人中心</Link>
        </div>
      </nav>
    </header>
  );
}

export default Header;

4. WebSocket 集成

我们使用 Socket.IO 实现实时通信。

配置 Socket.IO

main.jsx

js 复制代码
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { io } from 'socket.io-client';
import App from './App';
import store from './store';
import './index.css';

const socket = io('http://localhost:3000', { autoConnect: true });

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App socket={socket} />
  </Provider>
);
后端示例(Node.js)

为了测试,我们需要一个简单的 Socket.IO 后端(可单独运行):

js 复制代码
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });

io.on('connection', (socket) => {
  console.log('User connected:', socket.id);

  socket.on('message', (data) => {
    io.emit('message', { ...data, id: Date.now() });
  });

  socket.on('read', (data) => {
    io.emit('read', data);
  });

  socket.on('disconnect', () => {
    console.log('User disconnected:', socket.id);
  });
});

server.listen(3000, () => {
  console.log('Server running on port 3000');
});

安装后端依赖并运行:

bash 复制代码
npm init -y
npm install express socket.io
node server.js
发送和接收消息

ChatWindow.jsx

js 复制代码
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { addMessage } from '../features/chat/chatSlice';
import MessageList from './MessageList';
import InputBox from './InputBox';

function ChatWindow({ socket, chatId }) {
  const dispatch = useDispatch();
  const [isConnected, setIsConnected] = useState(socket.connected);

  useEffect(() => {
    socket.on('connect', () => setIsConnected(true));
    socket.on('disconnect', () => setIsConnected(false));

    socket.on('message', (message) => {
      dispatch(addMessage(message));
    });

    socket.emit('read', { chatId });

    return () => {
      socket.off('connect');
      socket.off('disconnect');
      socket.off('message');
    };
  }, [socket, dispatch, chatId]);

  const sendMessage = (text) => {
    if (!isConnected) return;
    socket.emit('message', { text, user: 'Me', chatId });
  };

  return (
    <div className="flex flex-col h-[calc(100vh-80px)] bg-white rounded-lg shadow-lg">
      <div className="p-4 bg-gray-200">
        <h2 className="text-lg font-semibold">聊天 #{chatId}</h2>
        <p className="text-sm">{isConnected ? '在线' : '离线'}</p>
      </div>
      <MessageList />
      <InputBox onSend={sendMessage} disabled={!isConnected} />
    </div>
  );
}

export default ChatWindow;

5. 状态管理

我们结合 Redux Toolkit 和 React Query 管理应用状态。

配置 Store

store.js

js 复制代码
import { configureStore } from '@reduxjs/toolkit';
import authReducer from './features/auth/authSlice';
import chatReducer from './features/chat/chatSlice';
import usersReducer from './features/users/usersSlice';

export const store = configureStore({
  reducer: {
    auth: authReducer,
    chat: chatReducer,
    users: usersReducer,
  },
});

export default store;
聊天状态

features/chat/chatSlice.js

js 复制代码
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  messages: [],
};

export const chatSlice = createSlice({
  name: 'chat',
  initialState,
  reducers: {
    addMessage: (state, action) => {
      state.messages.push(action.payload);
    },
    setMessages: (state, action) => {
      state.messages = action.payload;
    },
  },
});

export const { addMessage, setMessages } = chatSlice.actions;
export default chatSlice.reducer;
用户状态

features/users/usersSlice.js

js 复制代码
import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  users: [],
  online: {},
};

export const usersSlice = createSlice({
  name: 'users',
  initialState,
  reducers: {
    setUsers: (state, action) => {
      state.users = action.payload;
    },
    updateOnlineStatus: (state, action) => {
      state.online = { ...state.online, ...action.payload };
    },
  },
});

export const { setUsers, updateOnlineStatus } = usersSlice.actions;
export default usersSlice.reducer;
数据缓存

使用 React Query 获取消息历史:

Chat.jsx

js 复制代码
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { useDispatch } from 'react-redux';
import { setMessages } from '../features/chat/chatSlice';
import ChatWindow from '../components/ChatWindow';

const fetchMessages = async (chatId) => {
  const { data } = await axios.get(`/api/chats/${chatId}/messages`);
  return data;
};

function Chat({ socket }) {
  const dispatch = useDispatch();
  const chatId = useParams().id;

  const { data, isLoading } = useQuery({
    queryKey: ['messages', chatId],
    queryFn: () => fetchMessages(chatId),
    onSuccess: (messages) => {
      dispatch(setMessages(messages));
    },
  });

  if (isLoading) return <div className="text-center">加载中...</div>;

  return <ChatWindow socket={socket} chatId={chatId} />;
}

export default Chat;

6. 动画效果

使用 Framer Motion 为消息添加动画。

MessageItem.jsx

js 复制代码
import { motion } from 'framer-motion';

function MessageItem({ message }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
      className={`p-3 rounded-lg max-w-xs ${message.user === 'Me' ? 'bg-blue-500 text-white ml-auto' : 'bg-gray-200'}`}
    >
      <p>{message.text}</p>
      <span className="text-xs opacity-75">{new Date(message.id).toLocaleTimeString()}</span>
    </motion.div>
  );
}

export default MessageItem;

MessageList.jsx

js 复制代码
import { useSelector } from 'react-redux';
import { useEffect, useRef } from 'react';
import MessageItem from './MessageItem';

function MessageList() {
  const messages = useSelector((state) => state.chat.messages);
  const listRef = useRef(null);

  useEffect(() => {
    listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: 'smooth' });
  }, [messages]);

  return (
    <div ref={listRef} className="flex-1 overflow-y-auto p-4 space-y-4">
      {messages.map((msg) => (
        <MessageItem key={msg.id} message={msg} />
      ))}
    </div>
  );
}

export default MessageList;

7. 用户界面

聊天列表

ChatList.jsx

js 复制代码
import { Link } from 'react-router-dom';

function ChatList() {
  const chats = [
    { id: 1, name: '团队讨论' },
    { id: 2, name: '技术交流' },
  ];

  return (
    <div className="space-y-2">
      {chats.map((chat) => (
        <Link
          key={chat.id}
          to={`/chat/${chat.id}`}
          className="block p-3 bg-white rounded-lg shadow hover:bg-gray-50"
        >
          {chat.name}
        </Link>
      ))}
    </div>
  );
}

export default ChatList;
输入框

InputBox.jsx

js 复制代码
import { useState } from 'react';

function InputBox({ onSend, disabled }) {
  const [text, setText] = useState('');

  const handleSend = () => {
    if (!text.trim() || disabled) return;
    onSend(text);
    setText('');
  };

  return (
    <div className="p-4 border-t bg-white flex items-center space-x-2">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
        className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="输入消息..."
        disabled={disabled}
      />
      <button
        onClick={handleSend}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
        disabled={disabled}
      >
        发送
      </button>
    </div>
  );
}

export default InputBox;
用户列表

UserList.jsx

js 复制代码
import { useSelector } from 'react-redux';

function UserList() {
  const users = useSelector((state) => state.users.users);
  const online = useSelector((state) => state.users.online);

  return (
    <div className="p-4 bg-white rounded-lg shadow">
      <h3 className="text-lg font-semibold mb-2">在线用户</h3>
      <ul className="space-y-2">
        {users.map((user) => (
          <li key={user.id} className="flex items-center space-x-2">
            <span className={`w-2 h-2 rounded-full ${online[user.id] ? 'bg-green-500' : 'bg-gray-400'}`}></span>
            <span>{user.name}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default UserList;

8. 状态同步

已读状态

ChatWindow.jsx 中扩展:

js 复制代码
useEffect(() => {
  socket.on('read', ({ chatId: readChatId }) => {
    if (readChatId === chatId) {
      // 更新已读状态
    }
  });

  return () => {
    socket.off('read');
  };
}, [socket, chatId]);
在线状态

Home.jsx

js 复制代码
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { updateOnlineStatus } from '../features/users/usersSlice';
import ChatList from '../components/ChatList';
import UserList from '../components/UserList';

function Home({ socket }) {
  const dispatch = useDispatch();
  const users = useSelector((state) => state.users.users);

  useEffect(() => {
    socket.on('userStatus', (status) => {
      dispatch(updateOnlineStatus(status));
    });

    // 模拟用户数据
    dispatch(setUsers([{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]));

    return () => {
      socket.off('userStatus');
    };
  }, [socket, dispatch]);

  return (
    <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
      <div className="md:col-span-2">
        <ChatList />
      </div>
      <UserList />
    </div>
  );
}

export default Home;

9. 优化

消息缓存

已通过 React Query 实现,见 Chat.jsx

断线重连

main.jsx 中已启用 autoConnect,但可进一步优化:

js 复制代码
socket.on('disconnect', () => {
  console.log('Disconnected, attempting to reconnect...');
});

socket.on('reconnect', () => {
  console.log('Reconnected successfully');
});
防抖输入

InputBox.jsx

js 复制代码
import { useState, useCallback } from 'react';
import debounce from 'lodash/debounce';

function InputBox({ onSend, disabled }) {
  const [text, setText] = useState('');

  const debouncedSend = useCallback(
    debounce((value) => onSend(value), 300),
    [onSend]
  );

  const handleSend = () => {
    if (!text.trim() || disabled) return;
    debouncedSend(text);
    setText('');
  };

  return (
    <div className="p-4 border-t bg-white flex items-center space-x-2">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
        className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="输入消息..."
        disabled={disabled}
      />
      <button
        onClick={handleSend}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
        disabled={disabled}
      >
        发送
      </button>
    </div>
  );
}

export default InputBox;

安装 lodash

bash 复制代码
npm install lodash

10. 部署

构建项目
bash 复制代码
npm run build

生成 dist 文件夹。

部署到 AWS
  1. 创建 S3 桶
    在 AWS S3 控制台创建一个桶,上传 dist 文件夹内容,启用静态网站托管。
  2. 配置 CloudFront
    创建 CloudFront 分发,选择 S3 桶,设置默认根对象为 index.html
  3. 域名和 SSL
    配置自定义域名并通过 ACM 添加 SSL 证书。
  4. 访问
    部署完成后,通过 CloudFront 域名访问应用。

后端需部署到 AWS EC2 或 ECS,配置域名和 HTTPS。


练习:添加文件上传功能

为巩固所学,我们设计一个练习:为应用添加文件上传功能。

需求

  • 用户可上传文件并发送到聊天中。
  • 支持图片和视频预览。
  • 在输入框旁添加上传按钮。

实现步骤

  1. 创建 Upload 组件
    components/Upload.jsx 中实现文件选择和上传。
  2. 扩展 WebSocket
    修改后端支持文件数据传输。
  3. 消息预览
    MessageItem 中添加文件类型支持。
  4. 集成到输入框
    InputBox 中添加上传按钮。
示例代码

Upload.jsx

js 复制代码
import { useState } from 'react';

function Upload({ onUpload }) {
  const [file, setFile] = useState(null);

  const handleChange = (e) => {
    const selectedFile = e.target.files[0];
    if (selectedFile) setFile(selectedFile);
  };

  const handleUpload = () => {
    if (file) {
      onUpload(file);
      setFile(null);
    }
  };

  return (
    <div className="flex items-center space-x-2">
      <input
        type="file"
        onChange={handleChange}
        className="hidden"
        id="file-upload"
      />
      <label
        htmlFor="file-upload"
        className="px-3 py-1 bg-gray-200 rounded-lg cursor-pointer hover:bg-gray-300"
      >
        上传
      </label>
      {file && (
        <button
          onClick={handleUpload}
          className="px-3 py-1 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
        >
          发送
        </button>
      )}
    </div>
  );
}

export default Upload;

InputBox.jsx(更新):

js 复制代码
import { useState } from 'react';
import Upload from './Upload';

function InputBox({ onSend, disabled }) {
  const [text, setText] = useState('');

  const handleSend = () => {
    if (!text.trim() || disabled) return;
    onSend({ type: 'text', content: text });
    setText('');
  };

  const handleFileUpload = (file) => {
    const reader = new FileReader();
    reader.onload = () => {
      onSend({ type: 'file', content: reader.result, name: file.name });
    };
    reader.readAsDataURL(file);
  };

  return (
    <div className="p-4 border-t bg-white flex items-center space-x-2">
      <input
        type="text"
        value={text}
        onChange={(e) => setText(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && handleSend()}
        className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="输入消息..."
        disabled={disabled}
      />
      <Upload onUpload={handleFileUpload} />
      <button
        onClick={handleSend}
        className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
        disabled={disabled}
      >
        发送
      </button>
    </div>
  );
}

export default InputBox;

MessageItem.jsx(更新):

js 复制代码
import { motion } from 'framer-motion';

function MessageItem({ message }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
      className={`p-3 rounded-lg max-w-xs ${message.user === 'Me' ? 'bg-blue-500 text-white ml-auto' : 'bg-gray-200'}`}
    >
      {message.type === 'file' ? (
        message.content.startsWith('data:image') ? (
          <img src={message.content} alt={message.name} className="max-w-full rounded" />
        ) : message.content.startsWith('data:video') ? (
          <video controls src={message.content} className="max-w-full rounded" />
        ) : (
          <a href={message.content} download={message.name} className="underline">
            {message.name}
          </a>
        )
      ) : (
        <p>{message.content}</p>
      )}
      <span className="text-xs opacity-75">{new Date(message.id).toLocaleTimeString()}</span>
    </motion.div>
  );
}

export default MessageItem;
后端更新

server.js

js 复制代码
io.on('connection', (socket) => {
  socket.on('message', (data) => {
    io.emit('message', { ...data, id: Date.now(), user: socket.id });
  });
});

练习目标

通过此练习,您将学会在 WebSocket 中传输文件数据并实现文件预览。


注意事项

WebSocket 性能优化

  • 消息压缩 :在高并发场景下,使用 compression 中间件压缩消息。
  • 连接池管理:限制同时连接数,避免服务器过载。
  • 负载均衡:使用 AWS ELB 分发请求。
  • 心跳检测:定期发送心跳包检测连接状态。

安全考虑

  • 验证文件类型和大小,防止恶意上传。
  • 使用 HTTPS 加密 WebSocket 通信。

学习建议


结语

通过这个实时聊天应用项目,你完整地体验了一个 React 项目从需求分析到上线的全流程。你掌握了 WebSocket 集成、动画效果、状态同步、性能优化和 AWS 部署等核心技能。这些知识将成为你开发复杂应用的坚实基础。

相关推荐
DemonAvenger1 分钟前
Go sync.Pool 最佳实践:复用对象降低 GC 压力的技术文章
性能优化·架构·go
SleepyZone7 分钟前
Cline 源码浅析 - 从输入到输出
前端·ai编程·cline
Struggler28110 分钟前
pinia-基于monorepo的项目结构管理
前端
Struggler28115 分钟前
SSE的使用
前端
用户58061393930021 分钟前
前端文件下载实现深度解析:Blob与ObjectURL的完美协作
前端
Lin866624 分钟前
Vue 3 + TypeScript 组件类型推断失败问题完整解决方案
前端
coding随想24 分钟前
从零开始:前端开发者的SEO优化入门与实战
前端
前端工作日常27 分钟前
我理解的JSBridge
前端
Au_ust27 分钟前
前端模块化
前端
顺丰同城前端技术团队27 分钟前
还不会用 Charles?最后一遍了啊!
前端