全栈学习之react&node交互全链路
·
1、项目思考
- 这个项目是做什么?医院后台管理系统,帮助医院进行医生和病护管理,需要有医院首页、管理端控制台。
- 这个项目怎么做?前端使用react vite工程化生成项目,后端使用node
server的方式进行书写,通过 Vite 代理做到前后端通信。 - 为什么要做这个项目?学习node写法;搞清楚前后端全链路流程;
2、项目思路
2.1 项目运行机制详解
2.1.1 项目整体架构
node-test/ ← 项目根目录
├── package.json ← 根配置(管理后端依赖 + 启动脚本)
├── server/ ← 后端 (Node.js + Express)
│ ├── index.js ← 服务入口,端口 8080
│ ├── routes/ ← API 路由(7个模块)
│ └── data/mockData.js ← 内存数据库
│
└── frontend/ ← 前端 (React + Vite)
├── package.json ← 前端独立依赖
├── vite.config.js ← Vite 配置(含代理)
└── src/
├── main.jsx ← React 入口
├── App.jsx ← 路由配置
├── components/ ← 页面组件
└── utils/helpers.js ← 公共工具方法
2.1.2 启动方式:npm run dev
外层package.json下scripts内容
{
"scripts": {
"dev": "concurrently \"npm run server\" \"npm run client\"",
"server": "node server/index.js",
"client": "cd frontend && npm run dev"
}
}
执行 npm run dev 时的流程:
npm run dev
│
▼
┌─ concurrently ─────────────────────────────────────┐
│ │
│ ┌─ 进程 A: npm run server ──────┐ │
│ │ │ │
│ │ node server/index.js │ │
│ │ → Express 监听 :8080 │ │
│ │ → 提供 RESTful API │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ ↑ │
│ │ 并行启动 │
│ ↓ │
│ ┌─ 进程 B: npm run client ─────┐ │
│ │ │ │
│ │ cd frontend && vite │ │
│ │ → Vite Dev Server :3000 │ │
│ │ → 提供 HTML/JS/CSS 静态资源 │ │
│ │ → /api 请求代理到 :8080 │ │
│ │ │ │
│ └───────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────┘
concurrently 的作用:同时启动前后端两个进程,它们并行运行、互不阻塞。
2.1.3 前端运行详解
启动命令:cd frontend && npm run dev → 实际执行 vite
Vite 配置 frontend/vite.config.js
export default defineConfig({
plugins: [react()],
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "/src/styles/_variables.scss" as *;`,
},
},
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080', // ← 核心代理配置
changeOrigin: true,
}
}
}
})
运行过程:
① vite 启动开发服务器,监听 :3000
│
② 浏览器访问 http://localhost:3000/home
│
③ Vite 返回 index.html + main.jsx
│
④ React 应用挂载 → BrowserRouter 匹配路由
│
├─ /home → Home 组件(欢迎页)
├─ /admin/* → AdminLayout + 对应页面组件
│ ├─ /admin/dashboard → Dashboard
│ ├─ /admin/patients → Patients
│ ├─ /admin/doctors → Doctors
│ ├─ /admin/departments → Departments
│ ├─ /admin/appointments → Appointments
│ └─ /admin/medicines → Medicines
│
⑤ 组件 useEffect 触发 → fetch('/api/doctors')
│
│ 浏览器发出请求到 localhost:3000/api/doctors
│ │
│ ▼ Vite proxy 拦截
│ 转发到 → localhost:8080/api/doctors
│ │
│ ▼ Express 处理
│ 返回 JSON 数据
│
⑥ React setState → 组件重新渲染 → 页面显示数据
2.1.4 后端运行详解
入口文件 server/index.js
import express from 'express';
const app = express();
const PORT = 8080;
app.use(cors()); // 允许跨域
app.use(express.json()); // 解析 JSON 请求体
// 挂载 7 个 API 路由模块
app.use('/api/doctors', doctorsRouter);
app.use('/api/patients', patientsRouter);
// ... 共 7 个路由
app.listen(PORT, () => { ... }); // 监听 8080 端口,进程常驻
运行过程:
① node server/index.js 执行
│
② Node.js 引擎解析代码
│
③ import 各路由模块 → 触发加载 mockData.js
│
④ mockData.js 执行 → 在堆内存中创建数据数组
│
⑤ app.listen(8080) → 进程进入事件循环,等待 HTTP 请求
│
⑥ 收到请求 → 匹配路由 → 执行回调 → 返回 JSON 响应
│
⑦ 进程不退出,持续监听...
关键特性:
- 无热更新:修改
.js文件后必须手动重启进程才能生效 - 内存数据:数据存在 V8 堆内存中,重启即丢失
- 单线程事件循环:Node.js 通过异步 I/O 处理并发请求
2.1.5 前后端通信机制(核心)
这是最关键的部分——Vite 开发代理(Dev Proxy):
┌──────────────┐ HTTP ┌──────────────────┐
│ 浏览器 │ ─────────────────────▶│ Vite Dev Server │
│ :3000 │ GET /api/doctors │ :3000 │
│ │ ◀─────────────────────│ │
│ (渲染页面) │ JSON 响应 │ (代理转发) │
└──────────────┘ └────────┬─────────┘
│ proxy
│ /api → :8080
▼
┌──────────────────┐
│ Express Server │
│ :8080 │
│ │
│ (处理业务逻辑) │
│ (读写内存数据) │
└──────────────────┘
为什么需要代理?
| 问题 | 原因 |
|---|---|
浏览器直接请求 :8080 | 跨域问题(:3000 → :8080 端口不同) |
| 通过 Vite 代理 | 同源请求(浏览器只和 :3000 通信),Vite 在服务端转发 |
生产环境不同:生产环境通常将 Vite 构建的静态文件由 Express 直接托管,或通过 Nginx 统一反向代理,不需要开发代理。
注:内存中的 JavaScript 变量(In-Memory Mock Data)数据保存在 Node.js 进程的内存中,没有使用任何数据库(MySQL、MongoDB 等),也没有持久化到文件或浏览器
2.2、项目实现(以医生模块为例,展示前后端交互部分)
2.2.1 前端实现
const Doctors = () => {
// useState useForm 实现
// 获取医生列表
const fetchData = useCallback(() => {
setLoading(true)
const qs = new URLSearchParams(params).toString()
fetch(`/api/doctors?${qs}`)
.then(res => res.json())
.then(res => {
setData(res.data.list)
setTotal(res.data.total)
setLoading(false)
})
.catch(() => setLoading(false))
}, [params])
useEffect(() => { fetchData() }, [fetchData])
// 搜索医生
const handleSearch = (values) => {
setParams(prev => ({ ...prev, page: 1, ...filterEmptyValues(values) }))
}
// 打开新增弹窗
const handleAdd = () => {
setEditingRecord(null)
form.resetFields()
setModalVisible(true)
}
// 编辑
const handleEdit = (record) => {
setEditingRecord(record)
form.setFieldsValue(record)
setModalVisible(true)
}
// 删除
const handleDelete = (id) => {
fetch(`/api/doctors/${id}`, { method: 'DELETE' })
.then(res => res.json())
.then(() => { message.success('删除成功'); fetchData() })
}
// 编辑 | 新增医生
const handleSubmit = () => {
form.validateFields().then(values => {
const url = editingRecord ? `/api/doctors/${editingRecord.id}` : '/api/doctors'
const method = editingRecord ? 'PUT' : 'POST'
fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values),
}).then(res => res.json()).then(() => {
message.success(editingRecord ? '更新成功' : '添加成功')
setModalVisible(false)
fetchData()
})
})
}
return (
<div className="doctors-page fade-in-up">
<div className="page-header">
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增医生</Button>
</div>
<Card className="search-card" size="small">
<Form form={searchForm} layout="inline" onFinish={handleSearch}>
<Form.Item name="keyword">
<Input placeholder="搜索姓名/擅长领域" prefix={<SearchOutlined />} allowClear style={{ width: 200 }} />
</Form.Item>
</Form>
</Card>
<Card className="table-card">
<Table .... />
</Card>
<Modal
title={editingRecord ? '编辑医生信息' : '新增医生'}
onOk={handleSubmit}
>
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
<Form.Item name="name" label="姓名" rules={[{ required: true }]}>
<Input placeholder="请输入姓名" />
</Form.Item>
</Form>
</Modal>
</div>
)
}
2.2.2 后端实现
1、server/index.js 入口文件:
import express from 'express';
import cors from 'cors';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
import doctorsRouter from './routes/doctors.js';
const app = express();
const PORT = 8080;
app.use(cors());
app.use(express.json());
// API 路由
app.use('/api/doctors', doctorsRouter);
app.listen(PORT, () => {
console.log(`🏥 医院管理后端服务运行在 http://localhost:${PORT}`);
});
2、server/routes/doctors.js 文件
import { Router } from 'express';
import { doctors, generateId } from '../data/mockData.js';
const router = Router();
// 获取所有医生
router.get('/', (req, res) => {
const { page = 1, pageSize = 10, keyword, department, status } = req.query;
console.log('req.query', req.query)
let filtered = [...doctors];
if (keyword) {
const kw = keyword.toLowerCase();
filtered = filtered.filter(d =>
d.name.toLowerCase().includes(kw) ||
d.specialty.toLowerCase().includes(kw)
);
}
if (department && department !== 'all') {
filtered = filtered.filter(d => d.department === department);
}
if (status && status !== 'all') {
filtered = filtered.filter(d => d.status === status);
}
const total = filtered.length;
const start = (page - 1) * pageSize;
const data = filtered.slice(start, start + Number(pageSize));
res.json({ code: 200, data: { list: data, total, page: Number(page), pageSize: Number(pageSize) } });
});
// 获取单个医生
router.get('/:id', (req, res) => {
const doctor = doctors.find(d => d.id === req.params.id);
if (!doctor) return res.status(404).json({ code: 404, message: '医生不存在' });
res.json({ code: 200, data: doctor });
});
// 新增医生
router.post('/', (req, res) => {
const newDoctor = { id: generateId('doc'), patientsCount: 0, rating: 0, ...req.body };
doctors.unshift(newDoctor);
res.json({ code: 200, data: newDoctor, message: '添加成功' });
});
// 更新医生
router.put('/:id', (req, res) => {
const index = doctors.findIndex(d => d.id === req.params.id);
if (index === -1) return res.status(404).json({ code: 404, message: '医生不存在' });
doctors[index] = { ...doctors[index], ...req.body };
res.json({ code: 200, data: doctors[index], message: '更新成功' });
});
// 删除医生
router.delete('/:id', (req, res) => {
const index = doctors.findIndex(d => d.id === req.params.id);
if (index === -1) return res.status(404).json({ code: 404, message: '医生不存在' });
doctors.splice(index, 1);
res.json({ code: 200, message: '删除成功' });
});
export default router;
3、server/data/mockData.js
export let doctors = [
{ id: 'doc-001', name: '张伟', title: '主任医师', department: '内科', specialty: '心血管疾病', phone: '13800001001', email: 'zhangwei@hospital.com', avatar: '', experience: 20, consultationFee: 50, status: 'available', rating: 4.9, patientsCount: 1250 },
];
3、项目实现截图


更多推荐



所有评论(0)