Next.js长时任务分步反馈问题:流式响应未分块输出
解决方案
一、修复现有流式响应方案
你的代码问题在于:Node.js默认会缓冲输出内容,直到缓冲区满或调用flush()才会发送分块;同时Next.js可能默认启用响应压缩,导致所有内容被合并后一次性发送。按以下步骤修改:
服务端代码(Pages Router)
export default async function handler(req, res) { // 禁用压缩,避免内容被合并 res.setHeader('Content-Encoding', 'identity'); // 显式声明分块传输,设置响应类型 res.writeHead(200, { "Content-Type": "text/plain", "Transfer-Encoding": "chunked" }); // 模拟任务1(实际替换为await 外部API调用) res.write(JSON.stringify({ data: "Step1" }) + '\n'); // 加换行便于客户端拆分 await res.flush(); // 强制刷新缓冲区,立即发送分块 await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟API延迟 // 任务2 res.write(JSON.stringify({ data: "Step2" }) + '\n'); await res.flush(); await new Promise(resolve => setTimeout(resolve, 2000)); // 任务3 res.write(JSON.stringify({ data: "Step3" }) + '\n'); await res.flush(); res.end(); }
客户端代码
const response = await fetch("/api/hello", { method: "POST", body: JSON.stringify(config), mode: "cors", }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { value, done } = await reader.read(); if (done) break; buffer += decoder.decode(value); // 按换行分割完整的JSON块,避免多个JSON拼接解析失败 const lines = buffer.split('\n'); buffer = lines.pop(); // 保留未完成的行(如果有) lines.forEach(line => { if (line) { const data = JSON.parse(line); console.log('Received:', data); } }); } console.log("Response fully received");
二、更合适的第三种方案:Server-Sent Events(SSE)
SSE是专门为服务器向客户端单向推送实时更新设计的技术,API更简洁,无需手动处理分块拆分,适合你的分步任务反馈场景。
服务端代码(Pages Router)
export default async function handler(req, res) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Content-Encoding': 'identity', }); // 任务1 res.write(`data: ${JSON.stringify({ data: "Step1" })}\n\n`); await new Promise(resolve => setTimeout(resolve, 2000)); // 模拟API延迟 // 任务2 res.write(`data: ${JSON.stringify({ data: "Step2" })}\n\n`); await new Promise(resolve => setTimeout(resolve, 2000)); // 任务3 res.write(`data: ${JSON.stringify({ data: "Step3" })}\n\n`); // 发送结束信号 res.write('event: close\n'); res.write('data: done\n\n'); res.end(); }
客户端代码
// 若需传递config,可通过查询参数(适合小体积数据)或会话存储 const eventSource = new EventSource(`/api/stream?config=${encodeURIComponent(JSON.stringify(config))}`); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); console.log('Received:', data); }; // 监听结束事件 eventSource.addEventListener('close', () => { eventSource.close(); console.log("Response fully received"); }); // 错误处理 eventSource.onerror = (error) => { console.error('SSE error:', error); eventSource.close(); };
三、Next.js App Router 流式方案(现代替代)
如果使用App Router,可直接通过ReadableStream实现流式响应:
服务端路由(app/api/stream/route.js)
export async function POST(request) { const config = await request.json(); const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { // 任务1 controller.enqueue(encoder.encode(JSON.stringify({ data: "Step1" }) + '\n')); await new Promise(resolve => setTimeout(resolve, 2000)); // 任务2 controller.enqueue(encoder.encode(JSON.stringify({ data: "Step2" }) + '\n')); await new Promise(resolve => setTimeout(resolve, 2000)); // 任务3 controller.enqueue(encoder.encode(JSON.stringify({ data: "Step3" }) + '\n')); controller.close(); } }); return new Response(stream, { headers: { 'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked', 'Content-Encoding': 'identity', }, }); }
客户端代码与之前的流式读取逻辑一致。
内容的提问来源于stack exchange,提问作者niklas
相关产品推荐
相关产品推荐

