‌React与Node.js高效互连指南‌

目录

  1. 简介:为何要将 React 与 Node.js 结合
  2. 环境准备与项目结构

    1. Node.js 后端侧:使用 Express 快速搭建
    2. React 前端侧:使用 Create React App
  3. RESTful API 设计与数据交互

    1. 后端:定义 RESTful 接口
    2. 前端:使用 Axios/Fetch 进行请求
    3. 跨域与代理配置
    4. 数据流向图解
  4. 实时通信:Socket.io 实战

    1. 后端:集成 Socket.io
    2. 前端:在 React 中使用 Socket.io Client
    3. 实时通信流程图解
  5. GraphQL 方式:Apollo Server 与 Apollo Client

    1. 后端:搭建 Apollo Server
    2. 前端:使用 Apollo Client 查询
    3. GraphQL 查询流程示意
  6. 身份验证与授权

    1. 后端:基于 JWT 的登录与中间件
    2. 前端:React 中存储与刷新令牌
    3. 流程图解:登录到授权数据获取
  7. 服务器端渲染(SSR)与同构应用

    1. Next.js 简介与示例
    2. 自定义 Express + React SSR
    3. SSR 渲染流程图解
  8. 性能优化与发布部署

    1. 前端性能:Code Splitting 与懒加载
    2. 后端性能:缓存与压缩
    3. 生产环境部署示例
  9. 总结与最佳实践

1. 简介:为何要将 React 与 Node.js 结合

现代 Web 应用对于交互速度、灵活性和扩展性要求越来越高。前后端分离的架构备受欢迎,其中 React 负责构建用户界面与交互体验,Node.js 则作为后端服务器提供数据 API 或实时通信功能。将二者高效组合可以带来以下好处:

  • 统一语言栈:前后端皆使用 JavaScript/TypeScript,减少学习成本与团队沟通成本。
  • 易于构建同构(Isomorphic)应用:可将 React 组件在服务器端渲染,提升首次渲染速度与 SEO 友好度。
  • 实时通信能力:借助 Node.js 的事件驱动与非阻塞特性,可在 React 中轻松实现 WebSocket、Socket.io 等双向通信。
  • 灵活扩展:可在 Node.js 中集成数据库、第三方服务、身份认证等,然后通过 RESTful 或 GraphQL 对外暴露。

本指南将从基础的 REST API、实时通信到 GraphQL、SSR 及发布部署,深入剖析如何让 React 与 Node.js 配合得更加高效。


2. 环境准备与项目结构

在动手之前,需要先搭建一个最简的 React 前端项目与 Node.js 后端项目,并演示它们如何协同运行。

2.1 Node.js 后端侧:使用 Express 快速搭建

  1. 新建目录并初始化

    mkdir react-node-guide
    cd react-node-guide
    mkdir server client
    cd server
    npm init -y
  2. 安装依赖

    npm install express cors body-parser jsonwebtoken socket.io apollo-server-express graphql
    • express:Node.js 最流行的 Web 框架
    • cors:处理跨域
    • body-parser:解析 JSON 请求体(Express 4.16+ 可内置)
    • jsonwebtoken:JWT 身份验证
    • socket.io:实时双向通信
    • apollo-server-expressgraphql:GraphQL 服务
  3. 项目目录结构

    server/
    ├── package.json
    ├── index.js          // 入口,整合 Express、Socket.io、GraphQL
    ├── routes/           // RESTful 路由
    │   └── user.js
    ├── controllers/      // 控制器逻辑
    │   └── userController.js
    ├── middlewares/      // 中间件
    │   └── authMiddleware.js
    └── schema/           // GraphQL 模式与解析器
        ├── typeDefs.js
        └── resolvers.js
  4. Express 服务器示例(index.js)

    // server/index.js
    
    import express from 'express';
    import http from 'http';
    import cors from 'cors';
    import { json } from 'body-parser';
    import { Server as SocketIOServer } from 'socket.io';
    import { ApolloServer } from 'apollo-server-express';
    import typeDefs from './schema/typeDefs.js';
    import resolvers from './schema/resolvers.js';
    import userRouter from './routes/user.js';
    
    const app = express();
    const server = http.createServer(app);
    const io = new SocketIOServer(server, {
      cors: { origin: 'http://localhost:3000', methods: ['GET', 'POST'] }
    });
    
    // 中间件
    app.use(cors({ origin: 'http://localhost:3000' }));
    app.use(json());
    
    // RESTful 路由
    app.use('/api/users', userRouter);
    
    // Socket.io 逻辑
    io.on('connection', (socket) => {
      console.log('新客户端已连接:', socket.id);
      socket.on('message', (msg) => {
        console.log('收到消息:', msg);
        io.emit('message', `Echo: ${msg}`);
      });
      socket.on('disconnect', () => {
        console.log('客户端已断开:', socket.id);
      });
    });
    
    // GraphQL 服务
    const apolloServer = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => ({ req }) });
    await apolloServer.start();
    apolloServer.applyMiddleware({ app, path: '/graphql', cors: false });
    
    // 启动服务器
    const PORT = process.env.PORT || 4000;
    server.listen(PORT, () => {
      console.log(`Express+Socket.io+GraphQL 服务已启动,端口 ${PORT}`);
    });
    • 该示例整合了:

      • /api/users RESTful 路由
      • Socket.io 实时通信
      • ApolloServer GraphQL
    • 后续各部分会详细拆解路由与中间件。

