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

NextJs 13/14中如何在客户端展示Server Action执行进度?

Next.js 13/14 Server Action 实时反馈执行进度方案

Server Action本身是单次请求响应模式,无法直接流式返回执行进度,需要结合额外的通信机制实现实时状态反馈,以下是两种可行方案:

方案一:Server-Sent Events (SSE) 单向推送进度

SSE是最适配进度更新场景的方案,支持服务器主动向客户端推送消息,无需客户端频繁发起请求。

实现步骤

  1. 客户端触发任务时生成唯一taskId,同时开启SSE连接监听进度
  2. 服务器端处理数据库记录时,实时更新对应taskId的进度数据
  3. SSE接口定期将进度推送给客户端,客户端同步更新UI

代码示例

客户端组件(use client)

'use client';

import { useState } from 'react';

export default function ProgressDemo() {
  const [progress, setProgress] = useState(0);
  const [isRunning, setIsRunning] = useState(false);

  const startTask = async () => {
    setIsRunning(true);
    setProgress(0);

    // 生成唯一任务ID
    const taskId = crypto.randomUUID();

    // 建立SSE连接
    const eventSource = new EventSource(`/api/progress?taskId=${taskId}`);
    
    eventSource.onmessage = (event) => {
      const { progress: currentProgress, done } = JSON.parse(event.data);
      setProgress(currentProgress);
      if (done) {
        eventSource.close();
        setIsRunning(false);
      }
    };

    eventSource.onerror = () => {
      eventSource.close();
      setIsRunning(false);
      alert('任务执行出错');
    };

    // 触发数据处理接口
    await fetch('/api/process-records', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ taskId })
    });
  };

  return (
    <div className="p-4">
      <h2>批量处理记录进度</h2>
      {isRunning ? (
        <>
          <div className="w-full h-4 bg-gray-200 rounded-full overflow-hidden mt-2">
            <div 
              className="h-full bg-blue-600 transition-all duration-100" 
              style={{ width: `${progress}%` }}
            />
          </div>
          <p className="mt-2">已完成:{progress}%</p>
        </>
      ) : (
        <button 
          onClick={startTask} 
          disabled={isRunning}
          className="px-4 py-2 bg-blue-600 text-white rounded"
        >
          开始处理1000条记录
        </button>
      )}
    </div>
  );
}

数据处理API(app/api/process-records/route.ts)

import { NextResponse } from 'next/server';

// 生产环境建议用Redis替代内存Map
const taskProgressStore = new Map<string, { progress: number; done: boolean }>();

export async function POST(request: Request) {
  const { taskId } = await request.json();
  taskProgressStore.set(taskId, { progress: 0, done: false });

  try {
    const total = 1000;
    for (let i = 0; i < total; i++) {
      // 替换为你的实际数据库操作逻辑
      // await db.record.update(...)

      // 更新进度
      const currentProgress = Math.round(((i + 1) / total) * 100);
      taskProgressStore.set(taskId, { progress: currentProgress, done: i === total - 1 });

      // 模拟处理耗时
      await new Promise(resolve => setTimeout(resolve, 10));
    }

    return NextResponse.json({ success: true });
  } catch (error) {
    taskProgressStore.set(taskId, { progress: 0, done: true });
    return NextResponse.json({ success: false }, { status: 500 });
  }
}

SSE进度推送API(app/api/progress/route.ts)

import { NextResponse } from 'next/server';
import { taskProgressStore } from '../process-records/route';

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const taskId = searchParams.get('taskId');

  if (!taskId) {
    return NextResponse.json({ error: '缺少任务ID' }, { status: 400 });
  }

  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();

      // 每100ms推送一次进度
      const interval = setInterval(() => {
        const progressData = taskProgressStore.get(taskId);
        if (!progressData) {
          controller.close();
          clearInterval(interval);
          return;
        }

        // 发送SSE格式的消息
        controller.enqueue(encoder.encode(`data: ${JSON.stringify(progressData)}\n\n`));

        if (progressData.done) {
          controller.close();
          clearInterval(interval);
          taskProgressStore.delete(taskId); // 清理过期任务
        }
      }, 100);

      // 客户端断开连接时清理资源
      request.signal.addEventListener('abort', () => {
        clearInterval(interval);
        controller.close();
        taskProgressStore.delete(taskId);
      });
    }
  });

  return new NextResponse(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      'Connection': 'keep-alive',
    },
  });
}

方案二:客户端轮询进度

如果不想引入SSE,也可以用简单的轮询方案,适合小体量任务:

  1. 客户端触发任务后,每隔固定时间(比如500ms)请求进度接口
  2. 服务器返回对应taskId的进度数据,直到任务完成
  3. 客户端根据返回结果更新UI

该方案实现简单,但频繁请求会增加服务器压力,不适合大规模任务场景。

注意事项

  • 生产环境存储:示例中用内存Map存储进度,多实例部署时会丢失数据,建议用Redis或数据库持久化任务进度
  • 连接稳定性:SSE可能存在超时断开的情况,客户端需添加重连逻辑
  • 资源清理:任务完成或客户端断开后,要及时清理服务器端的进度缓存,避免内存泄漏

内容的提问来源于stack exchange,提问作者Gerald Fuchs

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 13:23:22