上周接了个小单:客户要在他那台 2019 款 MacBook(macOS 13.6)的 Chrome 118 里,把一份 1.2 万行的订单表格导出成 CSV。现象挺明确:点导出后白屏三秒,然后弹「导出失败」,控制台里飘一条 net::ERR_CONNECTION_RESET。客户说「换浏览器就好了」,可我偏偏只能在他这台老机上复现。
先别急着改代码,我打开 DevTools 的 Network 面板,把 Network conditions 里的「No throttling」改成 Slow 3G 又测了一遍。请求确实在 30 秒左右断开,响应头里没有 Content-Length,服务端是朋友公司的一个老 PHP 接口,默认 max_execution_time=30。换句话说,前端没超时,是后端先掐了连接。
定位到这一步,我做了三件事。第一,把导出的分页参数从 pageSize=5000 降到 800,前端循环拉,每页单独请求:
async function fetchAll(pageSize = 800) {
let page = 1, rows = [];
while (true) {
const res = await fetch(`/api/orders?page=${page}&size=${pageSize}`, {
signal: AbortSignal.timeout(25000)
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
rows = rows.concat(data.list);
if (!data.hasNext) break;
page++;
}
return rows;
}
第二,把 AbortSignal.timeout 的超时设成 25 秒,比服务端 30 秒少一点,让前端有主动权。第三,在 Network 面板里开了「Preserve log」,防止页面跳转把请求记录冲掉。改完之后,同样 1.2 万行,分 15 页拉完,最慢一页 6 秒,整体 40 秒左右,没再断过。
为什么这样取舍:把一次大请求拆成多次小请求,代价是总耗时变长、请求数变多,但换来的是每一页都能单独重试,失败不会拖垮整批导出。对客户这种「老机器 + 老接口」的场景,稳定性比速度重要。另外 AbortSignal.timeout 只是前端兜底,真正的大头还是后端那个 30 秒,我没权限改服务端,只能绕。
哪里会失效:如果每页数据本身超过 30 秒,这个方案就废了,得让后端改 max_execution_time 或走异步导出。我在自己机器上测的时候还踩过一个坑:AbortSignal.timeout 在 Chrome 103 之前不支持,客户要是更老的浏览器,这行代码会直接报错,得换 AbortController 手写 setTimeout。还有一次,分页接口的 hasNext 字段在最后一页返回的是 undefined 而不是 false,循环多跑了一页空请求,白等了 25 秒。
现在这个单子交付了,但我心里没底的是:这种「前端分页兜底」的写法,客户以后数据涨到十万行,是不是又得推倒重来?你们遇到这种老接口导出,是硬扛着改后端,还是先在前端拆页顶着?
0 comments
No comments yet — be the first.