问题复现
起因:和后端同事联调需求功能时,发现历史页面的一个表格点击出现了循环请求,可能会直接打挂后端数据接口,后端同事直呼害怕,差点要提枪到我的工位直接爆头了😨
问题:表格展示列表数据,快速点击表格的分页器页码多次请求时,会出现循环请求的情况。
效果图:
代码分析
一看情况不对,立马去仔细分析了下这段代码逻辑,看一下循环请求的罪魁祸首。
第一反应肯定是先找到代码中发送请求的地方简单瞅一眼, 定位到是这个函数getTableData
;请求的逻辑也比较简单:
-
重置页面的
loading
和error
状态,用来给加载期间给表格展示加载态和失败态 -
获取请求的参数配置,例如页码,开始、结束时间,排序号等
-
发送页面请求
-
等页面请求回来后,重新赋值表格数据以重新渲染页面
const getTableData = async () => {
try {
setTableConfig({
...tableConfig,
tableLoading: true,
tableError: false,
});
const { pageNum, pageSize, sortInfo, createFrom, createTo } = tableConfig;
const reqData: GetOvertimeValidListRequest = {
pageNum,
pageSize,
createFrom,
createTo,
sortInfo,
configurable,
ruleID: ruleID || '',
};
// xxx省略逻辑代码
const {
total: totalData,
definitionList: tableDefinitionList,
} = await APIS.getOvertimeValidList(reqData);
const tableData = formatTableData(tableDefinitionList);
setTableConfig({
...tableConfig,
tableData,
total: totalData,
tableLoading: false,
});
// xxx省略逻辑代码
} catch (error) {
setTableConfig({
...tableConfig,
tableLoading: false,
tableError: true,
});
logger.error('create overtime rule', 'getTableData error');
}
};
到此为此,不知道各位看官是否已经看出了问题!
由于当时时间紧迫,再晚几分钟后端大佬就要提刀相见了,我只能先输出下日志看下了,不知道大佬们都是用什么方式调试的,我直接原始办法直接加log输出;
至此,发现了问题的所在;
在多次点击页码的时候,每次请求会修改页码,但是上一次的请求没有回来的时候立马点击下次请求,导致下一次请求回来的时候页码又是旧的,effect的依赖项发生了改变又导致了重新发请求,以此不断循环请求。。。
举个栗子🌰:
-
点击页码2,发送请求前改变页码为2,再发送请求
-
点击页码3,发送请求前将页码2改为3,再发送请求
-
问题出现了,此时页码2的请求回来了,将页码3改为2,依赖项检测到改变又会重新请求页码2
-
然后页码3的请求回来了,将页码2改为3,依赖项检测到改变又会重新请求页码3
-
然后就是动词打次了。。。
所以是这个页面的参数出了问题,立马去看了下数据结构,果然是所有的页面参数都放在一起;
export type TableConfig = {
total: number;
pageSize: number;
pageNum: number;
tableData: TableData[];
sortInfo: SortInfo;
tableLoading: boolean;
tableError: boolean;
createFrom: number;
createTo: number;
};
页面请求effect:
useEffect(() => {
getTableData();
}, [
tableConfig.pageNum,
tableConfig.pageSize,
tableConfig.sortInfo,
tableConfig.createFrom,
tableConfig.createTo,
definitionID,
]);
解法1
但是问题还得解啊,可不能把后端接口打炸了。所以小脑袋一转,先想了一个比较hack的解法。就是去记录最后一次的请求时间,每次请求都去重置下时间,等到请求返回的时候,去比较:
如果当前时间是小于最后一次记录的时间,那么说明这次的请求并不是最后一次,那么直接舍弃不去更新表单数据,那么就不会修改到页码了;
如果当前是最后一次记录的时间,那么这个请求就是可以作为表单数据的更新的;
// 记录最后一次请求的时间,防止多次触发导致循环请求
const lastRequestTime = useRef ( Date . now ());
const getTableData = async () => {
try {
const currentRequestTime = Date . now ();
lastRequestTime. current = currentRequestTime;
setTableConfig({
...tableConfig,
tableLoading: true,
tableError: false,
});
const { pageNum, pageSize, sortInfo, createFrom, createTo } = tableConfig;
const reqData: GetOvertimeValidListRequest = {
pageNum,
pageSize,
createFrom,
createTo,
sortInfo,
configurable,
ruleID: ruleID || '',
};
// xxx省略逻辑代码
const {
total: totalData,
definitionList: tableDefinitionList,
} = await APIS.getOvertimeValidList(reqData);
const tableData = formatTableData(tableDefinitionList);
if (currentRequestTime < lastRequestTime. current ) return ;
setTableConfig({
...tableConfig,
tableData,
total: totalData,
tableLoading: false,
});
// xxx省略逻辑代码
} catch (error) {
setTableConfig({
...tableConfig,
tableLoading: false,
tableError: true,
});
logger.error('create overtime rule', 'getTableData error');
}
};
解法2
下班美美回家,躺在床上思考了一番;由于问题的根本是数据结构设计的不太合理,页面的数据和页面的请求参数混在一起了,导致循环请求了页面的数据,所以还是得合理的拆分一下数据结构才行!
所以把页面数据类的参数和页面请求类的参数分开来:
export type TableConfig = {
total: number;
tableData: TableData[];
tableLoading: boolean;
tableError: boolean;
};
export type RequestConfig = {
pageSize: number;
pageNum: number;
sortInfo: SortInfo;
createFrom: number;
createTo: number;
}
那么大功告成,只有页面请求的参数发生改变时才会重新请求数据,数据请求回来的时候只会修改页面的数据,井水不犯河水,直接来了一个楚河汉界。
useEffect(() => {
getTableData();
}, RequestConfig]);
const getTableData = async () => {
const {
total: totalData,
definitionList: tableDefinitionList,
} = await APIS.getOvertimeValidList(reqData);
const tableData = formatTableData(tableDefinitionList);
setTableConfig({
...tableConfig,
tableData,
total: totalData,
tableLoading: false,
});
}
至此,算是先暂时解决了问题,不知道各位看官姥爷有没有好的建议~
总结
问题复现:一个表格普通点击出现了循环请求,可能会直接打挂后端数据接口。经分析,问题的根本原因是数据结构设计不合理,导致页面的数据和页面请求参数混淆在一起,从而循环请求了页面数据。
解决方案1:记录最后一次请求的时间,每次请求都去重置时间,等到请求返回时,比较当前时间是否小于最后一次记录的时间,如果是,则舍弃更新表单数据,避免修改页码。但此方案较为复杂,需要额外的逻辑处理。
解决方案2:将页面数据类的参数和页面请求类的参数分开,使得页面请求的参数发生改变时才会重新请求数据,数据请求回来时只会修改页面的数据。此方案更为简洁,只需要合理拆分数据结构即可。