React开发咖啡馆网站使用fs写入JSON文件遇浏览器兼容性错误
React前端使用fs模块报错的解决方案
问题背景
我用React开发小型咖啡馆网站,需要实现订单提交后在管理员面板显示的功能,编写相关代码后,添加saveOrder()函数时控制台出现以下错误:
Uncaught Error: Module "fs" has been externalized for browser compatibility. Cannot access "fs.readFileSync" in client code.
原代码如下:
import React from 'react'; import { OrderType } from './types'; import { Check } from 'lucide-react'; import fs from 'fs'; import path from 'path'; const ORDERS_FILE = "orders.json"; export function saveOrder(order2: OrderType): void { let orders: OrderType[] = []; const data = fs.readFileSync(ORDERS_FILE, 'utf8'); orders = JSON.parse(data); orders.push(order2); fs.writeFileSync(ORDERS_FILE, JSON.stringify(orders, null, 2)); } type OrderConfirmationProps = { order: OrderType; onClose: () => void; }; export function OrderConfirmation({ order, onClose }: OrderConfirmationProps) { saveOrder(order); return ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4"> <div className="bg-white rounded-lg p-6 max-w-md w-full"> <div className="flex items-center justify-center mb-6"> <div className="bg-green-100 rounded-full p-2"> <Check className="w-8 h-8 text-green-600" /> </div> </div> <h3 className="text-xl font-semibold text-center mb-4">Order Confirmed!</h3> <p className="text-center mb-4">Your order ID is: <br /> <span className="font-mono bg-gray-100 px-2 py-1 rounded">{order.id}</span> </p> <div className="border-t border-b py-4 my-4"> <h4 className="font-semibold mb-2">Order Summary:</h4> {order.items.map((item) => ( <div key={item.id} className="flex justify-between text-sm"> <span>{item.quantity}x {item.name}</span> <span>${(parseFloat(item.price) * item.quantity).toFixed(2)}</span> </div> ))} </div> <div className="flex justify-between font-semibold mb-6"> <span>Total:</span> <span>${order.totalPrice.toFixed(2)}</span> </div> <button onClick={onClose} className="w-full bg-amber-700 text-white px-4 py-2 rounded hover:bg-amber-800 transition-colors" > Close </button> </div> </div> ); }
问题原因
fs是Node.js专属的核心模块,仅能在服务器环境运行,浏览器没有这个模块的实现,所以直接在React前端组件中调用fs的方法会触发兼容性错误。前端代码无法直接读写服务器或本地文件,必须通过后端接口完成文件操作。
修复步骤
1. 改造前端组件,通过接口提交订单
移除前端的fs相关导入和saveOrder函数,改用fetch向后端发送订单数据:
import React, { useEffect } from 'react'; import { OrderType } from './types'; import { Check } from 'lucide-react'; type OrderConfirmationProps = { order: OrderType; onClose: () => void; }; export function OrderConfirmation({ order, onClose }: OrderConfirmationProps) { // 组件挂载时发送订单到后端 useEffect(() => { const submitOrder = async () => { try { await fetch('/api/save-order', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(order), }); } catch (error) { console.error('订单提交失败:', error); } }; submitOrder(); }, [order]); return ( <div className="fixed inset-0 bg-black/50 flex items-center justify-center p-4"> <div className="bg-white rounded-lg p-6 max-w-md w-full"> <div className="flex items-center justify-center mb-6"> <div className="bg-green-100 rounded-full p-2"> <Check className="w-8 h-8 text-green-600" /> </div> </div> <h3 className="text-xl font-semibold text-center mb-4">Order Confirmed!</h3> <p className="text-center mb-4">Your order ID is: <br /> <span className="font-mono bg-gray-100 px-2 py-1 rounded">{order.id}</span> </p> <div className="border-t border-b py-4 my-4"> <h4 className="font-semibold mb-2">Order Summary:</h4> {order.items.map((item) => ( <div key={item.id} className="flex justify-between text-sm"> <span>{item.quantity}x {item.name}</span> <span>${(parseFloat(item.price) * item.quantity).toFixed(2)}</span> </div> ))} </div> <div className="flex justify-between font-semibold mb-6"> <span>Total:</span> <span>${order.totalPrice.toFixed(2)}</span> </div> <button onClick={onClose} className="w-full bg-amber-700 text-white px-4 py-2 rounded hover:bg-amber-800 transition-colors" > Close </button> </div> </div> ); }
2. 搭建后端接口处理订单存储
用Node.js+Express编写后端接口,负责读写订单文件:
// 后端示例文件:server.js const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); // 解析JSON请求体 app.use(express.json()); // 订单文件路径,确保路径正确 const ORDERS_FILE = path.join(__dirname, 'orders.json'); // 保存订单接口 app.post('/api/save-order', (req, res) => { const newOrder = req.body; let orders = []; try { // 检查文件是否存在,存在则读取现有订单 if (fs.existsSync(ORDERS_FILE)) { const data = fs.readFileSync(ORDERS_FILE, 'utf8'); orders = JSON.parse(data); } // 添加新订单 orders.push(newOrder); // 写入文件 fs.writeFileSync(ORDERS_FILE, JSON.stringify(orders, null, 2)); res.status(200).json({ message: '订单保存成功' }); } catch (error) { console.error('保存订单出错:', error); res.status(500).json({ message: '保存订单失败' }); } }); // 获取所有订单接口(供管理员面板使用) app.get('/api/get-orders', (req, res) => { try { if (fs.existsSync(ORDERS_FILE)) { const data = fs.readFileSync(ORDERS_FILE, 'utf8'); const orders = JSON.parse(data); res.status(200).json(orders); } else { // 文件不存在则返回空数组 res.status(200).json([]); } } catch (error) { console.error('获取订单出错:', error); res.status(500).json({ message: '获取订单失败' }); } }); // 启动后端服务 const PORT = 3001; app.listen(PORT, () => { console.log(`后端服务运行在 http://localhost:${PORT}`); });
3. 配置前端代理解决跨域
如果前端和后端运行在不同端口,需要配置代理让前端请求转发到后端:
- Vite项目:在
vite.config.js中添加代理配置:
export default { server: { proxy: { '/api': { target: 'http://localhost:3001', changeOrigin: true, }, }, }, };
- Create React App项目:在
package.json中添加一行:
"proxy": "http://localhost:3001"
4. 管理员面板获取订单数据
管理员面板通过后端接口获取订单列表:
import React, { useState, useEffect } from 'react'; import { OrderType } from './types'; export function AdminOrders() { const [orders, setOrders] = useState<OrderType[]>([]); useEffect(() => { const fetchOrders = async () => { try { const response = await fetch('/api/get-orders'); const data = await response.json(); setOrders(data); } catch (error) { console.error('获取订单失败:', error); } }; fetchOrders(); }, []); return ( <div className="p-6"> <h2 className="text-2xl font-bold mb-6">所有订单</h2> <div className="space-y-4"> {orders.map((order) => ( <div key={order.id} className="border border-gray-200 rounded-lg p-4"> <div className="flex justify-between items-center mb-3"> <span className="font-semibold">订单ID: {order.id}</span> <span className="text-lg font-bold">${order.totalPrice.toFixed(2)}</span> </div> <div className="space-y-2"> {order.items.map((item) => ( <div key={item.id} className="flex justify-between text-sm"> <span>{item.quantity}x {item.name}</span> <span>${(parseFloat(item.price) * item.quantity).toFixed(2)}</span> </div> ))} </div> </div> ))} </div> </div> ); }
内容的提问来源于stack exchange,提问作者george tavadze
相关产品推荐
相关产品推荐

