1、项目思考

  1. 这个项目是做什么?医院后台管理系统,帮助医院进行医生和病护管理,需要有医院首页、管理端控制台。
  2. 这个项目怎么做?前端使用react vite工程化生成项目,后端使用node server的方式进行书写,通过 Vite 代理做到前后端通信。
  3. 为什么要做这个项目?学习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、项目实现截图

更多推荐