深入探索浏览器File API与本地文件操作实战
先看效果,再看代码
上周接了个需求:让用户在网页里直接操作本地文件夹——增删改查、读写文本、甚至监控变化。你没听错,浏览器里操作本地文件系统。我第一反应是“这也能行?”但自从有了 File System Access API,还真能干。
别整那些虚的,先上一段核心代码,亲测有效:
async function openDirectory() {
const dirHandle = await window.showDirectoryPicker();
for await (const entry of dirHandle.values()) {
console.log(entry.name, entry.kind); // 文件名 + 类型(file 或 directory)
}
}
就这么几行,就能弹出系统选择框,选中一个文件夹后遍历里面所有文件和子目录。注意,for await...of 是必须的,因为 dirHandle.values() 返回的是异步迭代器。这里我踩过坑:一开始用 for...of 同步遍历,结果只拿到第一个就停了,折腾了半天才发现是异步问题。
这个场景最好用
我们有个内部工具,要批量处理 Markdown 文件生成配置。以前得手动拖拽上传,现在直接打开项目根目录,递归扫描所有 .md 文件:
async function scanMarkdownFiles(dirHandle) {
const files = [];
async function walk(handle) {
if (handle.kind === 'file') {
if (handle.name.endsWith('.md')) {
const file = await handle.getFile();
files.push({
name: handle.name,
path: handle.name, // 简化版路径,实际可拼接完整路径
content: await file.text()
});
}
} else if (handle.kind === 'directory') {
for await (const entry of handle.values()) {
await walk(entry);
}
}
}
await walk(dirHandle);
return files;
}
调用方式也很简单:
const dirHandle = await window.showDirectoryPicker();
const mdFiles = await scanMarkdownFiles(dirHandle);
console.log(mdFiles); // 所有 Markdown 内容都在这儿了
重点来了:用户一旦授权访问某个目录,下次刷新页面还能继续操作吗?答案是:不能直接访问。但你可以用 permission API 缓存句柄:
async function rememberPermission(dirHandle) {
const permission = await dirHandle.queryPermission({ mode: 'readwrite' });
if (permission === 'granted') {
// 可以考虑把 dirHandle 存到 IndexedDB,带上时间戳做缓存管理
await saveHandleToStorage(dirHandle); // 自定义存储逻辑
}
}
不过要注意,浏览器出于安全考虑,不会让你无限期保留权限。用户关闭页面或重启后,可能需要重新授权。目前 Chrome 和 Edge 支持较好,Firefox 还没跟上,Safari 基本别指望。
写入文件?小心权限问题
读是基础,写才是关键。我们的编辑器需要保存修改后的文件:
async function saveFile(handle, content) {
const writable = await handle.createWritable();
await writable.write(content);
await writable.close();
}
看着挺简单,但我在这儿栽了好几次。第一次运行时报错:Uncaught DOMException: The user aborted a request。排查半天发现是因为没有显式请求写权限。正确姿势是:
async function ensureWritable(handle) {
const permission = await handle.queryPermission({ mode: 'readwrite' });
if (permission !== 'granted') {
const newPerm = await handle.requestPermission({ mode: 'readwrite' });
if (newPerm !== 'granted') {
throw new Error('用户拒绝写入权限');
}
}
}
所以完整的保存流程应该是:
async function safeSaveFile(fileHandle, content) {
await ensureWritable(fileHandle);
const writable = await fileHandle.createWritable();
await writable.write(content);
await writable.close();
}
还有个细节:如果你要创建新文件而不是覆盖旧的,得先从目录句柄下手:
async function createNewFile(dirHandle, fileName, content) {
const fileHandle = await dirHandle.getFileHandle(fileName, { create: true });
await safeSaveFile(fileHandle, content);
return fileHandle;
}
{ create: true } 很关键,否则找不到文件会报错。但如果文件已存在,它也不会报错,而是直接返回现有句柄——相当于自动复用。
踩坑提醒:这三点一定注意
- HTTPS 是硬性要求:本地开发可以用
http://localhost,但部署到线上必须走 HTTPS,否则 API 根本不会暴露出来。别问我怎么知道的,线上环境突然挂掉的时候差点背锅。 - 移动端基本不用想:手机浏览器基本不支持这些 API,尤其是 iOS。这个功能目前只适合桌面端内部工具或 Electron 类应用。
- 文件句柄不是永久的:虽然可以存到 IndexedDB,但重启浏览器后再次使用时仍需重新验证权限。别指望一次授权永久生效。
还有一个容易被忽略的问题:大文件读写。我试过处理一个 200MB 的日志文件,直接 file.text() 导致页面卡死。后来改成流式读取:
async function readLargeFile(fileHandle) {
const file = await fileHandle.getFile();
const stream = file.stream();
const reader = stream.getReader();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
result += new TextDecoder().decode(value, { stream: true });
// 可在此处加进度反馈
}
return result;
}
虽然麻烦点,但至少不会把内存撑爆。
高级技巧:监听文件变化?其实不行
你以为能像 Node.js 那样用 fs.watch 监听文件变动?醒醒,浏览器没这功能。File System Access API 完全被动,没有事件机制。
但我们有个 workaround:轮询检测最后修改时间。虽然 low,但有效:
async function getFileLastModified(handle) {
const file = await handle.getFile();
return file.lastModified;
}
// 轮询检查
let lastTime = null;
async function startPolling(handle, interval = 3000) {
while (true) {
await new Promise(r => setTimeout(r, interval));
const current = await getFileLastModified(handle);
if (lastTime && current > lastTime) {
console.log('文件被修改了!');
// 触发重新加载或其他逻辑
}
lastTime = current;
}
}
当然,频繁轮询会影响性能,建议只在用户处于编辑状态时开启,并提供开关选项。
拓展玩法:配合 Worker 处理密集任务
既然能读大文件,那不如把解析工作丢给 Web Worker。比如我们要分析一堆 JSON 日志:
// main thread
async function processLogsInWorker(handles) {
const worker = new Worker('/log-processor.js');
const files = await Promise.all(handles.map(h => h.getFile()));
const data = files.map(f => ({
name: f.name,
lastModified: f.lastModified
}));
worker.postMessage({ type: 'start', files: data }, files.map(f => f.stream()));
}
// log-processor.js (Worker)
self.onmessage = async function(e) {
if (e.data.type === 'start') {
for (const stream of e.ports) {
const reader = stream.getReader();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (value) buffer += new TextDecoder().decode(value);
// 按行解析等逻辑…
if (done) break;
}
}
}
};
利用 Transferable Streams,可以把文件流直接传给 Worker,避免主线程阻塞。这个组合拳在处理大型项目时特别有用。
结语:这玩意儿还没成熟,但真香
File System Access API 现在还是实验性功能,兼容性和稳定性都有限。但在特定场景下,比如内部管理系统、文档编辑器、代码片段工具,它的价值非常明显。
我的建议是:不要拿来搞通用产品,但如果你做的工具主要跑在 Chromium 内核浏览器上(比如公司统一配的电脑),完全可以大胆用。毕竟比起传统上传下载那一套,这种原生般的体验提升太大了。
以上是我踩坑后的总结,希望对你有帮助。这个技术的拓展用法还有很多,后续会继续分享这类博客。

暂无评论