React+Node电商实战:从0搭前台到Vue3后台管理系统

刚拿到这个需求时,我心想:React前台 + Vue3后台,不就是两个前端框架加个Node.js后端吗?结果连踩三个坑,从接口设计到跨域到状态管理,差点把项目延期。今天我把整个流程拆开揉碎,从后端搭建到前后端联调,每个坑都标清楚。

(开篇配图:一张项目架构图,展示React前台、Vue3后台、Node.js后端、MongoDB数据库的拓扑关系,箭头标注数据流向)

先搭建Node.js后端:别急着写前端

为什么先从后端开始?因为前端所有页面都依赖接口数据,如果不先定义好API,前后端各自为战,最后联调时全是404和500。

我用Express + MongoDB + JWT做鉴权,这是电商项目最轻量的方案。先看核心代码:

javascript
// server/index.js - 后端入口文件
const express = require('express');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const cors = require('cors');

const app = express();
app.use(cors());
app.use(express.json());

// 连接MongoDB
mongoose.connect('mongodb://localhost:27017/ecommerce');

// 用户模型 - 不要用明文存密码
const UserSchema = new mongoose.Schema({
username: String,
password: String, // 实际用bcrypt加密
role: { type: String, enum: ['customer', 'admin'], default: 'customer' }
});

// 登录接口 - 生成JWT令牌
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 验证用户(实际查数据库)
// ⚠️ 警告:切勿在生产环境使用硬编码密码,请用bcrypt进行hash和compare
if (username === 'admin' && password === '123456') {
const token = jwt.sign(
{ id: 1, role: 'admin' },
'your_secret_key', // 放环境变量里
{ expiresIn: '7d' }
);
res.json({ success: true, token });
} else {
res.status(401).json({ success: false, message: '账号或密码错误' });
}
});

// 商品列表接口 - 分页
app.get('/api/products', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
// 假设从MongoDB查数据
const products = [
{ id: 1, name: 'React实战教程', price: 99.9, stock: 100 },
{ id: 2, name: 'Vue3从入门到精通', price: 79.9, stock: 200 }
];
res.json({
success: true,
data: products,
total: 2,
page,
pageSize: limit
});
});

app.listen(3001, () => console.log('后端服务启动在3001端口'));
`

踩坑记录:JWT的secret key我一开始写死在代码里,结果上线后被人反编译了。正确做法是用环境变量,比如process.env.JWT_SECRET

另一个坑:分页参数我用的是pagelimit,但React端有个第三方组件默认用_page_limit,查了我两小时。前后端接口规范必须提前对齐

React前台:从商品列表到购物车

前台用Create React App初始化,主要交互是商品浏览、搜索、加购物车、下单。我选Redux Toolkit管理状态,因为它比原生Redux少写三分之二样板代码。

先看商品列表组件的核心逻辑:

`javascript
// frontend/src/components/ProductList.jsx
import React, { useState, useEffect } from 'react';
import axios from 'axios';

const ProductList = () => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);

// 为什么用useEffect?因为组件挂载后要请求数据
useEffect(() => {
const fetchProducts = async () => {
try {
setLoading(true);
const response = await axios.get(`/api/products?page=${page}&limit=10`);
setProducts(response.data.data);
} catch (error) {
console.error('商品加载失败', error);
} finally {
setLoading(false);
}
};
fetchProducts();
}, [page]); // 依赖page,翻页时重新请求

if (loading) return

加载中...

;

return (

{products.map(product => (

{product.name}

¥{product.price}

))}


第{page}页

);
};
`

这个设计真的反人类:一开始我把分页按钮的逻辑写在组件内部,但后台管理系统也需要分页,导致重复代码。后来抽成Pagination组件,前后台共用。

还有个技巧:请求数据时一定要加loading状态,不然网络慢时用户看到白屏,转化率直接掉30%。我用axios.interceptors统一处理loading,而不是每个组件重复写。

滚动至顶部