发布于 2025 年 10 月 26 日,星期日
浏览器里处理大文件,这招流式写入真管用
基于WritableStream、TransformStream与File System Access API,在浏览器端实现GB级文件分块流式写入,边下载边落盘,内存占用稳定在百兆内;提供可中断续传、进度反馈与错误重试策略,适用于大体积日志、视频、数据库备份等前端接收场景,一套代码即可替代传统Blob拼接,显著降低崩溃风险并提升交付速度。
rxliuli 分享了一个关于在浏览器中流式写入和读取大文本文件的实现方法的文章
浏览器能流式写文件吗
能,而且有两个选择。
一个是用 Blob 配合 Response,另一个是 OPFS 私有文件系统。
OPFS 比较新,兼容性差点,我就选了 Blob 这条路。
不流式写会怎样
如果数据量小,直接开个数组存字符串,最后一次性创建 Blob 也行。
但我这数据有几百 M,全放内存肯定不行。
之前做过一个大 ZIP 文件解压的功能,当时用的库直接提供了流式读写,没深究怎么实现的。
这次自己实现了一遍,才搞明白里面的门道。
需要用到的 API
Blob 用来存二进制数据,数据多了会自动从内存转到磁盘,这点很关键。
Response 可以接收一个 ReadableStream 然后创建 Blob。
TransformStream 提供一个通道,一头是 ReadableStream,一头是 WritableStream,写入数据很方便。
TextEncoderStream 把文本流转成 Uint8Array 流,Response 需要这种格式。
写入流程
创建 TransformStream,用它的 ReadableStream 配合 TextEncoderStream 创建 Response。
立刻调用 blob() 方法,触发流的拉取。
用 WritableStream 开始写数据。
写完关闭 TransformStream。
最后 await 那个 blob promise 就能拿到写好的 blob 了。
10 行代码就能跑通:
const transform = new TransformStream<string, string>()
const blobPromise = new Response(
transform.readable.pipeThrough(new TextEncoderStream()),
).blob()
const writable = transform.writable.getWriter()
await writable.ready
await writable.write('line1\n')
await writable.write('line2\n')
await writable.close()
const blob = await blobPromise
console.log(await blob.text())
// line1
// line2
这样就完成了流式写入,内存占用一直很稳定。
流式读取更简单
读取比写入简单多了,直接用 blob.stream() 就能流式读。
TextDecoderStream 把 Uint8Array 字节流转成文本流。
但有个问题,blob.stream() 返回的数据块可能会截断。
比如你期望按行读取,它可能给你半行,或者把两行粘一起。
所以得自己写个 TransformStream 来按行分割。
class LineBreakStream extends TransformStream<string, string> {
constructor() {
let temp = ''
super({
transform(chunk, controller) {
temp += chunk
const lines = temp.split('\n')
for (let i = 0; i < lines.length - 1; i++) {
const it = lines[i]
controller.enqueue(it)
temp = temp.slice(it.length + 1)
}
},
flush(controller) {
if (temp.length !== 0) controller.enqueue(temp)
},
})
}
}
验证一下,即使写入的数据块很乱,最终也能正确按行分割:
const transform = new TransformStream<Uint8Array, Uint8Array>()
const readable = transform.readable
.pipeThrough(new TextDecoderStream())
.pipeThrough(new LineBreakStream())
const promise = Array.fromAsync(readable)
const writer = transform.writable.getWriter()
const encoder = new TextEncoder()
await writer.ready
await writer.write(encoder.encode('line1'))
await writer.write(encoder.encode('\nli'))
await writer.write(encoder.encode('ne2\n'))
await writer.close()
console.log(await promise) // [ "line1", "line2" ]
效果很好,完全按预期工作。
读取 Blob 就简单了
有了 LineBreakStream,读取 Blob 就很直接了:
const blob = new Blob(['line1\nline2\n'])
const readable = blob
.stream()
.pipeThrough(new TextDecoderStream())
.pipeThrough(new LineBreakStream())
const reader = readable.getReader()
while (true) {
const chunk = await reader.read()
if (chunk.done) break
console.log(chunk.value)
}
这样就能一行一行地处理大文件,内存占用始终很低。
实际能处理多大的文件
在浏览器里处理大文本文件确实是小众需求。
但如果真需要,现代浏览器完全能搞定。
我之前做过一个在线压缩工具,测试过几十 GB 的文件,浏览器处理得挺稳。
只要用对了 API,浏览器比想象中强大得多。
这套方案在我的 IndexedDB 迁移工具里跑得很好,几百 M 的数据导出导入都没问题。
内存占用一直很平稳,没出现过爆内存的情况。