Browser Storage APIs for 文件传输 Applications
在文件传输应用中使用 IndexedDB、File System Access API 和 Cache API。存储限额、性能与浏览器兼容性。
文件传输应用的浏览器存储分布在四个 API 上:IndexedDB 用于结构化会话和分块元数据(事务性、异步、有类型);File System Access API 用于直接读写用户磁盘上的多 GB 文件;Cache API 用于 HTTP 响应和应用外壳资源;Storage Manager API 用于配额管理和持久性提示。源私有文件系统(OPFS)提供沙盒化高性能 I/O。选择正确的 API 很重要,因为配额从 iOS Safari 的1GB到桌面 Chrome 的"可用磁盘的60%"不等——选错了,生产环境最终会出现 QuotaExceededError。
每个 API 对应的正确用途
IndexedDB 适合任何看起来像数据行的内容:上传会话、已完成分块索引、分享元数据、撤销令牌。它是异步、事务性的,可索引,跨会话持久。
File System Access API 适用于需要将字节写入磁盘而不将整个文件加载进内存的场景——理想情况下用于保存超过500MB的解密下载。Chrome、Edge 和 Opera 支持完整功能;Firefox 和 Safari 仅通过 showOpenFilePicker 实现只读子集。
Cache API 用于存储 HTTP Response 对象——你的 JS 包、CSS、图标,以及可能缓存的 API 响应。它针对 Service Worker 中的 fetch 拦截做了优化。
OPFS(File System Access API 的源私有分支)适合需要快速沙盒化、用户不可见存储的场景,例如多 GB 加密过程中的写缓冲。它的磁盘吞吐量比 IndexedDB 处理二进制 blob 高出一个数量级。
没有坑的 IndexedDB
原生 IndexedDB 的事件驱动 API 出了名的别扭。使用 Jake Archibald 的 idb 包(gzip 后1.5KB)或 Dexie.js(20KB,更丰富的查询 API):
import { openDB } from 'idb';
const db = await openDB('transfers', 2, {
upgrade(db, oldVersion) {
if (oldVersion < 1) {
const sessions = db.createObjectStore('sessions', { keyPath: 'id' });
sessions.createIndex('by_expiry', 'expiresAt');
}
if (oldVersion < 2) {
db.createObjectStore('chunks', { keyPath: ['sessionId', 'index'] });
}
}
});
await db.put('sessions', { id: 'abc', fileName: 'report.pdf', expiresAt: Date.now() + 86400000 });
版本迁移在 upgrade 回调中执行。始终用 oldVersion 保护迁移,这样从 v1 直接升级到 v3 的用户也能执行两个迁移步骤。
IndexedDB 通过结构化克隆处理大多数数据类型,包括 Blob 和 File 引用。这意味着你可以在会话记录中存储 File 句柄,标签页重载后重新读取原始文件字节——完美支持断点续传。
File System Access API 用于大文件下载
该 API 允许你向浏览器保存对话框传递可写流:
const handle = await window.showSaveFilePicker({
suggestedName: 'decrypted-archive.zip',
types: [{ description: 'Zip', accept: { 'application/zip': ['.zip'] } }]
});
const writable = await handle.createWritable();
await decryptionStream.pipeTo(writable);
字节直接流向磁盘,不会进入 JS 堆。这是在浏览器中保存10GB解密文件的唯一可行方式。
Firefox 和 Safari 上可回退到 StreamSaver.js,它利用 Service Worker 合成流式响应来触发下载 UI。接口相同,组成部件稍多。
持久文件句柄还允许应用跨会话重新打开文件。用户通过 showOpenFilePicker 授权一次后,你可以将 FileSystemFileHandle 持久化到 IndexedDB,之后调用 handle.requestPermission() 重新获取访问权,无需每次都重新提示。
OPFS 用作临时工作区
源私有文件系统是每个源的沙盒存储,行为类似文件系统,但对用户不可见:
const root = await navigator.storage.getDirectory();
const fh = await root.getFileHandle('scratch.bin', { create: true });
const access = await fh.createSyncAccessHandle(); // 仅限 workers
access.write(buffer, { at: offset });
access.flush();
access.close();
createSyncAccessHandle 仅在 Web Worker(包括 Service Worker)内可用。它是同步的,速度极快——基准测试显示顺序写入比 IndexedDB 快3-10倍。用它缓冲数百 MB 的加密输出再上传,或缓存解密后的工作副本而不污染用户的下载文件夹。
Safari 17 已发布带同步访问句柄的 OPFS,Firefox 111 随后跟进。三大主流浏览器均已支持,可用于生产代码。
存储配额及如何应对
所有 API 共享同一源配额池。大致上限:
- 桌面 Chrome:可用磁盘的60%
- 桌面 Firefox:可用磁盘的50%,每源默认上限2GB
- 桌面 Safari:提示1GB,用户批准后增至约磁盘的20%
- iOS Safari:每源1GB,未使用7天后强制驱逐
- Chrome Android:可用磁盘的10%,磁盘压力下驱逐
运行时检查配额:
const { quota, usage } = await navigator.storage.estimate();
console.log(`Using ${(usage/1e9).toFixed(2)} GB of ${(quota/1e9).toFixed(2)} GB`);
为关键存储请求持久性:
const persisted = await navigator.storage.persist();
返回 true 表示浏览器授予了持久存储,意味着磁盘压力下不会被驱逐。Chrome 对用户有足够交互的站点自动授予;Firefox 会弹出提示。
Cache API 用于应用外壳和离线
Cache API 存储 Request + Response 键值对,是 Service Worker 内部的正确选择:
const cache = await caches.open('shell-v7');
await cache.addAll([
'/', '/app.js', '/app.css', '/icons/192.png'
]);
拦截时检索:
self.addEventListener('fetch', (e) => {
e.respondWith(caches.match(e.request).then(r => r ?? fetch(e.request)));
});
不要将加密文件字节放入 Cache API。一个2GB的 Response 对象会立刻耗尽 iOS Safari 的配额,而且事后无法范围获取。字节应放入 OPFS 或通过 File System Access 直接写入磁盘。
优雅处理驱逐和数据丢失
非持久存储会被驱逐——必须做好规划。iOS Safari 在7天未使用后驱逐,不管配额是否充足。Chrome 只在磁盘实际有压力时驱逐。Firefox 在配额池满时驱逐最近最少使用的源。
两种防御模式:
- 将任何无法重建的状态(上传会话 ID、部分分块偏移)以对页面重载友好的格式写入,使新页面加载时能从服务器重新获取并继续。
- 对长期状态,调用
navigator.storage.persist()并在浏览器提示时为用户展示确认 UI。
对于任何不能丢失的数据,以服务器为真实来源。将浏览器存储视为可能在一夜间消失的高速缓存。
浏览器支持的坑
三个坑反复出现:
indexedDB.databases()枚举在 Firefox 中不支持(选择了"关闭时删除 Cookie"的用户会在没有任何事件触发的情况下丢失所有 IndexedDB 内容)。FileSystemFileHandle.queryPermission()在重载后行为不一致——有时即便已授权也返回'prompt'。始终防御性地调用requestPermission()。- 隐私/无痕模式给所有三个 API 分配独立的、更小的、仅限会话的配额。普通浏览中正常运行的代码在隐私窗口中可能立即触发
QuotaExceededError。
HexaTransfer 使用 IndexedDB 存储会话状态,OPFS 在流式加密过程中缓冲密文,File System Access API 在支持的浏览器上处理10GB解密文件下载。免费试用 hexatransfer.com——无需注册,单次最大10GB。
为你的应用选择技术栈
对大多数传输应用而言,正确的组合是:IndexedDB 上层用 idb 封装处理元数据,OPFS 同步访问句柄用于加解密临时空间,Cache API 在 Service Worker 中处理应用外壳,File System Access API 用于最终下载并以 StreamSaver 兜底,以及在引导过程中调用 navigator.storage.persist()。这涵盖了当前所有主流浏览器,在移动端保持配额可控,并在驱逐发生时优雅恢复。为每个 API 构建小型适配器,以后 OPFS 增加新方法或 Safari 提升配额上限,只需改一个文件即可发布。