2.2 React 前端侧:使用 Create React App

  1. 进入 client 目录并初始化

    cd ../client
    npx create-react-app .   # 或使用 Vite: `npm init vite@latest . --template react`
  2. 安装前端依赖

    npm install axios socket.io-client @apollo/client graphql
    • axios:HTTP 请求库
    • socket.io-client:Socket.io 前端客户端
    • @apollo/clientgraphql:GraphQL 客户端
  3. 项目目录结构

    client/
    ├── package.json
    ├── src/
    │   ├── index.js        // React 入口
    │   ├── App.js
    │   ├── services/       // 封装 API 请求逻辑
    │   │   ├── api.js
    │   │   ├── socket.js
    │   │   └── graphql.js
    │   ├── components/     // 组件
    │   │   ├── UserList.jsx
    │   │   ├── ChatRoom.jsx
    │   │   └── GraphQLDemo.jsx
    │   └── ...             
    └── public/
  4. 启动脚本
    client/package.json 中添加:

    "proxy": "http://localhost:4000",
    "scripts": {
      "start": "react-scripts start",
      "build": "react-scripts build",
      "test": "react-scripts test"
    }
    • proxy 配置可直接将 /api 请求代理到 http://localhost:4000(避免开发阶段 CORS 问题)。

此时前后端工程已创建完毕。接下来将分别讲解如何进行 RESTful、Socket.io、GraphQL 等高效互连。


3. RESTful API 设计与数据交互

这是最常见的前后端数据交换方式:后端暴露 RESTful 接口(JSON 数据),前端使用 fetchaxios 发起请求并渲染数据。

3.1 后端:定义 RESTful 接口

以用户管理为例,后端提供基本的 CRUD 接口:

  1. 路由定义:server/routes/user.js

    // server/routes/user.js
    import express from 'express';
    import {
      getAllUsers,
      getUserById,
      createUser,
      updateUser,
      deleteUser
    } from '../controllers/userController.js';
    
    const router = express.Router();
    
    router.get('/', getAllUsers);        // GET /api/users
    router.get('/:id', getUserById);     // GET /api/users/:id
    router.post('/', createUser);        // POST /api/users
    router.put('/:id', updateUser);      // PUT /api/users/:id
    router.delete('/:id', deleteUser);   // DELETE /api/users/:id
    
    export default router;
  2. 控制器逻辑:server/controllers/userController.js

    // server/controllers/userController.js
    
    // 模拟内存中的用户数据
    let users = [
      { id: 1, name: 'Alice', email: 'alice@example.com' },
      { id: 2, name: 'Bob', email: 'bob@example.com' }
    ];
    let nextId = 3;
    
    export const getAllUsers = (req, res) => {
      res.json(users);
    };
    
    export const getUserById = (req, res) => {
      const id = parseInt(req.params.id);
      const user = users.find(u => u.id === id);
      if (user) {
        res.json(user);
      } else {
        res.status(404).json({ message: '用户未找到' });
      }
    };
    
    export const createUser = (req, res) => {
      const { name, email } = req.body;
      if (!name || !email) {
        return res.status(400).json({ message: '参数不完整' });
      }
      const newUser = { id: nextId++, name, email };
      users.push(newUser);
      res.status(201).json(newUser);
    };
    
    export const updateUser = (req, res) => {
      const id = parseInt(req.params.id);
      const { name, email } = req.body;
      const userIndex = users.findIndex(u => u.id === id);
      if (userIndex === -1) {
        return res.status(404).json({ message: '用户未找到' });
      }
      users[userIndex] = { id, name, email };
      res.json(users[userIndex]);
    };
    
    export const deleteUser = (req, res) => {
      const id = parseInt(req.params.id);
      const userIndex = users.findIndex(u => u.id === id);
      if (userIndex === -1) {
        return res.status(404).json({ message: '用户未找到' });
      }
      const deleted = users.splice(userIndex, 1);
      res.json(deleted[0]);
    };
  3. 启动后端

    cd server
    node index.js
    # 或:npm run dev(若使用 nodemon)

    此时后端已在 http://localhost:4000/api/users 提供 RESTful 接口。

3.2 前端:使用 Axios/Fetch 进行请求

