现代浏览器当中,File System Access API 允许网页在获得用户授权后直接读写本地文件,不再需要先上传到服务器。这个能力对在线代码编辑器、Markdown 笔记工具、日志分析页面等场景很有价值。不过原生 API 基于 Promise 和文件句柄,流程分散,每次调用都要处理权限、异常和浏览器差异。如果项目中已经引入了 jQuery,把这一组能力封装成简单的插件,就能用 $.fs.openFile() 这样直白的方式调用。

一、原生的 File System Access API 提供了什么
File System Access API 主要由三个核心方法构成:window.showOpenFilePicker() 用来弹出文件选择器并返回一个文件句柄数组,window.showSaveFilePicker() 用来弹出保存对话框并返回可写入的文件句柄,而 FileSystemFileHandle 则代表了用户选中的文件。通过文件句柄可以进一步调用 getFile() 获取 File 对象,或者调用 createWritable() 创建一个可写流。
原生调用过程并不复杂,但需要处理多个异步步骤。例如读取一个文本文件,必须依次完成选择文件、获取句柄、请求权限、调用 getFile、再读取 text,每一步都可能抛出异常。写入文件也一样,要先创建可写流,写入内容后再关闭流,任何一步遗漏都会导致数据不完整。对于复杂业务来说,直接在页面里散落这些逻辑既难维护也容易出错。
此外,API 对用户手势有严格限制。选择器方法必须在用户点击、键盘等交互事件中触发,否则浏览器会拒绝执行。权限状态也分 granted、prompt、denied 三种,需要根据状态决定是否重新请求。这些细节如果每次都手动处理,代码量会迅速膨胀。封装成统一的 jQuery 插件正好解决这些问题。
二、用 jQuery Deferred 封装核心方法
jQuery 提供了 Deferred 对象,它与原生 Promise 类似,但可以更方便地与旧版代码和 jQuery 事件体系结合。利用 Deferred 封装文件操作,可以让调用方使用 done、fail、always 等方法处理异步结果,而不必写冗长的 then 链。
下面是一段完整的插件封装代码,包含打开文件、读取文本、写入文本和另存为四个核心方法。每个方法都返回一个 Promise,内部统一处理异常和权限前置检查。
(function($) {
function ensureSupport() {
return !!(window.showOpenFilePicker && window.showSaveFilePicker);
}
function openFile(options) {
var def = $.Deferred();
if (!ensureSupport()) {
return def.reject(new Error('当前浏览器不支持 File System Access API')).promise();
}
var opts = $.extend({
types: [],
multiple: false
}, options);
var pickerOptions = {
multiple: opts.multiple
};
if (opts.types && opts.types.length) {
pickerOptions.types = opts.types;
}
window.showOpenFilePicker(pickerOptions).then(function(handles) {
if (!handles || handles.length === 0) {
def.reject(new Error('未选择任何文件'));
return;
}
def.resolve(handles[0]);
}).catch(function(err) {
def.reject(err);
});
return def.promise();
}
function readText(handle) {
var def = $.Deferred();
if (!handle || !handle.getFile) {
return def.reject(new Error('无效的文件句柄')).promise();
}
handle.getFile().then(function(file) {
return file.text();
}).then(function(text) {
def.resolve(text);
}).catch(function(err) {
def.reject(err);
});
return def.promise();
}
function writeText(handle, content) {
var def = $.Deferred();
if (!handle || !handle.createWritable) {
return def.reject(new Error('无效的文件句柄')).promise();
}
handle.createWritable().then(function(writable) {
writable.write(content).then(function() {
writable.close().then(function() {
def.resolve();
});
});
}).catch(function(err) {
def.reject(err);
});
return def.promise();
}
function saveAs(content, suggestedName) {
var def = $.Deferred();
if (!ensureSupport()) {
return def.reject(new Error('当前浏览器不支持 File System Access API')).promise();
}
var options = {
suggestedName: suggestedName || 'untitled.txt'
};
window.showSaveFilePicker(options).then(function(handle) {
handle.createWritable().then(function(writable) {
writable.write(content).then(function() {
writable.close().then(function() {
def.resolve(handle);
});
});
});
}).catch(function(err) {
def.reject(err);
});
return def.promise();
}
$.fs = {
openFile: openFile,
readText: readText,
writeText: writeText,
saveAs: saveAs,
isSupported: ensureSupport
};
})(jQuery);
代码中 openFile 方法支持传入文件类型过滤器,比如只允许选择文本文件可以写成 { types: [{ description: '文本文件', accept: { 'text/plain': ['.txt'] } }] }。读取文本时直接调用句柄的 getFile() 获得 File 对象,再通过 text() 拿到内容。写入方法必须按照创建可写流、写入数据、关闭流的顺序执行,创建可写流之后如果发生异常,最好也在 catch 中尝试关闭,避免句柄悬挂。
调用时只需要一行代码就能打开并读取文件,无需接触底层句柄细节:
$.fs.openFile().done(function(handle) {
$.fs.readText(handle).done(function(text) {
console.log('文件内容:', text);
}).fail(function(err) {
console.error('读取失败:', err);
});
}).fail(function(err) {
console.error('打开失败:', err);
});
这种风格与 jQuery 一贯的异步处理方式一致,开发者在已有项目中几乎不需要额外学习成本。对于只需要读取的应用,甚至可以把打开和读取合并成一个便捷方法,不过拆分开能保留更多灵活性,例如后续把文件句柄缓存起来用于编辑后写回。
三、文件句柄缓存和权限状态判断
实际使用中,用户往往希望编辑后直接保存回原文件,而不是每次都重新弹出选择器。FileSystemFileHandle 对象可以被结构化克隆,因此可以存储到 IndexedDB 中。这样下次打开页面时,在用户手势内调用 queryPermission 查看权限,如果权限仍然有效就能直接读写,无需再次选择文件。
处理权限状态时要注意 queryPermission 返回三种可能:granted 表示已有权限,prompt 表示需要用户重新授权,denied 表示权限被拒绝。下面的封装方法可以统一处理这些状态,并在需要时自动调用 requestPermission。
function ensurePermission(handle, mode) {
var def = $.Deferred();
var options = { mode: mode || 'readwrite' };
handle.queryPermission(options).then(function(state) {
if (state === 'granted') {
def.resolve();
return null;
}
if (state === 'prompt') {
return handle.requestPermission(options);
}
def.reject(new Error('权限已被拒绝'));
return null;
}).then(function(state) {
if (state === 'granted') {
def.resolve();
} else if (state === 'prompt') {
def.reject(new Error('未获得写入权限'));
} else if (state === 'denied') {
def.reject(new Error('用户拒绝了权限请求'));
}
}).catch(function(err) {
def.reject(err);
});
return def.promise();
}
需要注意的是,浏览器可能在用户长时间未访问后自动撤销权限,或者在不同会话中要求重新确认。因此即使缓存了句柄,也要在每次读写前执行权限检查。另外,任何调用 showOpenFilePicker 或 showSaveFilePicker 的代码都必须处于用户手势上下文中,例如绑定在按钮的 click 事件里。如果放在异步回调或定时器中触发,浏览器会直接抛出安全错误。
四、兼容性检测与降级方案
File System Access API 目前只在 Chromium 内核浏览器中得到较好支持,包括 Chrome 86 及以上版本和 Edge 86 及以上版本。Firefox 和 Safari 尚未实现该 API,因此在面向更广泛用户的应用中必须提供降级方案。检测支持情况很简单,只要判断 window.showOpenFilePicker 和 window.showSaveFilePicker 是否存在即可。
对于不支持的浏览器,可以退回使用传统的 <input type="file"> 元素打开文件,以及通过带 download 属性的 <a> 元素触发保存。下面是一个降级封装示例,它模拟了与原生 API 相似的 Promise 返回结构,让上层代码无需关心具体实现。
function fallbackOpenFile() {
var def = $.Deferred();
var input = document.createElement('input');
input.type = 'file';
input.onchange = function() {
var file = input.files[0];
if (file) {
def.resolve({ file: file, name: file.name });
} else {
def.reject(new Error('未选择文件'));
}
};
input.click();
return def.promise();
}
function fallbackSaveAs(content, filename) {
var def = $.Deferred();
var blob = new Blob([content], { type: 'text/plain' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = filename || 'download.txt';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
def.resolve();
return def.promise();
}
封装时可以根据 isSupported() 的返回值内部切换实现,上层始终调用同一套 openFile 或 saveAs 方法。降级方案的限制在于无法获取可持久化的文件句柄,每次保存都必须重新弹出保存对话框,也无法直接写回原文件。但作为不支持环境下的替代体验已经足够。
五、实战:本地 Markdown 编辑器
为了更直观地展示封装效果,这里以一个本地 Markdown 编辑器为例。页面包含一个 textarea 用于编辑内容,两个按钮分别负责打开文件和保存文件。打开文件时如果之前缓存了句柄则直接读取,否则弹出选择器。保存时优先尝试写入原句柄,如果权限不过则请求权限,若没有句柄则另存为新文件。
$(function() {
var currentHandle = null;
var currentFileName = '';
$('#openBtn').on('click', function() {
if (currentHandle) {
$.fs.readText(currentHandle).done(function(text) {
$('#editor').val(text);
}).fail(function() {
currentHandle = null;
$('#openBtn').trigger('click');
});
} else {
$.fs.openFile({
types: [{ description: 'Markdown 文件', accept: { 'text/markdown': ['.md', '.markdown'] } }]
}).done(function(handle) {
currentHandle = handle;
$.fs.readText(handle).done(function(text) {
$('#editor').val(text);
});
}).fail(function(err) {
alert('打开文件失败: ' + err.message);
});
}
});
$('#saveBtn').on('click', function() {
var content = $('#editor').val();
if (currentHandle) {
$.fs.writeText(currentHandle, content).done(function() {
alert('保存成功');
}).fail(function() {
alert('保存失败,请尝试另存为');
});
} else {
$.fs.saveAs(content, currentFileName || 'untitled.md').done(function(handle) {
currentHandle = handle;
alert('保存成功');
}).fail(function(err) {
alert('保存失败: ' + err.message);
});
}
});
});
这个示例展示了句柄缓存带来的体验提升:用户第一次打开文件后,后续保存会直接写回原文件,不需要反复选择路径。如果权限被浏览器自动撤销,写入失败提示用户另存为,既保护数据不丢失,也避免流程中断。在真实项目中还可以把句柄写入 IndexedDB,实现刷新页面后仍然记住上次编辑的文件。
File System Access API 为前端打开了一扇直接操作本地文件的门,配合 jQuery 的 Deferred 和插件模式,可以大幅降低使用门槛。无论是构建轻量编辑器、日志分析工具还是需要读写配置文件的内部系统,都能通过这一层封装获得简洁稳定的调用体验。需要注意的是,所有文件选择操作都要保证在用户手势中触发,并始终做好不支持环境下的降级准备。
File System Access APIjQuery前端本地文件读写修改时间:2026-10-06 01:22:09