用Collection Runner高效批量测试API接口的实战技巧
项目初期的技术选型
上个月接了个内部工具项目,需求是批量执行一组 API 调用,每个请求的参数不同,但结构类似,还要支持失败重试、结果导出、执行日志。一开始我直接想用 Postman 的 Collection Runner —— 毕竟它可视化、支持变量、能跑多组数据,看起来挺香。
但很快发现不对劲:Postman 虽然能跑,但没法集成到我们自己的系统里,用户不能在我们界面上点一下就跑,而且执行过程不可控,日志也拿不到。更麻烦的是,客户要求“跑完后自动生成 Excel 报告”,这 Postman 根本做不到。
于是转头自己写一个轻量级的 Collection Runner。核心思路很简单:把请求配置成 JSON,前端读取后并发或串行发起 fetch,记录每一步状态,最后汇总结果。技术栈就用原生 JS + Vue(因为项目本身是 Vue 3),不搞复杂依赖,毕竟只是个辅助工具。
最大的坑:性能问题
开始写的时候,我天真地以为“不就是 for 循环发请求嘛”。于是第一版代码长这样:
async function runCollection(testCases) {
const results = [];
for (const testCase of testCases) {
const res = await fetch(testCase.url, {
method: testCase.method,
body: JSON.stringify(testCase.body)
});
results.push({ ...testCase, status: res.status, data: await res.json() });
}
return results;
}
串行执行,逻辑清晰,本地测试 10 条数据没问题。但一拿到真实环境,客户说有 200+ 条用例,我本地一跑——卡死!浏览器直接无响应,等了快一分钟才跑完。更糟的是,中间某个请求超时,整个流程就断了,前面跑过的全白费。
后来改成并发:
async function runCollection(testCases) {
const promises = testCases.map(testCase =>
fetch(testCase.url, {
method: testCase.method,
body: JSON.stringify(testCase.body)
}).then(res => ({
...testCase,
status: res.status,
data: res.json()
}))
);
return Promise.all(promises);
}
结果更惨:200 个请求同时发出去,后端直接限流,一大半返回 429。而且 Promise.all 有个致命问题:只要一个 reject,整个就崩了,连 partial result 都拿不到。
折腾了半天才发现,得控制并发数,还得容忍部分失败。这里注意我踩过好几次坑:一开始用第三方库 p-limit,但项目不允许加新依赖;后来手写了一个带并发控制的 runner,才算稳住。
核心代码就这几行
最终方案是自己实现一个带并发控制和容错的执行器。关键点:最大并发数设为 5,失败自动重试 2 次,所有请求无论成败都记录结果。
async function runWithConcurrency(tasks, maxConcurrency = 5, retries = 2) {
const results = [];
const executing = [];
for (let i = 0; i < tasks.length; i++) {
const task = tasks[i];
const attempt = async (retryCount = 0) => {
try {
const res = await fetch(task.url, {
method: task.method || 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(task.body)
});
const data = await res.json();
return { ...task, status: res.status, success: true, data };
} catch (err) {
if (retryCount < retries) {
await new Promise(r => setTimeout(r, 1000 * (retryCount + 1))); // 退避
return attempt(retryCount + 1);
}
return { ...task, success: false, error: err.message };
}
};
const promise = attempt().then(result => {
results[i] = result;
});
executing.push(promise);
if (executing.length >= maxConcurrency) {
await Promise.race(executing);
executing.splice(executing.findIndex(p => p === promise), 1);
}
}
await Promise.all(executing);
return results;
}
调用时也很简单:
const testCases = [
{ url: 'https://jztheme.com/api/validate', method: 'POST', body: { id: 1 } },
{ url: 'https://jztheme.com/api/validate', method: 'POST', body: { id: 2 } },
// ... 200+ 条
];
const results = await runWithConcurrency(testCases, 5, 2);
这个方案亲测有效:200 条数据 30 秒内跑完,失败的会重试,最终结果数组顺序和输入一致,方便后续处理。而且不会压垮后端,也不会让浏览器卡死。
踩坑提醒:这三点一定注意
- 别用 Promise.all 处理可能失败的批量请求:它不是为 partial failure 设计的。改用 Promise.allSettled 或自己收集结果,如上所示。
- 并发控制必须做:哪怕后端没限流,浏览器也有 TCP 连接数限制(Chrome 同域名约 6 个)。超过后请求会排队,反而拖慢整体速度。5-10 并发是个安全值。
- 重试要有退避策略:我一开始 retry 是立即重试,结果后端被连续打爆。改成指数退避(1s、2s、4s)后,成功率明显提升。
还有一个小问题一直没完美解决:如果用户中途点“停止”,怎么优雅中断?目前做法是加个 abortController,但正在执行的请求无法立刻终止(fetch 不支持 cancel 已发出的请求),只能忽略后续回调。好在影响不大,毕竟用户点了停止就表示不关心结果了。
回顾与反思
整体来看,这个自制的 Collection Runner 达到了预期:稳定、可控、可嵌入业务系统。比起 Postman,虽然少了图形化编辑,但灵活性和集成度高得多。客户现在可以在我们界面上上传 JSON 配置,点运行,等几秒就出报告,体验比切 Postman 好多了。
做得好的地方:并发控制 + 重试机制 + 顺序保持,这三点是核心。不足的地方也有:比如没有做请求之间的依赖(比如 A 的返回作为 B 的输入),但需求里没要求,就没加,避免过度设计。另外错误日志不够详细,目前只存了 error.message,其实应该把完整的 response 甚至 request 都记下来,方便排查。不过这些属于“锦上添花”,当前版本已经够用。
说到底,这种工具类功能,**简单、可靠、够用**比炫技重要。我一开始还想搞个 fancy 的进度条动画,后来发现用户根本不在乎,他们只关心“跑完没”“对不对”。
以上是我个人对这个 Collection Runner 的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多(比如结合 Web Worker 避免阻塞 UI),后续会继续分享这类博客。希望对你有帮助。

暂无评论