在 React 中,我们通常创建一个封装好的 API 服务文件,统一管理 REST 请求。

  1. 封装 API 服务:client/src/services/api.js

    // client/src/services/api.js
    
    import axios from 'axios';
    
    // 由于 package.json 中已设置 "proxy": "http://localhost:4000",
    // 直接请求 "/api/users" 即可,无需写全地址。
    const API_BASE = '/api/users';
    
    export const fetchUsers = async () => {
      const res = await axios.get(API_BASE);
      return res.data; // 返回用户列表
    };
    
    export const fetchUserById = async (id) => {
      const res = await axios.get(`${API_BASE}/${id}`);
      return res.data;
    };
    
    export const createUser = async (user) => {
      const res = await axios.post(API_BASE, user);
      return res.data;
    };
    
    export const updateUser = async (id, user) => {
      const res = await axios.put(`${API_BASE}/${id}`, user);
      return res.data;
    };
    
    export const deleteUser = async (id) => {
      const res = await axios.delete(`${API_BASE}/${id}`);
      return res.data;
    };
  2. 在 React 组件中调用:client/src/components/UserList.jsx

    // client/src/components/UserList.jsx
    
    import React, { useEffect, useState } from 'react';
    import {
      fetchUsers,
      createUser,
      updateUser,
      deleteUser
    } from '../services/api';
    
    export default function UserList() {
      const [users, setUsers] = useState([]);
      const [newName, setNewName] = useState('');
      const [newEmail, setNewEmail] = useState('');
    
      useEffect(() => {
        loadUsers();
      }, []);
    
      const loadUsers = async () => {
        try {
          const data = await fetchUsers();
          setUsers(data);
        } catch (err) {
          console.error('加载用户失败:', err);
        }
      };
    
      const handleAdd = async () => {
        if (!newName || !newEmail) return;
        try {
          await createUser({ name: newName, email: newEmail });
          setNewName('');
          setNewEmail('');
          loadUsers();
        } catch (err) {
          console.error('创建用户失败:', err);
        }
      };
    
      const handleUpdate = async (id) => {
        const name = prompt('请输入新用户名:');
        const email = prompt('请输入新邮箱:');
        if (!name || !email) return;
        try {
          await updateUser(id, { name, email });
          loadUsers();
        } catch (err) {
          console.error('更新用户失败:', err);
        }
      };
    
      const handleDelete = async (id) => {
        if (!window.confirm('确认删除?')) return;
        try {
          await deleteUser(id);
          loadUsers();
        } catch (err) {
          console.error('删除用户失败:', err);
        }
      };
    
      return (
        <div>
          <h2>用户列表</h2>
          <ul>
            {users.map(user => (
              <li key={user.id}>
                {user.name} ({user.email}){' '}
                <button onClick={() => handleUpdate(user.id)}>编辑</button>{' '}
                <button onClick={() => handleDelete(user.id)}>删除</button>
              </li>
            ))}
          </ul>
          <h3>新增用户</h3>
          <input
            placeholder="姓名"
            value={newName}
            onChange={e => setNewName(e.target.value)}
          />
          <input
            placeholder="邮箱"
            value={newEmail}
            onChange={e => setNewEmail(e.target.value)}
          />
          <button onClick={handleAdd}>新增</button>
        </div>
      );
    }
  3. App.js 中引用组件

    // client/src/App.js
    
    import React from 'react';
    import UserList from './components/UserList';
    
    function App() {
      return (
        <div style={{ padding: '20px' }}>
          <h1>React + Node.js 用户管理示例</h1>
          <UserList />
        </div>
      );
    }
    
    export default App;

此时启动前端(npm start),在浏览器访问 http://localhost:3000,即可看到与后端协作的完整用户列表增删改查功能。

3.3 跨域与代理配置

