浏览器不支持直接上传文件夹,可用webkitdirectory(Chrome/Edge)或directory(Firefox)配合multiple实现,但Safari不支持;推荐新项目使用showDirectoryPicker(),需HTTPS和用户手势触发,可递归遍历并保留目录结构。

用 webkitdirectory 或 directory 实现文件夹选择
浏览器不支持直接“上传文件夹”,但可通过 input[type="file"] 的扩展属性触发文件夹选择。Chrome/Edge 用 webkitdirectory,Firefox 用 directory,两者都需配合 multiple:
<input type="file" id="folderInput" webkitdirectory directory multiple>
注意:Safari 不支持这两个属性,IE 已淘汰;用户选中文件夹后,e.target.files 返回的是扁平化的 FileList,每个 File 对象带 webkitRelativePath(如 "src/main.js"),这是还原目录结构的唯一依据。
容易踩的坑:
- 没加
multiple—— 即使加了webkitdirectory,部分旧版 Chrome 会只返回根目录下的文件,漏掉子目录 - 用
event.preventDefault()阻止默认行为后又没手动读取files—— 事件仍触发,但files.length === 0时容易误判为“用户取消” - 未过滤 macOS 的
__MACOSX或隐藏文件(如.DS_Store)—— 这些文件会被包含在FileList中,但通常不该上传
用 showDirectoryPicker() 获取完整目录句柄(推荐新项目)
showDirectoryPicker() 是更现代、可控性更强的方案,能递归遍历、保留层级、长期持有权限(需用户再次授权才能持久化)。但它有硬性前提:
立即学习“前端免费学习笔记(深入)”;
- 必须运行在 HTTPS 安全上下文下(
http://localhost也允许) - 必须由用户手势触发(比如点击按钮,不能 onload 自动调)
- 目前仅 Chrome 86+、Edge 91+ 原生支持,Firefox 和 Safari 尚未实现
基础用法:
const dirHandle = await window.showDirectoryPicker();<br>const files = [];<br>for await (const entry of dirHandle.values()) {<br> if (entry.kind === 'file') {<br> const file = await entry.getFile();<br> file.relativePath = entry.name; // 可自行拼接完整路径<br> files.push(file);<br> }<br>}
相比 webkitdirectory,它天然规避了相对路径解析错误、隐藏文件混入等问题,且可提前 abort 某个 entry 的读取 —— 这对大文件夹预检很关键。
上传时捕获网络异常:别只看 fetch().then()
用 FormData + fetch 上传文件夹时,常见错误不是“报错”,而是“静默失败”。根本原因是:fetch 默认不 reject 网络异常(如断网、DNS 失败),也不 reject HTTP 错误状态码(如 400、500)。
必须显式检查:
-
!res.ok—— 判断 4xx/5xx 状态码,再用await res.text()拿后端返回的错误详情 -
catch块 —— 捕获网络中断、CORS 拒绝、AbortError(用户取消)、TypeError(跨域或非安全上下文)等 - 不要依赖
console.error输出 —— 用户看不到控制台,得把错误映射成用户可理解的提示,比如“上传超时,请检查网络”或“服务器忙,请稍后再试”
示例关键逻辑:
try {<br> const res = await fetch('/upload', { method: 'POST', body: formData });<br> if (!res.ok) {<br> throw new Error(`HTTP ${res.status}: ${await res.text()}`);<br> }<br>} catch (err) {<br> if (err.name === 'AbortError') {<br> console.log('用户取消上传');<br> } else if (err instanceof TypeError && err.message.includes('failed to fetch')) {<br> console.log('网络不可达');<br> } else {<br> console.log('其他错误:', err.message);<br> }<br>}
后端收不到文件?先盯住这三处
前端看着一切正常,但后端 req.files 为空,大概率卡在这三个地方:
-
Content-Type被手动设置了 ——fetch用FormData时,必须让浏览器自动设置Content-Type: multipart/form-data; boundary=...,手动加 header 会导致整个 body 被后端解析器丢弃 - 字段名不匹配 —— 前端
formData.append("folder_files", file),后端中间件(如 Express 的multer)必须配置name为"folder_files",否则找不到 - Nginx 或云服务商限制了请求体大小 —— 文件夹上传往往含几十个文件,总大小易超默认的
client_max_body_size 1m,需调大并同步检查后端框架(如 Flask 的MAX_CONTENT_LENGTH)
真正难调试的点在于:这些错误不会在前端报红,而是在后端日志里静默吞掉请求。建议上传前用 console.log([...formData.entries()]) 确认数据已塞进 FormData,再抓包看发出的请求头和 body 是否符合 multipart 格式。



















