React与Node.js高效互连指南
目录
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 快速搭建
新建目录并初始化
mkdir react-node-guide cd react-node-guide mkdir server client cd server npm init -y
安装依赖
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-express
与graphql
:GraphQL 服务
项目目录结构
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
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
进入 client 目录并初始化
cd ../client npx create-react-app . # 或使用 Vite: `npm init vite@latest . --template react`
安装前端依赖
npm install axios socket.io-client @apollo/client graphql
axios
:HTTP 请求库socket.io-client
:Socket.io 前端客户端@apollo/client
与graphql
:GraphQL 客户端
项目目录结构
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/
启动脚本
在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 数据),前端使用 fetch
或 axios
发起请求并渲染数据。
3.1 后端:定义 RESTful 接口
以用户管理为例,后端提供基本的 CRUD 接口:
路由定义:
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;
控制器逻辑:
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]); };
启动后端
cd server node index.js # 或:npm run dev(若使用 nodemon)
此时后端已在
http://localhost:4000/api/users
提供 RESTful 接口。
3.2 前端:使用 Axios/Fetch 进行请求
在 React 中,我们通常创建一个封装好的 API 服务文件,统一管理 REST 请求。
封装 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; };
在 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> ); }
在
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)问题。解决方法:
后端启用 CORS
// server/index.js 中已包含: app.use(cors({ origin: 'http://localhost:3000' }));
前端使用
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
封装 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;
创建聊天室组件:
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> ); }
在
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
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;
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; } } };
在
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 查询
配置 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 可类似定义
在
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') );
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> ); }
在
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 的登录与中间件
安装依赖
npm install bcrypt jsonwebtoken
bcrypt
:用于对密码进行哈希jsonwebtoken
:用于签发与验证 JWT
用户登录路由:
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;
登录控制器:
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: '无效或过期令牌' }); } };
受保护路由示例:
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 中存储与刷新令牌
封装登录逻辑:
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'); };
登录组件:
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> ); }
流程图解:登录到获取受保护数据
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 中,并配置
httpOnly
、secure
等属性,提高安全性。
7. 服务器端渲染(SSR)与同构应用
除了纯前后端分离,有时希望将 React 组件在服务器端渲染(SSR),提升首屏渲染速度、SEO 友好度,或者需要同构(Isomorphic)渲染。这里介绍两种方式:Next.js 以及自定义 Express + React SSR。
7.1 Next.js 简介与示例
Next.js 是基于 React 的 SSR 框架,开箱即用,无需手动配置 Webpack、Babel。它也可以集成自定义 API 路由,甚至将之前的 Express 代码嵌入其中。
初始化 Next.js 项目
cd react-node-guide/client npx create-next-app@latest ssr-demo cd ssr-demo
编写页面与 API
pages/index.js
:React 页面,可使用getServerSideProps
获取数据。pages/api/users.js
:Next.js 内置 API 路由,可直接处理/api/users
请求。
启动 Next.js
npm run dev
此时 Next.js 自带的 SSR 功能在
http://localhost:3000
实现,API 路由在http://localhost:3000/api/users
可用。示例:
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,过程如下:
安装依赖
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 等文件时忽略
配置 Babel:
server/babel.config.js
module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-react' ] };
创建 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}`); });
构建前端静态资源
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 与懒加载
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。
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-es
、dayjs
等轻量替代。
8.2 后端性能:缓存与压缩
Gzip 压缩
npm install compression
在 Express 中启用:
import compression from 'compression'; app.use(compression());
- 可自动对响应进行 Gzip 压缩,减少网络传输大小;若使用 HTTPS,还可启用 Brotli(
shrink-ray-current
等库)。
- 可自动对响应进行 Gzip 压缩,减少网络传输大小;若使用 HTTPS,还可启用 Brotli(
接口缓存
- 使用内存缓存或 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); };
使用 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 生产环境部署示例
部署前端静态文件
- 在前端项目执行
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"; } }
- 在前端项目执行
部署后端
构建为 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. 总结与最佳实践
前后端分离与统一语言栈
- 使用 React 构建 SPA,Node.js 提供 API,实现职责分离;
- 前后端皆用 JavaScript/TypeScript,提高团队协作效率。
合理选择通信方式
- RESTful:简单易上手,适合大多数 CRUD 场景;
- Socket.io:实时双向通信,适合聊天室、协同编辑;
- GraphQL:精确按需查询,避免过多/过少数据,适合数据结构复杂或多客户端需求场景;
- SSR:提升首屏速度与 SEO,适合新闻、博客、电商等需搜索引擎友好的应用。
跨域与代理
- 开发阶段可通过 CRA 的
proxy
或 Express 中启用 CORS; - 生产环境应使用 Nginx/Apache 做统一代理,并配置 HTTPS,提升安全性。
- 开发阶段可通过 CRA 的
身份验证与授权
- 推荐基于 JWT,将令牌存储在 HTTP-only Cookie 或
localStorage
(注意 XSS 风险); - 对受保护路由使用中间件校验令牌;
- 前端在切换路由时检查登录状态并动态渲染界面。
- 推荐基于 JWT,将令牌存储在 HTTP-only Cookie 或
性能优化
- 前端利用 Code Splitting、Lazy Loading、Tree Shaking 减少打包体积;
- 后端启用 Gzip/Brotli、缓存、连接池等;
- 生产环境使用 PM2 或 Docker/K8s 做负载均衡与容错。
安全性注意事项
- 对用户输入严防 SQL 注入、XSS、CSRF 等;
- 后端不将敏感信息(如密码、密钥)返回给前端;
- 使用 HTTPS,禁用不安全的 Cipher。
开发流程
- 使用 ESLint/Prettier 统一代码风格;
- 使用 Git Hooks(如 Husky)保证提交质量;
- 编写单元测试(Jest、Mocha)、集成测试(Supertest、Cypress)提高稳定性。
通过本文的详细示例与图解,你已经掌握了 React 与 Node.js 在多种场景下的高效互连方式:从最简单的 RESTful 数据交互,到实时通信、GraphQL 查询,乃至 SSR。希望这份指南能帮助你在实际项目中快速上手并应用最佳实践,构建出高性能、可扩展、安全可靠的全栈应用。
评论已关闭