在开发阶段,前端运行在 localhost:3000,后端运行在 localhost:4000,会遇到跨域(CORS)问题。解决方法:

  1. 后端启用 CORS

    // server/index.js 中已包含:
    app.use(cors({ origin: 'http://localhost:3000' }));
  2. 前端使用 proxy
    client/package.json 中添加:

    "proxy": "http://localhost:4000"
    • 这样对 /api/... 的请求会被 CRA 自动代理到后端,无需再写完整 URL(如 http://localhost:4000/api/users)。
    • 若在生产环境,需要自行在 Nginx 或后端做代理配置。

3.4 数据流向图解

React 浏览器 (http://localhost:3000)
┌─────────────────────────────────────────┐
│ 点击 "加载用户"                         │
│ axios.get('/api/users')                │
└───────────────┬─────────────────────────┘
                │(1)发起 HTTP GET 请求
                ▼
    ┌────────────────────────────────┐
    │  Node.js/Express (http://localhost:4000) │
    │  app.use('/api/users', userRouter)       │
    └───────────────┬─────────────────────────┘
                    │(2)转到 routes/user.js
                    └──────────────────────────────────┐
                                                       │
                                  ┌────────────────────▼────────────────────┐
                                  │ userController.getAllUsers (读取 users)    │
                                  └───────────────┬───────────────────────────┘
                                                  │(3)返回 JSON 数据
                                                  ▼
                                        ┌─────────────────────────────┐
                                        │   Express 返回 JSON 到前端   │
                                        └───────────────┬─────────────┘
                                                        │(4)axios 收到数据
                                                        ▼
                                        ┌─────────────────────────────┐
                                        │    React 更新状态并渲染      │
                                        │    setUsers([...])          │
                                        └─────────────────────────────┘
  • (1) React 发起请求;
  • (2) 请求到达 Express,交给对应路由;
  • (3) 控制器读取并返回数据;
  • (4) React 收到数据并渲染列表。

4. 实时通信:Socket.io 实战

有些场景(如聊天室、协同编辑、实时通知)需要做双向、低延迟的数据传输。此时可以使用 Socket.io,底层基于 WebSocket,兼容性更好,API 更友好。

4.1 后端:集成 Socket.io

我们在之前的 server/index.js 中已简要演示过,这里再做补充说明。

// server/index.js (摘录)
import http from 'http';
import { Server as SocketIOServer } from 'socket.io';

const app = express();
const server = http.createServer(app);

// 创建 Socket.io 服务器
const io = new SocketIOServer(server, {
  cors: { origin: 'http://localhost:3000', methods: ['GET','POST'] }
});

// 监听连接
io.on('connection', (socket) => {
  console.log('客户端已连接:', socket.id);

  // 收到客户端发送的 "chat message"
  socket.on('chat message', (msg) => {
    console.log('收到消息:', msg);
    // 广播给所有客户端
    io.emit('chat message', msg);
  });

  socket.on('disconnect', () => {
    console.log('客户端断开:', socket.id);
  });
});

// 启动 server.listen(...)
  • 在创建 SocketIOServer 时,传入了跨域配置,以允许前端(localhost:3000)连接。
  • 当客户端通过 io.connect 建立连接后,后端就可以在 connection 回调里监听并发送事件。

4.2 前端:在 React 中使用 Socket.io Client

  1. 封装 Socket 服务:client/src/services/socket.js

    // client/src/services/socket.js
    
    import { io } from 'socket.io-client';
    
    // 直接连接到后端 Socket.io 地址
    const socket = io('http://localhost:4000');
    
    export default socket;
  2. 创建聊天室组件:client/src/components/ChatRoom.jsx

    // client/src/components/ChatRoom.jsx
    
    import React, { useEffect, useState } from 'react';
    import socket from '../services/socket';
    
    export default function ChatRoom() {
      const [messages, setMessages] = useState([]);
      const [input, setInput] = useState('');
    
      useEffect(() => {
        // 监听后端广播的消息
        socket.on('chat message', (msg) => {
          setMessages(prev => [...prev, msg]);
        });
    
        // 组件卸载时移除监听
        return () => {
          socket.off('chat message');
        };
      }, []);
    
      const handleSend = () => {
        if (input.trim() === '') return;
        // 发送事件到后端
        socket.emit('chat message', input);
        setInput('');
      };
    
      return (
        <div style={{ padding: '20px', border: '1px solid #ccc' }}>
          <h2>聊天室</h2>
          <div
            style={{
              width: '100%',
              height: '300px',
              border: '1px solid #ddd',
              overflowY: 'auto',
              padding: '10px'
            }}
          >
            {messages.map((msg, idx) => (
              <div key={idx}>{msg}</div>
            ))}
          </div>
          <input
            style={{ width: '80%', marginRight: '10px' }}
            value={input}
            onChange={(e) => setInput(e.target.value)}
            placeholder="输入聊天内容..."
          />
          <button onClick={handleSend}>发送</button>
        </div>
      );
    }
  3. App.js 中引用聊天室组件

    // client/src/App.js
    
    import React from 'react';
    import ChatRoom from './components/ChatRoom';
    
    function App() {
      return (
        <div style={{ padding: '20px' }}>
          <h1>React + Node.js 实时聊天示例</h1>
          <ChatRoom />
        </div>
      );
    }
    
    export default App;

4.3 实时通信流程图解

React 浏览器 (http://localhost:3000)
┌─────────────────────────────┐
│    socket = io("...")       │
│    socket.on("chat message")│
└─────────────┬───────────────┘
              │(1)建立 WebSocket 连接
              ▼
    ┌─────────────────────────────────┐
    │  Node.js/Express + Socket.io    │
    │  io.on("connection", ...)       │
    └─────────────┬───────────────────┘
                  │(2)连接成功
                  ▼
┌────────────────────────────────────────┐
│ 前端 socket.emit("chat message", msg)  │
└─────────────┬──────────────────────────┘
              │(3)发送消息给后端
              ▼
   ┌─────────────────────────────────┐
   │ 后端 io.on("chat message", ...) │
   │ console.log(msg)                │
   │ io.emit("chat message", msg)    │
   └─────────────┬───────────────────┘
                 │(4)广播消息给所有客户端
                 ▼
┌────────────────────────────────────────┐
│ 前端 socket.on("chat message", newMsg)│
│ 将 newMsg append 到消息列表           │
└────────────────────────────────────────┘
  • (1) 前端初始化 Socket.io 客户端并向后端发起 WebSocket 连接;
  • (2) 后端 io.on("connection") 捕获新连接;
  • (3) 前端调用 socket.emit("chat message", msg) 发送消息;
  • (4) 后端接收到消息后,使用 io.emit(...) 广播给所有已连接客户端;
  • (5) 前端 socket.on("chat message") 收到并更新 UI。

5. GraphQL 方式:Apollo Server 与 Apollo Client

GraphQL 作为替代 REST 的数据查询方式,也可与 React+Node.js 高效配合。

5.1 后端:搭建 Apollo Server

  1. GraphQL 模式定义:server/schema/typeDefs.js

    // server/schema/typeDefs.js
    
    import { gql } from 'apollo-server-express';
    
    const typeDefs = gql`
      type User {
        id: ID!
        name: String!
        email: String!
      }
    
      type Query {
        users: [User!]!
        user(id: ID!): User
      }
    
      type Mutation {
        createUser(name: String!, email: String!): User!
        updateUser(id: ID!, name: String!, email: String!): User!
        deleteUser(id: ID!): User!
      }
    `;
    
    export default typeDefs;
  2. Resolvers 实现:server/schema/resolvers.js

    // server/schema/resolvers.js
    
    let users = [
      { id: '1', name: 'Alice', email: 'alice@example.com' },
      { id: '2', name: 'Bob', email: 'bob@example.com' }
    ];
    
    export default {
      Query: {
        users: () => users,
        user: (_, { id }) => users.find(u => u.id === id)
      },
      Mutation: {
        createUser: (_, { name, email }) => {
          const newUser = { id: String(users.length + 1), name, email };
          users.push(newUser);
          return newUser;
        },
        updateUser: (_, { id, name, email }) => {
          const index = users.findIndex(u => u.id === id);
          if (index === -1) throw new Error('用户未找到');
          users[index] = { id, name, email };
          return users[index];
        },
        deleteUser: (_, { id }) => {
          const index = users.findIndex(u => u.id === id);
          if (index === -1) throw new Error('用户未找到');
          const [deleted] = users.splice(index, 1);
          return deleted;
        }
      }
    };
  3. index.js 中集成 ApolloServer(已示例)

    // server/index.js 中摘要
    import typeDefs from './schema/typeDefs.js';
    import resolvers from './schema/resolvers.js';
    // ...
    const apolloServer = new ApolloServer({ typeDefs, resolvers, context: ({ req }) => ({ req }) });
    await apolloServer.start();
    apolloServer.applyMiddleware({ app, path: '/graphql', cors: false });

此时 GraphQL 服务在 http://localhost:4000/graphql 提供交互式 IDE(GraphQL Playground),可以直接执行查询与变更。

5.2 前端:使用 Apollo Client 查询

  1. 配置 Apollo Client:client/src/services/graphql.js

    // client/src/services/graphql.js
    
    import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
    
    const client = new ApolloClient({
      uri: 'http://localhost:4000/graphql',
      cache: new InMemoryCache()
    });
    
    export default client;
    
    // 定义常用查询/变更
    export const GET_USERS = gql`
      query GetUsers {
        users {
          id
          name
          email
        }
      }
    `;
    
    export const CREATE_USER = gql`
      mutation CreateUser($name: String!, $email: String!) {
        createUser(name: $name, email: $email) {
          id
          name
          email
        }
      }
    `;
    // 其余 UPDATE、DELETE 可类似定义
  2. index.js 中包裹 ApolloProvider

    // client/src/index.js
    
    import React from 'react';
    import ReactDOM from 'react-dom';
    import App from './App';
    import { ApolloProvider } from '@apollo/client';
    import client from './services/graphql';
    
    ReactDOM.render(
      <React.StrictMode>
        <ApolloProvider client={client}>
          <App />
        </ApolloProvider>
      </React.StrictMode>,
      document.getElementById('root')
    );
  3. GraphQL 示例组件:client/src/components/GraphQLDemo.jsx

    // client/src/components/GraphQLDemo.jsx
    
    import React, { useState } from 'react';
    import { useQuery, useMutation } from '@apollo/client';
    import { GET_USERS, CREATE_USER } from '../services/graphql';
    
    export default function GraphQLDemo() {
      const { loading, error, data, refetch } = useQuery(GET_USERS);
      const [createUser] = useMutation(CREATE_USER);
    
      const [name, setName] = useState('');
      const [email, setEmail] = useState('');
    
      const handleCreate = async () => {
        if (!name || !email) return;
        await createUser({ variables: { name, email } });
        setName('');
        setEmail('');
        refetch();
      };
    
      if (loading) return <p>加载中...</p>;
      if (error) return <p>出错:{error.message}</p>;
    
      return (
        <div style={{ padding: '20px', border: '1px solid #ccc' }}>
          <h2>GraphQL 用户列表</h2>
          <ul>
            {data.users.map(user => (
              <li key={user.id}>
                {user.name} ({user.email})
              </li>
            ))}
          </ul>
          <h3>新增用户</h3>
          <input
            placeholder="姓名"
            value={name}
            onChange={(e) => setName(e.target.value)}
          />
          <input
            placeholder="邮箱"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
          <button onClick={handleCreate}>创建</button>
        </div>
      );
    }
  4. App.js 中引用

    // client/src/App.js
    
    import React from 'react';
    import GraphQLDemo from './components/GraphQLDemo';
    
    function App() {
      return (
        <div style={{ padding: '20px' }}>
          <h1>React + Node.js GraphQL 示例</h1>
          <GraphQLDemo />
        </div>
      );
    }
    
    export default App;

5.3 GraphQL 查询流程示意

React 浏览器 (http://localhost:3000)
┌──────────────────────────────────────┐
│ useQuery(GET_USERS) -> 触发网络请求 │
│ POST http://localhost:4000/graphql  │
│ { query: "...", variables: { } }    │
└─────────────┬────────────────────────┘
              │(1)发送 GraphQL 查询
              ▼
    ┌───────────────────────────────────┐
    │  Node.js/Express + ApolloServer    │
    │  接收 POST /graphql 字节流          │
    └─────────────┬──────────────────────┘
                  │(2)解析 GraphQL 查询
                  ▼
    ┌───────────────────────────────────┐
    │  执行对应 resolver,读取 User 数组  │
    │  将数据组装成符合 GraphQL 规范的 JSON │
    └─────────────┬──────────────────────┘
                  │(3)返回 { data: { users: [...] } }
                  ▼
    ┌───────────────────────────────────┐
    │  React Apollo Client 收到数据     │
    │  更新组件状态并渲染               │
    └───────────────────────────────────┘
  • GraphQL 将请求与响应都以 JSON 格式表达,更加灵活,但需要定义 Schema 和 Resolver,适合接口复杂、联合查询需求多的场景。

6. 身份验证与授权

对于大多数应用,都需要用户登录、身份验证和授权。这里以 JWT(JSON Web Token)为例,演示前后端如何协作完成登录到获取受保护数据的流程。

6.1 后端:基于 JWT 的登录与中间件

  1. 安装依赖

    npm install bcrypt jsonwebtoken
    • bcrypt:用于对密码进行哈希
    • jsonwebtoken:用于签发与验证 JWT
  2. 用户登录路由:server/routes/auth.js

    // server/routes/auth.js
    
    import express from 'express';
    import { loginUser } from '../controllers/authController.js';
    const router = express.Router();
    
    router.post('/login', loginUser); // POST /api/auth/login
    
    export default router;
  3. 登录控制器:server/controllers/authController.js

    // server/controllers/authController.js
    
    import bcrypt from 'bcrypt';
    import jwt from 'jsonwebtoken';
    
    // 模拟存储的用户(正式项目应存入数据库)
    const mockUser = {
      id: 1,
      username: 'admin',
      // bcrypt.hashSync('password123', 10)
      passwordHash: '$2b$10$abcdefghijk1234567890abcdefghijklmnopqrstuv'
    };
    
    const JWT_SECRET = 'your_jwt_secret_key';
    
    export const loginUser = async (req, res) => {
      const { username, password } = req.body;
      if (username !== mockUser.username) {
        return res.status(401).json({ message: '用户名或密码错误' });
      }
      const match = await bcrypt.compare(password, mockUser.passwordHash);
      if (!match) {
        return res.status(401).json({ message: '用户名或密码错误' });
      }
      // 签发 JWT,有效期 1 小时
      const token = jwt.sign({ id: mockUser.id, username }, JWT_SECRET, {
        expiresIn: '1h'
      });
      res.json({ token });
    };
    
    // 中间件:验证 JWT
    export const authMiddleware = (req, res, next) => {
      const authHeader = req.headers.authorization;
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ message: '缺少令牌' });
      }
      const token = authHeader.split(' ')[1];
      try {
        const payload = jwt.verify(token, JWT_SECRET);
        req.user = payload; // 将 payload(id、username)挂载到 req.user
        next();
      } catch (err) {
        return res.status(401).json({ message: '无效或过期令牌' });
      }
    };
  4. 受保护路由示例:server/routes/protected.js

    // server/routes/protected.js
    
    import express from 'express';
    import { authMiddleware } from '../controllers/authController.js';
    
    const router = express.Router();

// GET /api/protected/profile
router.get('/profile', authMiddleware, (req, res) => {
// req.user 已包含 id、username
res.json({ id: req.user.id, username: req.user.username });
});

export default router;


5. **在 `index.js` 中挂载**  
```js
import authRouter from './routes/auth.js';
import protectedRouter from './routes/protected.js';

app.use('/api/auth', authRouter);
app.use('/api/protected', protectedRouter);

此时后端已具备以下接口:

  • POST /api/auth/login:接收 { username, password },返回 { token }
  • GET /api/protected/profile:需在头部 Authorization: Bearer <token>,返回用户信息

6.2 前端:React 中存储与刷新令牌

  1. 封装登录逻辑:client/src/services/auth.js

    // client/src/services/auth.js
    
    import axios from 'axios';
    
    const API_AUTH = '/api/auth';
    
    // 登录并保存 token 到 localStorage
    export const login = async (username, password) => {
      const res = await axios.post(`${API_AUTH}/login`, { username, password });
      const { token } = res.data;
      localStorage.setItem('token', token);
      return token;
    };
    
    // 获取用户 profile
    export const fetchProfile = async () => {
      const token = localStorage.getItem('token');
      const res = await axios.get('/api/protected/profile', {
        headers: { Authorization: `Bearer ${token}` }
      });
      return res.data;
    };
    
    // 退出登录
    export const logout = () => {
      localStorage.removeItem('token');
    };
  2. 登录组件:client/src/components/Login.jsx

    // client/src/components/Login.jsx
    
    import React, { useState } from 'react';
    import { login, fetchProfile, logout } from '../services/auth';
    
    export default function Login() {
      const [username, setUsername] = useState('');
      const [password, setPassword] = useState('');
      const [profile, setProfile] = useState(null);
      const [error, setError] = useState('');
    
      const handleLogin = async () => {
        try {
          await login(username, password);
          const data = await fetchProfile();
          setProfile(data);
          setError('');
        } catch (err) {
          setError(err.response?.data?.message || '登录失败');
        }
      };
    
      const handleLogout = () => {
        logout();
        setProfile(null);
      };
    
      if (profile) {
        return (
          <div>
            <h2>欢迎,{profile.username}!</h2>
            <button onClick={handleLogout}>退出登录</button>
          </div>
        );
      }
    
      return (
        <div style={{ padding: '20px', border: '1px solid #ccc' }}>
          <h2>登录</h2>
          {error && <p style={{ color: 'red' }}>{error}</p>}
          <input
            placeholder="用户名"
            value={username}
            onChange={(e) => setUsername(e.target.value)}
          />
          <input
            placeholder="密码"
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
          <button onClick={handleLogin}>登录</button>
        </div>
      );
    }
  3. 流程图解:登录到获取受保护数据

    React 浏览器
    ┌─────────────────────────────────────────┐
    │ 点击 登录 按钮 -> login(username,password) │
    └───────────────┬─────────────────────────┘
                    │(1)POST /api/auth/login
                    ▼
        ┌───────────────────────────────────┐
        │  Node.js Express -> authController.loginUser │
        │  验证用户 -> 签发 JWT -> 返回 {token} │
        └───────────────┬───────────────────┘
                        │(2)前端收到 {token} 并保存在 localStorage
                        ▼
        ┌──────────────────────────────────────────┐
        │ React 调用 fetchProfile() -> GET /api/protected/profile │
        │ headers: { Authorization: "Bearer <token>" }           │
        └───────────────┬──────────────────────────┘
                        │(3)Node.js authMiddleware 验证 JWT
                        ▼
        ┌──────────────────────────────────────────┐
        │ profile 路由返回用户信息 -> React 接收并渲染 │
        └──────────────────────────────────────────┘
  • (1) 前端调用 login,后端验证并返回 JWT;
  • (2) 前端将 JWT 存储在 localStorage
  • (3) 前端调用受保护接口,附带 Authorization 头部;
  • (4) 后端 authMiddleware 验证令牌通过后,返回用户数据。

6.3 安全提示

  • 使用 HTTPS:生产环境务必使用 HTTPS,防止令牌在网络中被窃取。
  • 短令牌有效期:结合 Refresh Token 机制,减少令牌被滥用的风险。
  • HTTP Only Cookie:也可将 JWT 存储在 Cookie 中,并配置 httpOnlysecure 等属性,提高安全性。

7. 服务器端渲染(SSR)与同构应用

除了纯前后端分离,有时希望将 React 组件在服务器端渲染(SSR),提升首屏渲染速度、SEO 友好度,或者需要同构(Isomorphic)渲染。这里介绍两种方式:Next.js 以及自定义 Express + React SSR

7.1 Next.js 简介与示例

Next.js 是基于 React 的 SSR 框架,开箱即用,无需手动配置 Webpack、Babel。它也可以集成自定义 API 路由,甚至将之前的 Express 代码嵌入其中。

  1. 初始化 Next.js 项目

    cd react-node-guide/client
    npx create-next-app@latest ssr-demo
    cd ssr-demo
  2. 编写页面与 API

    • pages/index.js:React 页面,可使用 getServerSideProps 获取数据。
    • pages/api/users.js:Next.js 内置 API 路由,可直接处理 /api/users 请求。
  3. 启动 Next.js

    npm run dev

    此时 Next.js 自带的 SSR 功能在 http://localhost:3000 实现,API 路由在 http://localhost:3000/api/users 可用。

  4. 示例:pages/index.js

    // ssr-demo/pages/index.js
    
    import React from 'react';
    import axios from 'axios';
    
    export async function getServerSideProps() {
      const res = await axios.get('http://localhost:3000/api/users');
      return { props: { users: res.data } };
    }
    
    export default function Home({ users }) {
      return (
        <div style={{ padding: '20px' }}>
          <h1>Next.js SSR 用户列表</h1>
          <ul>
            {users.map(user => (
              <li key={user.id}>{user.name} ({user.email})</li>
            ))}
          </ul>
        </div>
      );
    }

7.2 自定义 Express + React SSR

如果不想引入完整 Next.js,也可以手动配置 Express + React SSR,过程如下:

  1. 安装依赖

    cd react-node-guide/server
    npm install react react-dom @babel/core @babel/preset-env @babel/preset-react babel-register ignore-styles
    • babel-register:在运行时动态转译 React 组件
    • ignore-styles:让服务器端在 require CSS/LESS 等文件时忽略
  2. 配置 Babel:server/babel.config.js

    module.exports = {
      presets: [
        ['@babel/preset-env', { targets: { node: 'current' } }],
        '@babel/preset-react'
      ]
    };
  3. 创建 SSR 入口:server/ssr.js

    // server/ssr.js
    
    // 在 Node.js 环境下注册 Babel,让后续 `require` 支持 JSX
    require('ignore-styles');
    require('@babel/register')({
      ignore: [/(node_modules)/],
      presets: ['@babel/preset-env', '@babel/preset-react']
    });
    
    import path from 'path';
    import fs from 'fs';
    import express from 'express';
    import React from 'react';
    import ReactDOMServer from 'react-dom/server';
    import App from '../client/src/App'; // 假设 CRA 的 App.js
    
    const app = express();
    
    // 提供静态资源(由 CRA build 生成)
    app.use(express.static(path.resolve(__dirname, '../client/build')));
    
    app.get('/*', (req, res) => {
      const appString = ReactDOMServer.renderToString(<App />);
      const indexFile = path.resolve(__dirname, '../client/build/index.html');
      fs.readFile(indexFile, 'utf8', (err, data) => {
        if (err) {
          console.error('读取 HTML 模板失败:', err);
          return res.status(500).send('内部错误');
        }
        // 将渲染好的 appString 塞回到 <div id="root"></div> 中
        const html = data.replace('<div id="root"></div>', `<div id="root">${appString}</div>`);
        return res.send(html);
      });
    });
    
    const PORT = process.env.PORT || 4000;
    app.listen(PORT, () => {
      console.log(`Express SSR 已启动,端口 ${PORT}`);
    });
  4. 构建前端静态资源

    cd client
    npm run build

    这会在 client/build 目录下生成静态文件,然后 SSR 服务器即可将其作为模板注入渲染后的内容并返回给浏览器。

7.3 SSR 渲染流程图解

浏览器请求 http://localhost:4000/
┌────────────────────────────┐
│ Express SSR (server/ssr.js) │
└───────────────┬────────────┘
                │(1)捕获所有 GET 请求
                ▼
     ┌─────────────────────────────────┐
     │ ReactDOMServer.renderToString   │
     │ 生成 HTML 字符串(<App />)     │
     └───────────────┬─────────────────┘
                     │
                     ▼
         ┌─────────────────────────────────┐
         │ 读取 client/build/index.html      │
         │ 并将 <div id="root"> 替换为渲染结果 │
         └───────────────┬─────────────────┘
                         │
                         ▼
         ┌─────────────────────────────────┐
         │ 返回包含首屏渲染内容的 HTML     │
         └─────────────────────────────────┘
  • (1) 浏览器首次请求时,Express SSR 将 React 组件渲染为 HTML,嵌入到模板中后返回。
  • (2) 浏览器接收到首屏 HTML,并继续下载后续静态资源(JS、CSS),在客户端完成 React 的 hydrating(挂载)过程。

8. 性能优化与发布部署

8.1 前端性能:Code Splitting 与懒加载

  1. Route-based Splitting(按路由分割)

    • 使用 React.lazy 和 Suspense 来按需加载组件:

      import React, { Suspense, lazy } from 'react';
      import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
      
      const Home = lazy(() => import('./pages/Home'));
      const Users = lazy(() => import('./pages/Users'));
      
      function App() {
        return (
          <Router>
            <Suspense fallback={<div>加载中...</div>}>
              <Switch>
                <Route path="/users" component={Users} />
                <Route path="/" component={Home} />
              </Switch>
            </Suspense>
          </Router>
        );
      }
    • CRA 默认支持 Code Splitting,打包时会自动生成多个 JS chunk。
  2. Bundle 分析与优化

    • 安装 source-map-explorer

      npm install --save-dev source-map-explorer
    • 构建后运行:

      npm run build
      npx source-map-explorer 'build/static/js/*.js'
    • 分析各依赖包体积,尽量剔除不必要的重型库(如 lodash 全量、moment.js),可用 lodash-esdayjs 等轻量替代。

8.2 后端性能:缓存与压缩

  1. Gzip 压缩

    npm install compression

    在 Express 中启用:

    import compression from 'compression';
    app.use(compression());
    • 可自动对响应进行 Gzip 压缩,减少网络传输大小;若使用 HTTPS,还可启用 Brotli(shrink-ray-current 等库)。
  2. 接口缓存

    • 使用内存缓存或 Redis 缓存常用数据,提高响应速度。
    • 示例:在获取用户列表时,若数据变化不频繁,可先检查缓存:

      import Redis from 'ioredis';
      const redis = new Redis();
      
      export const getAllUsers = async (req, res) => {
        const cacheKey = 'users:list';
        const cached = await redis.get(cacheKey);
        if (cached) {
          return res.json(JSON.parse(cached));
        }
        // 模拟 DB 查询
        const data = users;
        await redis.set(cacheKey, JSON.stringify(data), 'EX', 60); // 缓存 60s
        res.json(data);
      };
  3. 使用 PM2 或 Docker 进程管理

    • 使用 PM2 启动 Node.js,并开启日志、进程守护:

      npm install -g pm2
      pm2 start index.js --name react-node-server -i max
    • 或将后端打包为 Docker 镜像,在 Kubernetes / Docker Swarm 中编排多副本实例,保证高可用。

8.3 生产环境部署示例

  1. 部署前端静态文件

    • 在前端项目执行 npm run build,将 build/ 目录下的内容上传到 CDN 或静态服务器(Nginx)。
    • Nginx 配置示例:

      server {
        listen 80;
        server_name your-domain.com;
      
        location / {
          root /var/www/react-app/build;
          index index.html;
          try_files $uri /index.html;
        }
      
        # 代理 API 请求到后端
        location /api/ {
          proxy_pass http://backend:4000/api/;
        }
      
        location /graphql {
          proxy_pass http://backend:4000/graphql;
        }
      
        # WebSocket 转发
        location /socket.io/ {
          proxy_pass http://backend:4000/socket.io/;
          proxy_http_version 1.1;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection "Upgrade";
        }
      }
  2. 部署后端

    • 构建为 Docker 镜像:

      # server/Dockerfile
      
      FROM node:16-alpine
      WORKDIR /app
      COPY package*.json ./
      RUN npm ci --only=production
      COPY . .
      EXPOSE 4000
      CMD ["node", "index.js"]
    • 构建并运行:

      cd server
      docker build -t react-node-server .
      docker run -d --name react-node-server -p 4000:4000 react-node-server
    • 若使用 Kubernetes,可通过 Deployment/Service/Ingress 等方式发布。

9. 总结与最佳实践

  1. 前后端分离与统一语言栈

    • 使用 React 构建 SPA,Node.js 提供 API,实现职责分离;
    • 前后端皆用 JavaScript/TypeScript,提高团队协作效率。
  2. 合理选择通信方式

    • RESTful:简单易上手,适合大多数 CRUD 场景;
    • Socket.io:实时双向通信,适合聊天室、协同编辑;
    • GraphQL:精确按需查询,避免过多/过少数据,适合数据结构复杂或多客户端需求场景;
    • SSR:提升首屏速度与 SEO,适合新闻、博客、电商等需搜索引擎友好的应用。
  3. 跨域与代理

    • 开发阶段可通过 CRA 的 proxy 或 Express 中启用 CORS;
    • 生产环境应使用 Nginx/Apache 做统一代理,并配置 HTTPS,提升安全性。
  4. 身份验证与授权

    • 推荐基于 JWT,将令牌存储在 HTTP-only Cookie 或 localStorage(注意 XSS 风险);
    • 对受保护路由使用中间件校验令牌;
    • 前端在切换路由时检查登录状态并动态渲染界面。
  5. 性能优化

    • 前端利用 Code Splitting、Lazy Loading、Tree Shaking 减少打包体积;
    • 后端启用 Gzip/Brotli、缓存、连接池等;
    • 生产环境使用 PM2 或 Docker/K8s 做负载均衡与容错。
  6. 安全性注意事项

    • 对用户输入严防 SQL 注入、XSS、CSRF 等;
    • 后端不将敏感信息(如密码、密钥)返回给前端;
    • 使用 HTTPS,禁用不安全的 Cipher。
  7. 开发流程

    • 使用 ESLint/Prettier 统一代码风格;
    • 使用 Git Hooks(如 Husky)保证提交质量;
    • 编写单元测试(Jest、Mocha)、集成测试(Supertest、Cypress)提高稳定性。

通过本文的详细示例与图解,你已经掌握了 React 与 Node.js 在多种场景下的高效互连方式:从最简单的 RESTful 数据交互,到实时通信、GraphQL 查询,乃至 SSR。希望这份指南能帮助你在实际项目中快速上手并应用最佳实践,构建出高性能、可扩展、安全可靠的全栈应用。

评论已关闭

推荐阅读

DDPG 模型解析,附Pytorch完整代码
2024年11月24日
DQN 模型解析,附Pytorch完整代码
2024年11月24日
AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日