Web 流式写入文件
本文最后更新于:2025年6月5日 早上
背景
由于吾辈之前使用的一个域名即将到期,需要将 IndexedDB 数据迁移到新的域名,因此这两天创建了一个新的浏览器扩展 IDBPort,用于迁移 IndexedDB 数据到其他域名。而在迁移数据时,需要将数据导出为并下载到本地,然后导入到新的域名。由于数据量较多,同时包含一些图像之类的二进制数据,所以需要使用流式写入的方式来避免内存占用过高。
首先,Web 中有什么 Target 可以流式写入数据吗?
实际上,是有的,例如 Blob+Response,或者 OPFS 私有文件系统,它们都支持流式写入数据到磁盘,考虑到 OPFS 仍然相对较新,所以下面使用 Blob+Response 来实现。
流式写入
如果不考虑流式写入,可以将数据全部都放在内存中的话,那么直接使用一个 string[]
来存储数组,然后一次性创建一个 Blob 对象,也是一种选择。但如果数据有数百 M(包含图像或视频)甚至上 G,那么内存就会爆掉,需要使用流式写入保持内存不会线形增长。在之前 在 Web 中解压大型 ZIP 并保持目录结构 中有提到过,但由于当时使用 zip.js,而它们直接提供了 BlobWriter/BlobReader 来使用,所以并未深入研究实现,也没有尝试手动实现。这里涉及到几个关键 API
- Blob: 二进制数据存储接口,它会在数据过多时透明的从内存转移到磁盘 [1],这保证了内存占用不会太大
- Response: Response 允许接收一个 ReadableStream 并创建一个 Blob 对象
- TransformStream:提供一个通道,提供一个 ReadableStream 和 WritableStream,让流式写入变的简单
- TextEncoderStream: 将一个文本流转换为 Uint8Array 流,这是 Response ReadableStream 所需要的数据格式
基本流程
- 创建 TransformStream
- 使用 ReadableStream 结合 TextEncoderStream 创建 Response
- 立刻获取 blob,触发 ReadableStream 的拉取
- 使用 WritableStream 开始写入
- 关闭 TransformStream
- await promise blob 来获取写入完成的 blob
10 行代码即可验证
1 |
|
流式读取
相比之下,流式读取使用的 API 要更少,只需要使用 blob.stream()
即可流式读取一个 Blob(或者一个一个 File)。几个关键的 API
- TextDecoderStream: 将一个 Uint8Array 字节流转换为文本流
由于 blob.stream()
返回的 chunk 可能存在截断或不完整,例如假使预期的 chunk 是按照换行分割点文本 line1\nline2\n
,blob.stream()
可能会返回 line1
甚至截断的 line1\nli
,所以必须使用自定义的 TransformStream 来将默认的流转换为预期的按行分割的流。
- 用户选择文件
- 得到 File(Blob 子类)
- file.stream() 流式读取
- 使用 TextDecodeStream 解码 Uint8Array 为文本
- 自定义 LineBreakStream 根据 line 分割 chunk
- 流式遍历
1 |
|
然后来验证它是否有效,下面写入了 3 个不规则的 chunk,但最终得到的结果仍然是 [ "line1", "line2" ]
,也就是说,LineBreakStream 生效了。
1 |
|
现在,来使用它读取 Blob 就很简单了。
1 |
|
总结
在浏览器中创建和读取大型文本文件似乎是个小众的需求,但如果确实需要,现代浏览器确实可以处理。考虑到之前做过的在线压缩工具,确认甚至可以处理数十 GB 尺寸的文件。