webkitdirectory 可递归获取子文件夹文件但丢失路径信息,需通过 file.webkitrelativepath 提取相对路径;firefox 不支持,无原生兼容方案;后端接收时须校验路径防遍历攻击。

webkitdirectory 是 Chrome、Edge、Safari(部分版本)支持的非标准 HTML 属性,用于让 <input type="file"> 元素直接选择整个文件夹(而非单个文件),并递归读取其内所有文件。但它不被 Firefox 支持,且行为与标准 Web API 存在关键差异——不能直接上传目录结构,只能获取扁平化的文件列表。
为什么 webkitdirectory 选不了子文件夹里的文件?
它确实会递归列出子目录中的文件,但 Event.target.files 返回的是一个 FileList,所有文件都“压平”在顶层,**丢失原始路径信息**。比如选中 /project/src/,里面有个 /project/src/utils/helper.js,你拿到的只是 helper.js 这个 File 对象,File.webkitRelativePath 字段才保存了相对路径(如 "src/utils/helper.js")。
常见错误是直接忽略 webkitRelativePath,导致后端无法还原目录结构。
- 必须显式读取每个
File的.webkitRelativePath属性 - 该属性只在
webkitdirectory触发时存在,普通单文件选择无此字段 - Safari 16.4+ 才稳定支持
webkitRelativePath;旧版 Safari 可能返回空字符串
怎么用 webkitdirectory 正确获取带路径的文件?
核心是监听 change 事件后遍历 event.target.files,提取 .webkitRelativePath 并构造可上传的数据结构。
const input = document.querySelector('input[type="file"][webkitdirectory]');
input.addEventListener('change', (e) => {
Array.from(e.target.files).forEach(file => {
const path = file.webkitRelativePath || file.name; // fallback
console.log(path, file.size); // 如 "src/index.js", 1234
});
});
-
webkitdirectory必须配合multiple使用,否则只触发一次且仅得根目录名(不是文件) - 不要依赖
file.path—— 它是 Chromium 内部私有字段,未暴露给 JS,且已被移除 - 若需上传,建议用
FormData逐个 append,并以路径为 key:formData.append('files', file, path)
Firefox 不支持怎么办?有没有兼容方案?
没有原生等效替代。Firefox 明确拒绝实现 webkitdirectory,因其违背同源策略和安全模型(目录枚举可能泄露本地结构)。目前可行路径只有两个:
- 引导用户使用 Chrome/Edge/Safari —— 对内部工具或可控环境最实际
- 改用桌面端桥接(如 Electron +
dialog.showOpenDialog)或 WebKit-based WebView 封装
试图用 showDirectoryPicker()(File System Access API)替代?注意:它虽是标准 API,但默认只返回目录句柄,**不自动递归读取内容**,需手动调用 dirHandle.values() 遍历,且目前仅 Chrome/Edge 支持,Firefox 仍无计划支持。
后端接收时要注意什么?
服务端不会自动理解前端传来的路径字符串。如果用 multipart/form-data 上传多个同名字段(如 files),需确保后端框架能正确解析多值,并保留原始字段名或额外传参标识路径。
- Node.js + Express:用
multer时需配置storage.filename回调,从req.body.path或字段名提取路径 - Python + Flask:
request.files.getlist("files")得到文件列表,但路径信息必须通过额外字段(如request.form.getlist("paths"))同步传入 - 绝对避免把
webkitRelativePath当作可信路径直接拼接服务器文件系统路径 —— 存在路径遍历风险,务必做白名单校验或规范化处理
真正麻烦的从来不是怎么选文件夹,而是怎么让服务端相信这个 “../../etc/passwd” 是用户故意输的,而不是浏览器悄悄塞进来的。










