File System Access API 实战:浏览器端本地文件读写架构
浏览器文件访问的演进
| 方案 | 读写能力 | 持久化 | 权限模型 | 性能 |
|---|---|---|---|---|
<input type="file"> |
只读 | ❌ | 无 | 中 |
| File + FileReader | 只读 | ❌ | 无 | 中 |
| IndexedDB | 读写字节 | ✅ | 同源 | 低(序列化) |
| File System Access | 读写 | ✅ | 用户授权 | 高 |
| OPFS | 读写 | ✅ | 同源沙箱 | 最高 |
File System Access API 让浏览器真正具备了本地应用的文件读写能力。
文件选择器:showOpenFilePicker / showSaveFilePicker
打开文件
const [fileHandle] = await window.showOpenFilePicker({
types: [{
description: '图片文件',
accept: { 'image/*': ['.png', '.jpg', '.webp'] },
}],
multiple: false,
});
const file = await fileHandle.getFile();
const arrayBuffer = await file.arrayBuffer();
保存文件
const handle = await window.showSaveFilePicker({
suggestedName: 'compressed.webp',
types: [{
description: 'WebP 图片',
accept: { 'image/webp': ['.webp'] },
}],
});
const writable = await handle.createWritable();
await writable.write(compressedBlob);
await writable.close();
FileSystemFileHandle:读写与权限
读取文件内容
const fileHandle: FileSystemFileHandle = handle;
const file = await fileHandle.getFile();
// 多种读取方式
const text = await file.text(); // 文本
const buffer = await file.arrayBuffer(); // 二进制
const stream = file.stream(); // 流式读取
写入文件
async function writeFile(handle: FileSystemFileHandle, content: Blob | string | BufferSource) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
权限查询与请求
async function verifyPermission(handle: FileSystemFileHandle, readWrite = true) {
const options: FileSystemHandlePermissionDescriptor = { mode: readWrite ? 'readwrite' : 'read' };
if ((await handle.queryPermission(options)) === 'granted') return true;
if ((await handle.requestPermission(options)) === 'granted') return true;
return false;
}
权限模型:首次通过 picker 获取的 handle 自动获得读权限。写权限需要用户显式授权(requestPermission 会弹出提示)。
目录访问与遍历
const dirHandle = await window.showDirectoryPicker();
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
const file = await handle.getFile();
console.log(`文件: ${name}, 大小: ${file.size}`);
} else if (handle.kind === 'directory') {
console.log(`目录: ${name}`);
}
}
递归查找文件
async function* findFiles(dirHandle: FileSystemDirectoryHandle, pattern: RegExp) {
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file' && pattern.test(name)) {
yield { name, handle };
} else if (handle.kind === 'directory') {
yield* findFiles(handle, pattern);
}
}
}
// 查找所有 PDF 文件
for await (const pdf of findFiles(dirHandle, /\.pdf$/)) {
console.log(pdf.name);
}
Origin Private File System (OPFS)
OPFS 是浏览器为每个源提供的私有沙箱文件系统,无需用户授权:
const opfsRoot = await navigator.storage.getDirectory();
// 创建文件
const fileHandle = await opfsRoot.getFileHandle('work-data.bin', { create: true });
// 创建子目录
const subDir = await opfsRoot.getDirectoryHandle('cache', { create: true });
// 删除文件
await opfsRoot.removeEntry('work-data.bin');
OPFS vs IndexedDB
| 特性 | IndexedDB | OPFS |
|---|---|---|
| 存储类型 | 结构化数据 | 文件(二进制) |
| 读写方式 | 事务 + 序列化 | 直接文件 I/O |
| 大文件性能 | 差(序列化开销) | 优秀 |
| 随机访问 | ❌ | ✅(Access Handle) |
| 容量 | 同源限制 | 同源限制 |
| Worker 支持 | ✅ | ✅ |
Access Handle:高性能随机读写
OPFS 文件支持 createSyncAccessHandle(),提供同步、随机位置的文件读写——性能接近原生文件系统:
const fileHandle = await opfsRoot.getFileHandle('large-data.bin', { create: true });
const accessHandle = await fileHandle.createSyncAccessHandle();
// 写入数据到指定位置
const writeBuffer = new Uint8Array([1, 2, 3, 4, 5]);
accessHandle.write(writeBuffer, { at: 0 });
// 读取指定位置的数据
const readBuffer = new Uint8Array(5);
accessHandle.read(readBuffer, { at: 0 });
// 获取文件大小
const fileSize = accessHandle.getSize();
// 刷新到磁盘
accessHandle.flush();
// 关闭句柄
accessHandle.close();
关键优势:Access Handle 是同步的,可在 Worker 中使用,无需 async/await,性能比 WritableStream 快 10-100 倍。
实战:PDF 合并的文件读写架构
工具库 PDF 合并 利用 File System Access API 实现"打开→处理→保存"的完整流程:
async function mergePdfWorkflow() {
// 1. 用户选择多个 PDF
const handles = await window.showOpenFilePicker({
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
multiple: true,
});
// 2. 读取所有文件
const buffers: ArrayBuffer[] = [];
for (const handle of handles) {
const file = await handle.getFile();
buffers.push(await file.arrayBuffer());
}
// 3. 合并处理
const mergedPdf = await mergePdfs(buffers);
// 4. 保存结果
const saveHandle = await window.showSaveFilePicker({
suggestedName: 'merged.pdf',
types: [{ accept: { 'application/pdf': ['.pdf'] } }],
});
const writable = await saveHandle.createWritable();
await writable.write(mergedPdf);
await writable.close();
}
持久化文件句柄
通过 IndexedDB 保存 handle,下次打开时恢复访问:
// 保存句柄
async function saveHandle(key: string, handle: FileSystemFileHandle) {
const db = await openDB('file-handles', 1, {
upgrade(db) { db.createObjectStore('handles'); },
});
await db.put('handles', handle, key);
}
// 恢复句柄
async function loadHandle(key: string) {
const db = await openDB('file-handles', 1);
const handle: FileSystemFileHandle = await db.get('handles', key);
// 验证权限
if (await verifyPermission(handle, true)) {
return handle;
}
return null;
}
常见问题
File System Access API 的浏览器支持?
Chrome/Edge 86+ 完整支持。Firefox 和 Safari 未支持。需要做特性检测和降级处理。
OPFS 的存储容量限制?
OPFS 与 Cache API、IndexedDB 共享同源存储配额(通常为可用磁盘空间的 50%+)。可通过 navigator.storage.estimate() 查询。
Access Handle 能在主线程用吗?
可以,但同步 I/O 会阻塞主线程。最佳实践是在 Worker 中使用 Access Handle,避免阻塞 UI。
如何处理大文件写入?
使用 WritableStream 的分块写入,避免一次性加载整个文件到内存:
const writable = await handle.createWritable();
for await (const chunk of readableStream) {
await writable.write(chunk);
}
await writable.close();
总结
File System Access API 让浏览器具备了本地应用级的文件读写能力。showOpenFilePicker/showSaveFilePicker 提供用户授权的文件访问,FileSystemFileHandle 管理读写权限,OPFS 提供无需授权的沙箱存储,Access Handle 实现高性能同步随机读写。这是构建浏览器端文件处理工具的核心基础设施。
本站提供浏览器本地工具,免注册即可试用 →