期末复习

软开(四):后端数据交互与项目架构

2026 年 06 月 29 日 约 5698 字 · 15 分钟 期末复习

后端数据交互与项目架构学习报告

本报告讲解教材项目的后端通信机制、存储过程规范,以及项目整体目录架构。


一、项目目录结构

src/
├── api/                         ← 核心工具库(不要改动)
│   ├── ajax.js                  ← axios 封装,发送 HTTP 请求
│   ├── functions.js             ← reqdoSQL、reqdoTree 等核心函数 + 系统变量
│   ├── antdClass.js             ← AntdInputBox、AntdComboBox 等封装组件
│   ├── antdTrees.js             ← AntdTree、AntdCascader 树组件
│   ├── antdTable.js             ← AntdTable 表格组件
│   ├── antdFormMethod.js        ← MyFormComponent 基类(saveTableRow 等方法)
│   └── antdParseProperty.js     ← 属性解析工具(parseParams)
│
├── data/                        ← 静态数据文件
│   └── pinyin.json              ← 汉字拼音对照表(搜索用)
│
└── pages/                       ← 页面文件(按章节分目录)
    ├── chp11/
    │   ├── Page1101.jsx         ← demo1101(树+表+弹窗CRUD)
    │   └── Page1102.jsx         ← 更复杂的综合页面
    └── test/
        ├── practice.jsx         ← 练习文件(您的作业)
        └── practice_2.jsx       ← demo1101 的复刻参考

二、数据请求机制

2.1 整体调用链路

React 组件
    ↓ 调用
reqdoSQL(params) / reqdoTree(params)     ← src/api/functions.js
    ↓ 调用
ajax(url, data, 'POST')                  ← src/api/ajax.js(axios 封装)
    ↓ HTTP POST 请求
http://localhost:8080/myServer/doSQL     ← 后端 Java 服务器
    ↓ 执行
MySQL 存储过程
    ↓ 返回
JSON 数据 { rows: [...], error: '' }
    ↓ 回到
React 组件处理数据

2.2 reqdoSQL——表格数据请求

用于普通的增删改查数据请求:

import { reqdoSQL } from '../../api/functions';

// 基本用法:传入存储过程名,返回数据
let rs = await reqdoSQL({
    sqlprocedure: 'demo1101b',  // 必须:存储过程名
    categoryid: 'A1',           // 额外参数:传给存储过程的筛选条件
    pageno: 1,                  // 分页:页码
    pagesize: 20,               // 分页:每页行数
});

// 返回值结构:
// rs.rows  → 数据行数组
// rs.error → 错误信息(空字符串表示成功)

if (rs && rs.error === '') {
    console.log(rs.rows);  // 所有数据行
    console.log(rs.rows[0]._total);  // 总记录数(分页时后端返回)
}

2.3 reqdoTree——树节点数据请求

专门用于加载树形结构数据:

import { reqdoTree } from '../../api/functions';

let rs = await reqdoTree({
    sqlprocedure: 'demo804a',   // 树节点的存储过程
    style: 'expand',            // 加载方式:full=全量,expand=懒加载
    parentnodeid: '',           // 懒加载时传父节点ID
});

// 返回值结构与 reqdoSQL 相同
// rs.rows 是平级数组,需要用 toTreeData() 转成嵌套树形结构

reqdoSQL 与 reqdoTree 的区别:

// reqdoSQL 内部:
params.nodetype = 'datagrid';  // 告诉后端返回表格格式

// reqdoTree 内部:
params.nodetype = 'tree';      // 告诉后端返回树格式
// 实际上两者都请求同一个后端接口 /myServer/doSQL

三、存储过程规范

存储过程是预先在 MySQL 中保存的 SQL 程序,前端通过名称调用。

3.1 存储过程参数命名规则

教材项目中,存储过程的参数名以 $ 开头:

CREATE PROCEDURE demo1101b(
    IN `$pageno` int,          -- 页码(前端传入)
    IN `$pagesize` int,        -- 每页行数
    IN `$categoryid` varchar(50) -- 分类筛选条件
)
BEGIN
    -- 分页查询商品数据
    SELECT *, COUNT(*) OVER() AS _total
    FROM products p
    LEFT JOIN categories c ON p.categoryid = c.categoryid
    WHERE ($categoryid = '' OR p.subcategoryid = $categoryid)
    LIMIT ($pageno - 1) * $pagesize, $pagesize;
END;

前端传参时,去掉 $ 前缀:

reqdoSQL({
    sqlprocedure: 'demo1101b',
    pageno: 1,          // 对应 $pageno
    pagesize: 20,       // 对应 $pagesize
    categoryid: 'A1',   // 对应 $categoryid
})

3.2 分页存储过程的返回格式

-- 必须在结果集中包含 _total 列,告诉前端总记录数
SELECT *, COUNT(*) OVER() AS _total
FROM products
LIMIT $start, $pagesize;

前端处理:

let rs = await reqdoSQL({ sqlprocedure: '...', pageno: 1, pagesize: 20 });
let total = rs.rows[0]._total;  // 总记录数,用于显示分页控件
let data = rs.rows;              // 当前页的数据

3.3 树节点存储过程返回格式

树的存储过程必须返回这些字段:

SELECT
    categoryid AS id,           -- 节点唯一ID(必须)
    categoryid AS key,          -- React Tree 的 key(必须)
    CONCAT(categoryid, ' ', categoryname) AS text,  -- 显示文字(必须)
    parentnodeid,               -- 父节点ID(必须,用于构建树形结构)
    ancestor,                   -- 祖先节点路径(必须,用于展开定位)
    isparentflag,               -- 是否有子节点(1/0)
    *                           -- 其他业务字段
