2026/10/17 23:29:18

加载结果数据的工程化指南:从异步分页到弱网优化

加载结果数据的工程化指南:从异步分页到弱网优化 1. 从一句“加载结果数据”说起的真实场景如果你在代码仓库里翻到一个只有“加载结果数据”六个字的提交记录第一反应大概率是困惑。这也太模糊了。可真正做过数据处理或者后台管理系统的人都会心一笑——这六个字背后往往藏着一整套“从数据源取数到界面可交互呈现”的完整链路。它可能是某个爬虫跑完后把结果灌进数据库也可能是前端页面异步拉取接口返回的列表还可能是批处理脚本读文件后做内存聚合。标题越短能延展的内容反而越多。我这里想结合自己做过的一个“某运行数据管理后台”项目把这个短语掰开揉碎讲清楚从需求到落地、从弱网环境到大数据量卡顿、从接口异常到重复加载这几个关键环节里最值得注意的技术决策和踩坑经验。这个项目的初级形态特别朴素业务方每天需要把一批设备上报的历史运行数据从远端服务拉取下来在页面上按时间倒序展示支持关键字筛选和分页浏览。听起来像是一个非常标准的“查询列表”功能但真正动手之后才发现“加载结果数据”这个动作要做得又快又稳又有良好的异常兜底比想象中要复杂得多。特别是当数据量从几千条增长到几十万条弱网环境下接口超时频发用户疯狂点击翻页时出现重复提交整个加载链路的脆弱性就彻底暴露了。适合看这篇文章的人我认为有三类。第一类是刚接触前后端数据交互、对“拿数据并把数据放到页面上”这件事还没有形成体系认知的入门开发者这篇文章能帮你建立一个完整的加载链路视角。第二类是已经写过不少业务代码但发现自己写的加载逻辑在数据量一大、网络一差就崩的工程师文中排查和优化思路可以直接复用。第三类是自己动手做小工具、小脚本的开发者你需要理解的不只是“怎么取数”还有“怎么让取数这个过程可控、可观测、出错能恢复”。下面所有内容都会围绕这个真实项目展开。它不涉及多高深的前沿技术但每一个点都是我在踩坑之后总结出来的。2. 加载结果数据的技术选型同步还是异步哪个更适合你的场景2.1 先想清楚你的数据从哪里来、到哪里去在做任何技术选型之前第一件事永远是盘清楚数据链路。所谓“加载结果数据”本质上包含三个环节数据源Source、数据传输通道Channel、数据呈现端Sink。不同的组合方式决定了你是用同步流程还是异步流程也决定了后面所有开发和排查方向的取舍。我这边当时的数据源分为两路。一路是远端的数据库表存放设备上报的原始记录有设备编号、时间戳、运行参数、状态码这些字段表里大概积累了上百万条数据。另一路是业务方偶尔会以导出文件的形式提供一批离线数据格式是CSV大小从几十兆到几百兆不等。数据传输通道则统一走内部网关提供的HTTP接口返回JSON格式。数据呈现端就是管理后台的浏览器页面核心交互是表格展示。这个链路结构其实非常典型。数据源在远端而不是本地进程意味着网络IO是绕不开的开销。数据量级达到百万级别意味着全量加载到内存再交给前端渲染的方案是绝对行不通的。数据要展示在浏览器端意味着最终结果的“加载”动作必须考虑浏览器的渲染能力和用户交互节奏。有了这个盘底就可以做选型了。2.2 同步加载的适用条件和致命短板如果数据量不大接口响应时间稳定在几十毫秒到两三百毫秒之间同步加载完全够用。它的实现逻辑非常直接页面发起请求等待接口返回完整数据然后一次性渲染到界面上。代码写起来简单调试也很直观。但同步加载有三个致命短板在真实生产环境中很容易被放大接口响应时间的波动会被用户直接感知。本来两百毫秒的接口一旦远端服务负载升高、网络抖动响应时间可能涨到三秒以上。三秒钟的等待里用户面对的是一个毫无反馈的页面紧接着就是质疑和投诉。超大结果集的序列化和反序列化成本很高。一次拉取十万条记录每条记录有二十个字段JSON字符串的体积可能达到几十兆。前端解析这么大的JSON主线程会被卡死出现白屏或者假死现象。同步加载的失败恢复机制非常弱。只要请求失败整个页面没有任何数据用户必须手动重试体验很差。所以在数据量可控、网络稳定的内部小场景里同步加载勉强可用。但一旦涉及百万级数据、弱网环境、或者移动端网络切换频繁的场景这个方案基本可以放弃。2.3 为什么这个项目最终选择了异步加载我最终定的方案是异步加载。这倒不是因为它听起来更高级而是这个项目的实际约束条件决定了同步方案走不通。第一上百万条数据不可能一次性传输到浏览器。浏览器能流畅渲染的表格行数走虚拟滚动的话能到几万行不走虚拟滚动的话几千行就会出现明显的滚动卡顿。这意味着必须在服务端做分页页面按需拉取一次只拿几十条或者几百条。而这已经是异步加载的天然形态。第二业务方对页面反馈速度有硬性要求。查询按钮点下去之后第一帧数据必须在600毫秒内出现否则这个后台就会被质疑“太卡了”。异步加载配合加载态提示可以先用骨架屏或者菊花图撑住等待过程数据到了以后平滑填充。第三弱网环境下的容错能力是必须考虑的。业务方有一部分使用者是在现场通过4G网络访问后台的信号不稳定请求超时是常态。异步加载可以把单次请求的失败影响隔离开这一页请求挂了顶多这一页不出数据补一个重试按钮前面的数据还在体验不会崩。2.4 从一次真实故障看异步加载的必要性有一个印象很深的故障经历完全印证了异步加载的正确性。某一次流量高峰远端数据库出现了慢查询正常情况下一百毫秒内能返回的接口实际耗时涨到了五秒以上。如果是同步加载方案用户的体验会是点击查询页面白屏鼠标转圈长达五秒之后才出数据期间用户多次点击浏览器又迅速堆积多个并发请求进一步压垮本来就吃力的后端服务。而当时我已经把加载动作改成了异步分页页面加载态正常显示虽然数据出来得慢一些但用户能看到“正在加载”的反馈不会误以为页面死了。而且分页请求是串行发起的每页最多发一个请求后端压力也相对可控。从那次之后我更加笃定在数据类后台系统中异步加载不是一个“高级优化”而是一个基础鲁棒性需求。3. 核心加载流程的完整实现状态管理、结果包装与请求竞态处理3.1 加载状态机设计三个关键状态不能少很多初级开发者写的加载逻辑只有一个布尔变量比如isLoading。数据请求中把它设为true请求结束设为false。这种做法在小场景下没问题但一旦要处理“加载中、加载成功、加载失败、正在刷新、正在加载更多”这些混合状态单布尔变量的表达力就完全不够了。状态之间还会互相覆盖出现“明明还在请求却被标志成已完成”这类逻辑错乱。我自己习惯把加载过程抽象成状态机核心状态有五个状态含义触发的动作idle初始状态尚未发起任何请求无loading正在请求第一页数据显示全屏加载态loadingMore正在加载后续分页数据底部显示加载更多提示success请求成功数据已加载渲染列表 / 表格error请求失败显示错误占位提供重试入口这里最关键的设计是loading和loadingMore被拆成了两个独立状态。原因很简单它们对应的界面反馈完全不同。首次加载的失败可以整体展示错误页而加载更多失败只要在底部弹一个“点击重试”就行不能把整个页面都废掉。这段状态迁移逻辑虽然简单但一定要显式写出来不要靠某个状态位的隐式变化去推断。我在项目里把所有状态迁移都集中在一个 reducer 里任何一次状态变更都有迹可循排查问题的时候能省一半的时间。3.2 统一结果包装没有Match的穷举就没有安全的调用方另外一个容易被忽视的问题是加载完成后拿到的结果到底长什么样后端接口返回的JSON正常情况下可能有data、total、page这些字段。但异常情况下可能返回业务错误码也可能直接网络超时或者返回一个结构完整但data为空的JSON。如果调用方通过一堆 if 判断去处理所有可能性代码会变得非常丑陋而且总有漏掉的分支。我用的方案是后端先做一层统一结果包装同时前端也封装对应的解析函数。形态上类似于结果的“匹配”或者“模式拆分”。所有请求最终都收敛成三种情况之一加载成功有数据 加载成功无数据 加载失败有错误信息前端拿到这个包装后的结果直接做三路分支渲染不再需要关心JSON里的字段到底叫data还是rows网络层是 200 还是 500。这个统一的抽象非常值得尤其当后端接口不止一个、字段命名风格不一致的时候它的价值会被进一步放大。这个方案的源码逻辑其实不长核心就是一个分类函数function normalizeLoadResult(response, error) { if (error) { return { status: error, message: parseErrorMessage(error) }; } if (!response || response.code ! 0) { return { status: error, message: response?.message || 未知错误 }; } const list response.data?.list || []; return { status: list.length 0 ? success : empty, list: list, total: response.data?.total || 0, page: response.data?.page || 1 }; }有了这层包装后续的所有分支逻辑都可以做得清清爽爽调用方不需要自己去猜空指针到底出现在哪里。3.3 请求竞态处理消失的旧数据和作废的回调异步加载还有一个非常隐蔽的坑就是请求竞态。场景是这样的用户先输入关键词A点击查询请求发出去了。这个请求比较慢迟迟没有返回。用户等不及换了关键词B又点击查询第二个请求发出去了。这时候如果第一个请求先返回它携带的是关键词A的结果会直接把页面上本应展示关键词B结果的区域覆盖掉。用户看到的是“我搜B却出来A的数据”——哪怕只有一两秒的错乱也会让人对整个系统产生强烈不信任。这个问题的根因在于后发出的请求不一定后返回。异步世界里响应顺序和请求顺序没有任何硬性保证。解决办法有很多种我当时采用的是“自增序号 失效标记”的组合策略。每次发起新请求前把全局的序号自增一并把当前请求序号记为currentRequestId响应回来时检查这个响应携带的序号是否等于currentRequestId。如果不等说明这个响应已经过期直接丢弃不触发任何状态更新。let requestSequence 0; let latestRequestId -1; async function loadData(params) { const requestId requestSequence; latestRequestId requestId; setIsLoading(true); try { const result await fetchData(params); if (requestId ! latestRequestId) { // 过期响应直接丢弃 return; } applyResult(result); } catch (error) { if (requestId ! latestRequestId) { return; } applyError(error); } finally { if (requestId latestRequestId) { setIsLoading(false); } } }这个看起来很简单的逻辑避免了我后面遇到的无数个“帮忙看一下为什么数据不对”的排查请求。在实际做联调的时候我甚至用人工模拟慢网络的方式验证过——先在 DevTools 里把网络限速调成 Slow 3G然后快速切换两次查询关键词旧结果果然被拦截了。那一刻真的有一种踏实的感觉。3.4 翻页连续加载时的重复数据与丢数据还有一个容易被忽视的问题翻页加载更多时如果用户快速翻页上一页加载还没完成下一页的请求又发起了会导致两个问题。一个是重复数据另一个是丢数据。重复数据通常是因为后端的分页游标在并发请求下不稳定同一批记录被返回了两次。丢数据则是反过来——游标跳过了某些记录导致某些行永久不可见。我的处理策略是翻页请求串行化在加载更多状态未结束之前禁止发起新的加载更多请求。界面上直接置灰按钮或者只保留最后一次触发的加载。游标分页服务端支持如果数据量在十万级以内可以接受偏移量分页。如果超过这个量级就让后端改成基于排序字段的游标分页。游标分页的好处是并发请求下更容易保持数据一致性。前端去重兜底拿到新一页数据后按记录的唯一ID进行去重再追加。这个兜底非常值得写因为即使后端逻辑正确也可能因为网络重试导致同一页数据被前端重复处理。4. 大数据量加载时的三种优化策略分批、虚拟滚动与懒加载4.1 分批读取50万行数据不能一口气端上桌当数据量真的涨上去之后首先被挑战的就是“一次加载到底”的思维惯性。业务方曾经提过一个需求把某一天某台设备的全部运行参数导出来看数据量在三十万行左右。如果走常规的分页列表用户得点五千次下一页才能看完这不现实。这时候分批读取就是一个合理的折中。它的核心思想是把一个大的加载任务切分成多个小批次按用户的可感知节奏逐步完成。可以是自动的——前端在页面空闲时继续拉取下一批直到拉完。也可以是手动的——用户点“继续加载”按钮一次加载五千行。我在做分批读取时关注的三个核心指标是单批次的耗时需要控制在200毫秒以内保证用户感知不到明显的等待停顿。每批次的数据量一万行差不多是上限。理由是后端序列化一万行记录并传输到前端这个过程耗时可能超过一秒。而如果用户对某批数据不满意可以直接中断避免浪费。中断续做的能力如果加载到一半用户做了其他操作或者网络断开了已加载的数据必须保留不能全部推倒重来。当时我实现的分批读取逻辑大致是这样一个调度流程先加载第一批数据并渲染然后启动一个空闲调度器当浏览器主线程空闲时自动加载下一批直到所有批次加载完毕。同时暴露出一个“取消剩余批次”的接口用户点击其他筛选项时自动调用。4.2 虚拟滚动不管数据多大界面只渲染看得见的行分批读取解决的是“数据慢慢拿”的问题但用户依然可能面对一个“已经拿到手但渲染不动”的表格——如果一次性渲染几万行DOM节点浏览器的渲染引擎会非常吃力滚动时掉帧严重。虚拟滚动是解决“渲染不动”的标准思路。它的逻辑是不管总数据有多少行界面上只渲染用户当前可视区域内的那几十行以及上下各一小段缓冲区域。当用户滚动时动态调整渲染的行范围。我选用虚拟滚动方案时对比过几个开源库最终基于成本和可控性选择了一个轻量级的实现。它需要的参数有三个每一行的高度、可视区域的高度、总行数。然后通过计算滚动偏移量得出当前应该渲染的行的起始索引和结束索引。这个方案实测下来的效果非常明显。一次性渲染五万行页面滚动能稳定在60帧左右不再出现明显的白屏或卡顿。不过虚拟滚动有一个代价它改变了正常表格滚动条的交互模式普通表格自带的“跳到某一行”功能会失效定位搜索也会变得比较复杂。所以如果产品对“找到第M行”这种交互有硬需求虚拟滚动方案需要重新评估。4.3 懒加载与懒渲染做得少反而更快懒加载和虚拟滚动的思路一脉相承但应用的位置不同。虚拟滚动关注的是“当前可见行”懒加载关注的是“当前用户真正需要的数据”。我用懒加载处理的场景是表格每一行有展开详情的功能点击某一行后需要展示该行对应的二十个扩展字段。这些字段如果一次性全部加载下来接口响应时间会平白多出几百毫秒而用户可能根本不会点开任何一行的详情。方案调整为基础列表的接口只返回列表渲染必需的字段点击展开行时再单独发一个详情接口加载该行的扩展数据。这个改动让列表首屏的响应时间下降了大约40%而点击展开行的场景本身就有加载态用户等待一两百毫秒完全无感。还有一个相关的优化叫懒渲染。数据已经加载到前端内存中但不立即插入DOM。等到用户滚动到某个区域、或者某个容器进入可视范围时再真正渲染那部分内容。这相当于把虚拟滚动的思路往前再推了一步适用范围更灵活。4.4 大数据量加载的分层决策表格做了这么多优化之后你会发现“如何加载结果数据”其实是分层决策的数据规模首屏策略滚动策略加载方式1000行以内一次性渲染普通滚动同步加载或异步加载均可1千~1万行一次性渲染普通滚动 懒加载详情异步加载 统一结果包装1万~10万行分批读取虚拟滚动异步加载 游标分页10万行以上分批读取 服务端聚合虚拟滚动 懒渲染异步加载 缓存与超时熔断这个表格只是一个参考坐标具体数据量阈值会因浏览器性能、行结构复杂度、网络环境不同而上下浮动。但基本原则非常明确能少传输就少传输能少渲染就少渲染。永远不要高估浏览器的承受能力也永远不要低估用户滚动的速度。5. 弱网与异常场景下的加载鲁棒性超时重试与错误恢复5.1 弱网模拟DevTools限速测出的问题比想象中多正常开发环境走本地或内网接口响应都在十几毫秒根本暴露不了问题。我第一次认真做弱网测试是在联调阶段把浏览器的网络模拟调到了Slow 3G结果一下子暴露出三个问题。第一个问题是加载态的显示时间太短短到用户根本感知不到反馈就已经结束了。这听上去是好事但在慢网络下不是。Slow 3G下一个接口需要三到八秒才能返回如果加载态的样式做得不明显用户看到的页面就是死气沉沉的空白加一个转圈图标很容易误判为系统崩溃。第二个问题是没有超时控制。一个请求发出去了如果网络完全断开浏览器默认会等很久才报错这段时间用户点击任何按钮都没有任何响应。移动端4G网络切换基站、进电梯、过隧道这些场景都会触发这种“悬空请求”。处理方式很简单给请求设置一个显式的超时时间。我当时根据接口历史P95耗时把超时时间定在五秒。超过五秒没返回直接按失败处理并展示重试入口。第三个问题是错误提示语太技术化。原本有一个分支会把底层报错信息直接展现在界面上里面包含“connect timeout”“getaddrinfo ENOTFOUND”这类英文技术术语。后来统一改成了人话文案“加载失败请检查网络后重试”。虽说这只是文案层面的调整但对实际使用体验的提升非常明显。5.2 重试策略重试不是无脑重发要有退避和熔断异常场景下的重试策略是加载逻辑里容易被过度设计的部分也很容易被完全忽略。我的实践经验是重试可以但必须带约束。约束一重试次数封顶。单次请求最多重试三次超过三次之后不再自动重试只保留手动重试按钮。不封顶的话弱网环境下自动重试会把后端服务压垮形成更严重的雪崩。约束二退避间隔。两次重试之间要有等待间隔并且间隔递增。比如第一次重试等待500毫秒第二次等待1秒第三次等待2秒。这种退避策略能有效避开网络瞬时抖动和短时拥堵。约束三全局熔断。如果一个时间段内失败的请求数超过一个阈值比如十秒钟内连续失败五次就启动熔断——接下来一段时间内所有自动加载请求直接快速失败不再真正打到后端页面统一展示“服务暂时不可用”。等熔断窗口过去后再允许请求慢慢恢复。这个策略在高峰故障时保护过我们的后端服务避免了请求风暴。5.3 错误恢复用ErrorBoundary兜住渲染层异常加载过程中除了网络异常还有一类异常非常隐蔽数据本身没问题但在渲染阶段因为某条脏数据触发了前端运行时报错导致整个列表区域崩溃。这种问题在开发环境不一定会出现生产环境却会因为某条数据库里的异常记录突然爆发。我的处理是给列表区域加了一层错误边界渲染过程中抛出的任何异常都会被这层边界捕获页面不会整块白屏而是显示一个“部分内容加载失败”的占位区域并提供两个动作重试当前区域或者提交反馈信息。这个兜底非常值得写因为在真实环境中脏数据的出现是无法完全避免的。它可以是一条超过最大长度的文本字段、一个非预期为空的嵌套结构、或者一个乱序的枚举值。有了错误边界最坏的情况也只是局部降级而不是整个页面崩溃用户不用刷新重来系统的稳定性感知会好很多。5.4 弱网与异常的排查工具箱实战中的排查我常用的工具和手段是这几样浏览器的网络面板重点看单次请求的耗时分段尤其是TTFB和内容下载时间的比例。TTFB长说明服务端处理慢下载时间长说明数据量大或者带宽受限。DevTools的限速模拟虽然不完全等同于真实弱网但在开发阶段能快速暴露问题。后端接入层日志看当前时间段接口的成功率、P95耗时、慢查询数。这些数据和前端观察到的现象互相对照能快速定位问题是在网络链路还是后端计算。自定义的性能埋点在前端加载动作的关键节点打点比如请求发出时间、响应返回时间、渲染完成时间。有了这些埋点才能客观判断慢是慢在哪个环节而不是靠感觉。6. 性能压测与优化实录从接口慢查询到前端渲染瓶颈的全链路定位6.1 一次真实压测数据暴露了三个层面的优化空间项目上线之前我做了一轮相对正式的性能压测。测试环境的数据量大概是八十万条记录模拟的并发用户数是三十个。压测得到的关键数据如下列表接口在低并发下的平均响应时间是420毫秒P95是680毫秒。这个数据对于分页接口来说还算能接受但对于首屏加载来说已经偏慢。前端从拿到数据到渲染完成平均耗时在350毫秒左右其中有接近70%的时间消耗在对象序列化和样式计算上。当并发升到三十个用户时后端数据库的慢查询明显增多主要体现在深度分页的场景——用户翻到第100页之后偏移量很大查询耗时指数级上涨。这三个层面——接口层、前端渲染层、数据库查询层——都有优化空间而且优化手段并不冲突可以同时进行。6.2 服务端的深度分页优化用游标替换偏移量偏移量分页的痛点在于它会随着页码变大而越来越慢。数据库需要先扫描并丢弃前一万行再取后面的一百行。这个“丢弃”的成本在数据量大的时候是不可忽略的。游标分页的做法是排序字段必须唯一且有序通常用自增ID或者时间戳ID组合。查询下一页时前端把上一页最后一条记录的排序字段值传给后端后端直接从这个值开始继续往后取。这种方式的查询时间基本恒定不会随着页码变大而变差。改造成游标分页后接口在八十万数据量下的P95耗时从680毫秒降到了220毫秒左右效果非常明显。但它有一个代价用户不能直接跳到任意第M页只能一页一页往下翻。所以这个方案适合“浏览型”场景不太适合“精确跳页型”场景。我当时和业务方确认过发现用户的行为主要是按时间顺序往下翻看几乎没有跳页需求才放心改造。6.3 前端的渲染耗时拆分与懒渲染优化前端的350毫秒渲染耗时我通过 Performance 面板拆解了一下发现主要压在三个环节把接口返回的原始JSON转成前端组件需要的结构、行组件的创建和挂载、样式计算与布局。第一个环节的优化很直接——把转换逻辑做成memo化数据没有变化时跳过转换。第二个环节的优化是减少组件的嵌套层级和DOM节点数量每行从原来的三十多个DOM节点压缩到二十个左右。第三个环节的优化则是减少强制同步布局——避免在渲染过程中反复读取某个元素的尺寸属性从而触发布局抖动。还有一个很有效的优化是懒渲染。列表区域的容器高度固定数据加载完成后不立即渲染全部行而是只渲染可视区域附近的几十行。用户滚动时再动态补充。实测下这个改动让首次渲染的耗时降到了120毫秒左右用户体验提升非常直观。6.4 最终压测结果与验收做完这轮优化后我重新压测了一次数据对比如下指标优化前优化后列表接口P95680ms220ms首屏渲染完成350ms120ms左右并发三十人时的服务端成功率98.2%99.9%翻到第100页时的接口耗时2.1s260ms这份数据比任何文字描述都有说服力。它证明了一件事加载结果数据慢往往不是某一个单一环节的锅。接口慢、渲染慢、查询方案不合理三者叠加起来才是完整的问题画像。逐个击破之后整体性能才会有质的飞跃。7. 加载结果数据的工程化沉淀单元测试、埋点与团队规范7.1 单元测试覆盖的核心场景加载逻辑是最容易被测试遗漏的模块因为它涉及异步、竞态、异常恢复手动测试很难把每条路径都覆盖到。我当时把核心逻辑抽成了纯函数方便做单元测试。重点覆盖这几个场景请求成功且返回数据结果分类是否正确分到success。请求成功但列表为空是否正确分到empty。请求失败错误信息是否正确解析并分到error。两个请求并发返回时旧请求是否正确丢弃。超时后重新发起请求状态机是否能正确从error回到loading。其中竞态测试最容易出问题也很不好模拟。我的做法是用可手动控制的Promise让测试代码自己决定哪个请求先resolve。这样就彻底掌握了时序可以精确构造“后发先至”的场景并且验证结果。写这个测试时我感觉特别踏实因为竞态问题永远是最隐蔽的有了自动测试之后再做任何改动都不用担心回归。7.2 埋点与可观测性加载过程必须是透明的“加载结果数据”这个动作光做对还不够还要让它可观测。我在项目里埋了几类数据请求发出与返回的耗时、状态机每次迁移的轨迹、每类错误的次数与分布、以及用户从点击到看到首屏数据的完整时延。这些埋点的价值在故障排查时尤为突出。曾经有一次线上反馈“列表加载特别慢”我通过埋点数据发现慢的并不是接口本身而是某些用户环境下的浏览器渲染耗时异常高定位到的原因是那部分用户打开了很多其他页面标签主进程被挤占。如果没有埋点这个结论很难快速得出。7.3 团队协作中的约定最后想说说团队协作中的规范沉淀。加载逻辑的命名、错误码的约定、接口字段的语义在我参与的项目里实实在在踩过一些坑。后端有人把“无数据”返回为null有人返回空数组还有人返回带错误码的response。前端对接不同接口时就得写不同的解析逻辑非常琐碎。后面大家约定了一套统一的规范所有列表返回都走{ code, message, data: { list, total, page } }的结构无数据时list统一返回空数组而不是 null。错误码也约定了一个码表超过三位数的错误码一定是业务错误前缀区分模块。有了这套规范之后新的前端同事接手项目时学了不到半天就能自己完成一个新的列表页开发这是规范带来的最大收益。返回的 result 或者错误处理也不再是各写各的大家都一个套路评审和审查的效率一下子提高了很多。