刚拿到这个需求时,我心想: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。
另一个坑:分页参数我用的是page和limit,但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 (
{product.name}
¥{product.price}
))}
第{page}页
);
};
`
这个设计真的反人类:一开始我把分页按钮的逻辑写在组件内部,但后台管理系统也需要分页,导致重复代码。后来抽成Pagination组件,前后台共用。
还有个技巧:请求数据时一定要加loading状态,不然网络慢时用户看到白屏,转化率直接掉30%。我用axios.interceptors统一处理loading,而不是每个组件重复写。