FROM categorytree
WHERE parentnodeid = $parentnodeid
ORDER BY CONCAT(TRIM(ancestor), id);

四、分页机制

教材使用 MySQL LIMIT 物理分页,由后端控制每次只返回一页数据。

4.1 分页原理

总记录:100 条
每页显示:20 条

第1页:LIMIT 0, 20  → 第1~20条
第2页:LIMIT 20, 20 → 第21~40条
第3页:LIMIT 40, 20 → 第41~60条

公式:LIMIT ($pageno - 1) * $pagesize, $pagesize

4.2 前端与后端的协作

// 前端(AntdTable 内部的 loadTableData)
loadTableData = async () => {
    let { pageno, pagesize } = this.state;

    let rs = await reqdoSQL({
        sqlprocedure: 'demo1101b',
        pageno: pageno,    // 当前页码
        pagesize: pagesize // 每页行数
    });

    // 后端返回当前页的数据 + 总记录数
    let total = rs.rows[0]._total;
    this.setState({ data: rs.rows, total });
}

4.3 翻页事件

// AntdTable 内部处理翻页
handlePageChange = (pageno, pagesize) => {
    this.setState({ pageno, pagesize }, () => {
        setTimeout(() => {
            this.loadTableData();  // 重新请求新的一页数据
        });
    });
}

4.4 保存后定位到记录的逻辑

// 存储过程保存后返回该记录在同类中的序号 rowno
let rowno = rs.rows[0].rowno;

// 根据 rowno 计算在第几页、第几行
pageno = Math.floor((rowno - 1) / pagesize) + 1;    // 页码
rowindex = rowno - (pageno - 1) * pagesize - 1;      // 页内行号(0起)

// 跳转到正确的页
this.myTable1.setState({ pageno, rowindex }, () => {
    setTimeout(() => {
        this.myTable1.loadTableData();  // 加载那一页数据
    });
});

五、toTreeData——平级数组转树形结构

后端返回的是平级数组(每条记录含 parentnodeid),前端需要转成嵌套树形:

// 原始数据(平级):
[
    { id: 'A',  text: '饮料',     parentnodeid: '' },
    { id: 'A1', text: '非酒精饮料', parentnodeid: 'A' },
    { id: 'A2', text: '酒精饮料',  parentnodeid: 'A' },
    { id: 'B',  text: '食品',     parentnodeid: '' },
]

// toTreeData() 转换后(嵌套树形):
[
    {
        id: 'A', text: '饮料',
        children: [
            { id: 'A1', text: '非酒精饮料', children: [] },
            { id: 'A2', text: '酒精饮料', children: [] },
        ]
    },
    { id: 'B', text: '食品', children: [] }
]
// toTreeData 核心逻辑(简化版)
const toTreeData = (data) => {
    let map = {};
    let result = [];

    // 第一步:建立 id → 节点对象的映射表,方便快速查找父节点
    data.forEach(item => { map[item.id] = item; });

    // 第二步:遍历,把每个节点挂到父节点的 children 里
    data.forEach(item => {
        let parent = map[item.parentnodeid];
        if (parent && parent.id !== '') {
            // 有父节点:挂到父节点的 children
            if (!parent.children) parent.children = [];
            parent.children.push(item);
        } else {
            // 没有父节点:作为根节点
            result.push(item);
        }
    });

    return result;
}

六、常见问题与解决方案

6.1 为什么要加 setTimeout?

setState 的回调里操作子组件时,有时需要等待 DOM 渲染完成:

// 问题场景:setState 后立刻操作子组件,但子组件还没渲染
this.setState({ myWin1: true }, () => {
    // ❌ 可能失败:弹窗刚打开,里面的 AntdInputBox 还没挂载
    this.productid.setState({ value: '001' });
});

// ✅ 加 setTimeout,等到下一个事件循环(DOM 已更新)
this.setState({ myWin1: true }, () => {
    setTimeout(() => {
        this.productid.setState({ value: '001' }); // 此时组件已挂载
    });
});

6.2 可选链操作符 ?.

// 不确定对象是否存在时,用 ?. 避免报错
this.myTable1?.loadTableData();
// 等价于:
if (this.myTable1) this.myTable1.loadTableData();

// 访问可能为 null 的嵌套属性
let name = user?.profile?.name;  // user 或 profile 为 null 时返回 undefined,不报错

6.3 展开运算符 ...

// 数组展开:合并数组
let arr = [...arr1, ...arr2];

// 对象展开:浅拷贝
let newState = { ...this.state };          // 复制状态
let newAttr = { ...this.myTable1.state.attr, categoryid: 'A1' }; // 更新某个字段

// 展开 props:把所有属性透传给子组件
<Tree {...this.props} />

6.4 ?.() 可选链调用函数

// 函数可能不存在时,用 ?.() 避免报错
afterLoadData?.();
// 等价于:
if (typeof afterLoadData === 'function') afterLoadData();

七、考试知识点速查

知识点关键代码/概念
state 修改this.setState({ key: value })
初始化请求componentDidMount = async () => { await reqdoSQL(...) }
父控制子ref={ref => this.myChild = ref}
子通知父props 回调函数 onXxx={this.handleXxx}
弹窗控制this.setState({ myWin1: true/false })
表单赋值this.setFormValues('myForm1', row)
树节点联动handleSelectNode + myTable1.setState + loadTableData()
分页计算pageno = Math.floor((rowno-1)/pagesize) + 1
去重排序[...new Set([...arr1,...arr2])].sort((a,b)=>a-b)
深拷贝JSON.parse(JSON.stringify(obj))
树节点插入递归找父节点,计算 level/ancestor/isparentflag