如何在NextJS API端点增量返回搜索结果?
解决方案
核心问题分析
- 顺序错误:你用
for...of遍历taskPromises数组,本质是按任务定义的顺序依次等待结果,而非按任务实际完成的顺序返回。 - 分块合并:缺少正确的响应头提示,导致浏览器/NextJS缓冲所有输出后一次性返回;同时当前的stream配置没有强制禁用缓冲。
- 无效JSON:直接输出多个JSON对象会导致整体无效,应该采用**NDJSON(换行分隔JSON)**格式,每个分块是独立的JSON对象,用换行分隔,既符合JSON规范,又支持增量解析。
修改后的代码
export async function GET(request) { const searchParams = request.nextUrl.searchParams const query = searchParams.get('q') const year = searchParams.get('year') // 定义并行任务 const tasks = [ async () => ({ type: 'coordinates', result: await parseCoordinates(query) }), async () => ({ type: 'address', result: await runAddressSearch(query) }), async () => ({ type: 'metro', result: await runMetroSearch(year, query) }), async () => ({ type: 'road', result: await runRoadSearch(year, query) }), async () => ({ type: 'district', result: await runDistrictSearch(year, query) }), ]; // 转换为带标识的Promise,方便后续跟踪 const pendingPromises = tasks.map((task, index) => task().then(result => ({ status: 'fulfilled', type: result.type, data: result.result })) .catch(error => ({ status: 'rejected', type: tasks[index].name, error: error.toString() })) ); const stream = new ReadableStream({ async start(controller) { const encoder = new TextEncoder(); // 循环处理所有完成的Promise,直到没有待处理任务 while (pendingPromises.length > 0) { // 等待第一个完成的Promise const result = await Promise.race(pendingPromises); // 从待处理列表中移除已完成的Promise const promiseIndex = pendingPromises.findIndex(p => p === result); if (promiseIndex !== -1) { pendingPromises.splice(promiseIndex, 1); } // 生成NDJSON格式的分块 let chunk; if (result.status === 'fulfilled') { if (result.data.length > 0) { chunk = JSON.stringify({ type: result.type, result: result.data }) + '\n'; } } else { chunk = JSON.stringify({ type: result.type, error: result.error }) + '\n'; } // 写入流 if (chunk) { controller.enqueue(encoder.encode(chunk)); // 强制触发流的刷新(部分环境需要) await new Promise(resolve => setTimeout(resolve, 0)); } } controller.close(); }, // 配置流禁用缓冲 highWaterMark: 1, size() { return 1; } }); // 设置正确的响应头,告诉客户端是分块的NDJSON return new Response(stream, { headers: { 'Content-Type': 'application/x-ndjson', 'Transfer-Encoding': 'chunked', 'Cache-Control': 'no-cache', 'X-Content-Type-Options': 'nosniff' } }); }
关键修改点说明
- 按完成顺序返回:
- 用
Promise.race监听所有待处理Promise,每次取最先完成的结果 - 处理完成后从
pendingPromises列表中移除该Promise,确保后续只处理剩余任务
- 用
- 避免分块合并:
- 添加
Transfer-Encoding: chunked头,明确告知服务器采用分块传输 - 设置
Cache-Control: no-cache禁止缓冲 - 加入
await new Promise(resolve => setTimeout(resolve, 0))强制让出事件循环,确保分块及时推送
- 添加
- 有效JSON格式:
- 使用
application/x-ndjson作为Content-Type,客户端可按行解析每个JSON块 - 每个分块末尾添加换行符,保证单个JSON块的独立性
- 使用
客户端解析示例(可选)
如果需要在客户端处理流式返回的NDJSON,可以这样实现:
async function fetchSearchResults(query) { const response = await fetch(`/api/search?q=${encodeURIComponent(query)}`); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value); // 按换行分割处理完整的JSON块 const lines = buffer.split('\n'); buffer = lines.pop(); // 保留不完整的最后一行 for (const line of lines) { if (line.trim()) { const result = JSON.parse(line); // 处理单个结果 console.log(result.type, result.result || result.error); } } } }
内容的提问来源于stack exchange,提问作者Nick K9
相关产品推荐
相关产品推荐

