You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 04:40:27