如何在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
相关产品推荐
相关产品推荐

