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

如何在Next.js中向客户端组件逐次流式传输本地文件分块?

如何在Next.js中实现本地文件的分块流式传输(逐块请求)

我尝试将本地文件的数据分块流式传输到Next.js客户端组件,当前通过fetch调用服务端GET路由时,只能获取文件的第一块数据,无法获取后续分块。期望实现以下流程:

  • 客户端点击开始读取文件流
  • 服务端路由发送第一块数据
  • 客户端请求第二块数据
  • 服务端路由发送第二块数据
  • 以此类推,直到所有分块传输完成

现有代码如下:

客户端组件(app/useStreamingData/page.tsx)

"use client";
import React, { useEffect, useState } from "react";

const UseStreamingData = () => {
  const [currentChunk, setCurrentChunk] = useState<string>("");
  useEffect(() => {
    if (currentChunk !== "") {
      setNextChunk();
    }
  }, [currentChunk]);

  const setNextChunk = async () => {
    let response = await fetch("http://localhost:3000/stream-file");
    // 每次都只返回第一块,如何获取下一块?
    let json = await response.json();
    setCurrentChunk(json.data);
  };

  return <div onClick={setNextChunk}>UseStreamingData</div>;
};

export default UseStreamingData;

服务端路由(app/stream-file/route.ts)

import { createReadStream } from "fs";
import { NextResponse } from "next/server";
const filepath = "path/to/app/bigFile.json";

export async function GET(): Promise<NextResponse<{ data: string; error: string }>> {
  let response = await resolveChunkPromise(filepath);
  return NextResponse.json(response);
}

function resolveChunkPromise(filepath: string): Promise<{ data: string; error: string }> {
  return new Promise((resolve, reject) => {
    // 创建本地文件可读流,设置小缓冲区来拆分数据
    const readstream = createReadStream(filepath, { highWaterMark: 16 });
    readstream
      .on("data", (chunk) => {
        // 只返回第一块,剩余分块从未发送到客户端
        resolve({ data: chunk.toString(), error: "" });
      })
      .on("error", (err) => {
        reject({ data: "", error: err.message });
      });
  });
}

我试过类似react-fetch-streams的钩子但没成功,can-ndjson-stream也不适用(它只支持换行分隔的JSON),找到的示例大多没提供服务端代码,且都是基于外部URL而非本地文件。


解决方案

要实现逐块请求,核心是服务端需要跟踪文件的读取偏移量,客户端每次请求时带上当前已读取的位置,服务端从该位置开始返回下一块数据。

1. 修改服务端路由(app/stream-file/route.ts)

需要接收客户端传来的offset参数,从指定位置开始读取文件,并返回当前读取的字节数和是否完成的标识:

import { createReadStream, statSync } from "fs";
import { NextRequest, NextResponse } from "next/server";
const filepath = "path/to/app/bigFile.json";

export async function GET(request: NextRequest): Promise<NextResponse> {
  try {
    // 获取客户端传来的偏移量,默认从0开始
    const offset = Number(request.nextUrl.searchParams.get("offset")) || 0;
    const fileStats = statSync(filepath);
    const fileSize = fileStats.size;

    // 如果偏移量已超过文件大小,返回完成标识
    if (offset >= fileSize) {
      return NextResponse.json({ data: "", done: true, error: "" });
    }

    // 从offset位置开始读取,每次读取16字节(可根据需求调整)
    const chunkData = await new Promise<string>((resolve, reject) => {
      const readStream = createReadStream(filepath, {
        start: offset,
        highWaterMark: 16,
      });

      let chunk = "";
      readStream.on("data", (data) => {
        chunk += data.toString();
        readStream.destroy(); // 读取一块后立即停止
      });

      readStream.on("end", () => resolve(chunk));
      readStream.on("error", (err) => reject(err.message));
    });

    return NextResponse.json({
      data: chunkData,
      done: offset + chunkData.length >= fileSize,
      error: "",
      bytesRead: chunkData.length,
    });
  } catch (err) {
    return NextResponse.json({
      data: "",
      done: false,
      error: (err as Error).message,
      bytesRead: 0,
    });
  }
}

2. 修改客户端组件(app/useStreamingData/page.tsx)

需要维护当前的读取偏移量offset,每次请求带上该参数,根据服务端返回的done标识判断是否停止请求:

"use client";
import React, { useState } from "react";

const UseStreamingData = () => {
  const [currentChunk, setCurrentChunk] = useState<string>("");
  const [offset, setOffset] = useState<number>(0);
  const [isDone, setIsDone] = useState<boolean>(false);
  const [error, setError] = useState<string>("");

  const fetchNextChunk = async () => {
    if (isDone) return;

    try {
      const response = await fetch(`http://localhost:3000/stream-file?offset=${offset}`);
      const result = await response.json();

      if (result.error) {
        setError(result.error);
        return;
      }

      if (result.done) {
        setIsDone(true);
      }

      if (result.data) {
        setCurrentChunk(prev => prev + result.data); // 拼接所有分块(可选,根据需求处理)
        setOffset(prev => prev + result.bytesRead);
      }
    } catch (err) {
      setError((err as Error).message);
    }
  };

  return (
    <div>
      <button onClick={fetchNextChunk} disabled={isDone || error}>
        {isDone ? "读取完成" : error ? "出错了" : "读取下一块"}
      </button>
      {error && <p style={{ color: "red" }}>错误:{error}</p>}
      <div>已读取内容:{currentChunk}</div>
    </div>
  );
};

export default UseStreamingData;

说明

  • 服务端通过statSync获取文件大小,判断是否已读取完成;
  • 客户端每次请求带上当前偏移量,服务端从该位置返回下一块数据;
  • 客户端可以选择拼接所有分块,或单独处理每一块;
  • 增加了错误处理,避免请求失败导致的死循环;

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 00:45:23