ReactJS前端请求api/products/:id返回undefined及500状态码问题咨询
问题描述
展示商品时发起GET请求api/products/:id接口时,浏览器跳转地址为localhost:3000/produit/undefined,同时返回请求失败,状态码500。
仓库地址:https://github.com/Kwonsongji/mern-stack-calone-shop
后端代码
模型文件:Product.js
const mongoose = require('mongoose'); const productSchema = new mongoose.Schema({ name: { type: String, required: true }, image: { type: String, required: true }, price: { type: Number, required: true }, material: { type: String, required: true }, countInStock: { type: Number, required: true }, }, { timestamps: true }) const Product = mongoose.model('Product', productSchema); module.exports = Product;
控制器文件:productCtrl.js
const db = require('../config/db'); const Product = require('../models/Product'); const datas = require('../data/datas'); const productCtrl = { sendDatasProductToMoongoose: async (req, res) => { try { //await db.products.dropIndexes({}) await Product.deleteMany({}) //.remove({}); const createdProducts = await Product.insertMany(datas.products); console.log('Data Import Sucess createdProducts',createdProducts); res.send({ createdProducts}); } catch (error) { res.status(500).send(error.message) } }, getAllProducts: async (req, res) => { try { console.log("req :", req.body); // Product.find({}) const products = await Product.find() .then((Document) => { console.log("Document: ", Document); console.log("Req.params: ", req.params); if (!Document) { return res .status(404) .json({ message: "This ressource doesn't exist " }); } res.status(200).json(datas.products); }) .catch((error) => { console.log(error); res.status(500).json(error) }); } catch (error) { console.error(error); res.status(500).json({ message: "Server Error" }); } }, getProductById: async (req, res) => { try { /* const product = await Product.findOne(_id) */ const product = await Product.findById(req.params.id) /* .then((Document) => { if (!Document) { return res .status(404) .json({ message: "This document doesn't exist" }); } res.status(200).json(Document); }) .catch((error) => { res.status(500).json(error); }); */ res.send(product); // returne moi les données en json } catch (error) { console.error(error); res.status(500).json({ message: "Server Error" }); } }, }; module.exports = productCtrl;
路由文件:product.js
const productCtrl = require('../controllers/productCtrl'); const router = express.Router(); router.get('/seed', productCtrl.sendDatasProductToMoongoose ) router.get('/', productCtrl.getAllProducts); router.get('/:id', productCtrl.getProductById); module.exports = router;
服务端入口文件:server.js
const express = require("express"); const cors = require("cors"); // require connexion db and run it const connectDB = require("./config/db"); const app = express(); connectDB(); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(cors()); app.use("/api/products",require("./routes/product.js")); app.use("/api/users",require("./routes/user.js")) const PORT = process.env.PORT || 5000; app.listen(PORT, () => console.log(`Server running on ${PORT}`));
前端代码
action文件:productActions.js
import * as actionTypes from '../constants/productConstants'; import axios from 'axios'; export const getProducts = () => async (dispatch) => { try { dispatch({ type: actionTypes.GET_PRODUCTS_REQUEST }); const { data } = await axios.get(`/api/products`); dispatch({ type: actionTypes.GET_PRODUCTS_SUCESS, payload: data }) } catch (error) { dispatch({ type: actionTypes.GET_PRODUCTS_FAIL, payload: error.response && error.response.message ? error.response.data.message : error.message }); } }; export const getProductDetails = (id) => async (dispatch) => { try { dispatch({ type: actionTypes.GET_PRODUCT_DETAILS_REQUEST }); const { data } = await axios.get(`/api/products/${id}`); dispatch({ type: actionTypes.GET_PRODUCT_DETAILS_SUCESS, payload: data }) } catch (error) { dispatch({ type: actionTypes.GET_PRODUCT_DETAILS_FAIL, payload: error.response && error.response.message ? error.response.data.message : error.message }); } }; export const removeProductDetails = () => (dispatch) => { dispatch({ type: actionTypes.GET_PRODUCT_DETAILS_RESET }); };
reducer文件:productReducer.js
import * as actionTypes from '../constants/productConstants'; const initialStateForGetProducts = { products: [] }; export const getProductsReducer = (state = initialStateForGetProducts, action) => { switch (action.type) { case actionTypes.GET_PRODUCTS_REQUEST: return { loading: true, products:[] } case actionTypes.GET_PRODUCTS_SUCESS: return { loading: false, products: action.payload } case actionTypes.GET_PRODUCTS_FAIL: return { loading: false, error: action.payload //on veut afficher l'erreur } default: return state; } } const initialStateForGetOneProduct = { product: {} } export const getProductDetailsReducer = (state = initialStateForGetOneProduct, action) => { switch (action.type) { case actionTypes.GET_PRODUCT_DETAILS_REQUEST: return { loading: true, } case actionTypes.GET_PRODUCT_DETAILS_SUCESS: return { loading: false, product: action.payload } case actionTypes.GET_PRODUCT_DETAILS_FAIL: return { loading: false, error: action.payload } case actionTypes.GET_PRODUCT_DETAILS_RESET: return { product: {} } default: return state; } }
商品详情页面组件:ProductScreen.js
/* eslint-disable react/prop-types */ import React from 'react'; import './style.scss'; //import moonPendant from '../../assets/fonts/moon-pendant.png'; import { useState, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; // Actions import { getProductDetails } from '../../../redux/actions/productActions'; import { addToCart } from '../../../redux/actions/cartActions'; const ProductScreen = ( {match, history}) => { const [qty, setQty] = useState(1); const dispatch = useDispatch(); const productDetails = useSelector(state => state.getProductDetails); const { loading, error, product } = productDetails; useEffect(() => { if (product && match.params.id !== product._id) { dispatch(getProductDetails(match.params.id)); } }, [dispatch, product, match]); const addToCartHandler = () => { dispatch(addToCart(product._id, qty)); history.push("/panier"); } return ( <div className="productScreen"> { loading ? <h2>Loading...</h2> : error ? <h2>{error}</h2> : ( <> <div className="productScreen__left"> <div className="productScreen__left__image"> {/* <img src={moonPendant} alt={product.name} /> */} <img src={product.image} alt={product.name} /> </div> <div className="productScreen__left__info"> <p className="productScreen__left__name"> {product.name}</p> <p className="productScreen__left__price"> {product.price}</p> <p className="productScreen__left__material"> {product.material}</p> </div> </div> <div className="productScreen__right"> <div className="productScreen__right__info"> <p className="productScreen__right__price"> Prix: <span> {product.price} euros </span> </p> <p className="productScreen__right__status"> Statut: <span> {product.countInStock > 0 ? "En Stock" : "En Rupture de Stock"} </span> </p> <p className="productScreen__right__qty"> Quantité: <select value={qty} /* lorsqu'on slct le nbre voulu */ onChange={(e) => setQty(e.target.value)}> {[...Array(product.countInStock).keys()].map((x) => ( <option key={x+1} value={x+1} > {x+1} </option> //on utilise l'instance array pour créer un tableau de liste ))} </select> </p> <p> <button type="button" onClick={ addToCartHandler} className="productScreen__right__add" > Ajouter </button> </p> </div> </div> </> )} </div> ) } export default ProductScreen;
解决方案
- 修复后端
getAllProducts返回值错误:该接口当前返回本地静态数据datas.products,该数据没有数据库生成的_id字段,导致前端商品列表跳转时拿不到正确的id,拼接出undefined的地址。找到productCtrl.js中getAllProducts方法的返回行,将res.status(200).json(datas.products);修改为res.status(200).json(Document); - 补全
getProductById的异常判断逻辑:当前接口没有对查询结果做非空校验,也没有对id格式错误的异常做分类处理,容易触发500报错。修改后的getProductById代码如下:
getProductById: async (req, res) => { try { const product = await Product.findById(req.params.id) if(!product) { return res.status(404).json({ message: "商品不存在" }); } res.json(product); } catch (error) { console.error(error); if(error.name === 'CastError' && error.kind === 'ObjectId') { return res.status(400).json({ message: "商品ID格式错误" }); } res.status(500).json({ message: "服务器错误" }); } }
- 修复前端详情页请求触发逻辑:当前
ProductScreen的useEffect判断逻辑有问题,首次加载时product是空对象,product._id为undefined,不会触发详情请求。将useEffect修改为:
useEffect(() => { dispatch(getProductDetails(match.params.id)); }, [dispatch, match.params.id]);
内容的提问来源于stack exchange,提问作者Minzy
相关产品推荐
相关产品推荐

