软开(四):后端数据交互与项目架构
后端数据交互与项目架构学习报告
本报告讲解教材项目的后端通信机制、存储过程规范,以及项目整体目录架构。
一、项目目录结构
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 |