本文详解为何 webkitGetAsEntry() 在本地文件场景下触发 URI 错误,以及如何通过现代、安全的 getAsFileSystemHandle() + getFile() 方案替代旧 API,实现本地拖放文件的可靠读取。
先说明一下本文要解决的核心问题:在本地打开的 HTML 页面(即以 file:// 协议运行)中使用拖放(Drag & Drop)API 读取文件时,常常会碰到这样一个让人头疼的错误:
DOMException: A URI supplied to the API was malformed, or the resulting Data URL has exceeded the URL length limitations for Data URLs.
这个错误看起来像是在说“你给的 URI 格式不对”,但其实跟手动拼接 URI 没有半毛钱关系。它真正的罪魁祸首,是那个已经退役多年的 webkitGetAsEntry() 方法——这个接口在 file:// 的上下文里触发了一种安全层面的限制。
简单解释一下:webkitGetAsEntry() 返回的是一个 FileSystemEntry 对象,本身看起来好像还能继续用 file() 方法读取内容。但问题在于,在本地环境中,它生成的私有文件系统 URI(比如 filesystem:file__0:Isolated_...)在浏览器看来属于“危险品”,直接被沙箱策略挡在了外头。最终结果就是 item.file(callback) 默默失败。
说起来,根子就出在下面这三点:
第一,webkitGetAsEntry() 是早期 WebKit/Blink 的实验性接口,从未被纳入正式标准。
第二,在 file:// 协议下,浏览器出于安全考虑,对它的限制非常严格。
第三,它无法安全地将本地文件路径转换成可读的 Blob 或 File 对象,从而导致 FileReader 流程彻底中断。
✅ 所以正确的解法是什么呢? —— 迁移到标准化的 File System Access API。目前它已经是 W3C 的推荐草案,Chrome、Edge、Bra ve 等 Chromium 内核浏览器都默认支持。
✅ 推荐方案:getAsFileSystemHandle() + getFile()
这套方案直接绕开了老旧的私有文件系统 URI,通过 getAsFileSystemHandle() 获取一个 FileSystemFileHandle,然后调用它的 getFile() 方法,返回一个标准的 File 对象。File 对象本身继承自 Blob,用 FileReader 来读是完全没有问题的。看看代码:
async function scanFiles(item, container) {
const elem = document.createElement("li");
elem.textContent = item.name;
container.appendChild(elem);
if (item.kind === "file") {
// ✅ 获取标准 File 对象(非 FileSystemFileEntry)
const file = await item.getFile();
const dataDiv = document.createElement("div");
dataDiv.innerHTML = "Loading...";
container.appendChild(dataDiv);
const fr = new FileReader();
fr.onload = (e) => {
dataDiv.innerHTML = `${e.target.result}`;
};
fr.onerror = (e) => {
console.error("Failed to read file:", e);
dataDiv.textContent = "Error reading file.";
};
fr.readAsText(file); // 或 readAsArrayBuffer / readAsDataURL
} else if (item.kind === "directory") {
const dirContainer = document.createElement("ul");
container.appendChild(dirContainer);
// ✅ 使用现代异步遍历(替代已废弃的 createReader().readEntries)
for await (const entry of item.values()) {
await scanFiles(entry, dirContainer);
}
}
}
dropzone.addEventListener("drop", async (event) => {
event.preventDefault();
listing.textContent = "";
const items = event.dataTransfer.items;
for (let i = 0; i < items.length; i++) {
try {
// ✅ 替换:items[i].webkitGetAsEntry() → items[i].getAsFileSystemHandle()
const handle = await items[i].getAsFileSystemHandle();
if (handle) {
await scanFiles(handle, listing);
}
} catch (err) {
console.warn("Failed to get handle for item", i, err);
}
}
});
⚠️ 注意事项与最佳实践
先看看浏览器是否支持:getAsFileSystemHandle() 需要 Chromium 86 以上的版本(Bra ve、Chrome、Edge 都已支持),Firefox 目前还没实现。生产环境一定要加特性检测:
if ('getAsFileSystemHandle' in DataTransferItem.prototype) { // 使用新 API } else { // 回退到 input[type=file] 或提示用户升级浏览器 }权限弹窗是躲不掉的:第一次调用 getAsFileSystemHandle() 时,浏览器会弹出一个“允许网站访问文件”的授权窗口。这是强制性的安全门控,不能跳过。
file:// 协议仍然有限制:哪怕用了新的 API,纯 file:// 页面下还是可能受到部分安全策略的影响(比如跨源 iframe)。所以开发阶段建议还是用本地服务器(比如 https://localhost)最稳妥。
旧 API 和新 API 别混着用:不要同时用 webkitGetAsEntry 和 getAsFileSystemHandle,前者已经明确被标为废弃,而且两者行为还不一致。
错误处理要做得显式:getFile() 和 getAsFileSystemHandle() 返回的都是 Promise,必须用 try/catch 或 .catch() 来捕获拒绝情况(比如用户点了取消授权,或者文件被移动/删除了)。
总结
事实非常清楚:webkitGetAsEntry() 的失败,本质上是一个历史遗留 API 在现代安全模型下的淘汰信号。迁移到 getAsFileSystemHandle() 不仅解决了本地文件读取的老问题,也更契合 Web 平台的发展方向——一切以用户明确授权为前提,对本地文件的访问做到可控、标准化。配合 await getFile() 拿到标准 File 对象后,就可以无缝接入 FileReader、fetch()(配合 URL.createObjectURL)、甚至 Streams API,轻松构建健壮的离线文件处理应用。
