JS前端怎么批量读取本地文件夹内的所有文件
深入解析前端JavaScript批量读取本地文件夹内所有文件的三大主流技术方案:使用input的webkitdirectory属性、拖拽落区结合File and Directory Entries API递归解析,以及现代File System Access API的showDirectoryPicker应用,提供完整可运行代码、多级目录还原及大文件并发性能控制指南。
<input type="file" webkitdirectory> 文件选择器、支持用户直接把文件夹拖入页面的 File and Directory Entries API(利用 webkitGetAsEntry 递归遍历),以及针对现代桌面 Chromium 浏览器的 File System Access API(通过 window.showDirectoryPicker() 获取目录句柄)。本文将详细剖析这三种方案的具体代码实现、多级目录结构还原与读取海量文件时的性能处理方式。
一、浏览器安全边界与文件夹读取的核心机制
前端与服务端的最大区别在于运行环境受严格的沙箱(Sandbox)限制。在 Node.js 中,开发者可以调用 fs.readdir 并传入如 /Users/name/data 这样的绝对路径直接读取磁盘;但在浏览器中,任何尝试用纯代码探测本地路径的动作都会被系统安全策略拦截。这是为了防止恶意网页在用户不知情的情况下窃取本地个人数据或敏感文件。
因此,前端读取本地文件夹的核心原则是“用户显式授权”。无论是点击上传控件唤起操作系统目录选择窗口、将本地文件夹拖入网页指定的投放区域,还是在现代浏览器中点击按钮确认权限弹窗,所有的文件数据都必须在用户主动参与并确认后,由浏览器操作系统底层封装为虚拟句柄或只读集合传递给 JavaScript 运行上下文。
通过上图可以看到,当浏览器完成了用户的授权动作后,会根据调用的具体 Web API 将本地文件系统转化为不同的数据对象:FileList 列表、FileSystemEntry 树状节点,或者 FileSystemDirectoryHandle 异步句柄。下面我们按照实际开发中使用频率由高到低的顺序逐一展开实现。
二、方案一:使用 input 标签的 webkitdirectory 属性(基础且通用)
这是在实际生产项目中应用最普遍、各浏览器兼容性最成熟的方案。只需在标准 HTML 文件输入标签上增加非标准的 webkitdirectory 属性,浏览器在弹出选择窗口时就会自动切换为“文件夹选取”模式,允许用户选中整个文件夹。
1. HTML 声明与基本结构
在页面中声明如下输入控件。需要说明的是,虽然该属性源自早期的 WebKit 前缀,但目前所有现代桌面浏览器(包括 Chrome、Edge、Firefox 以及 Safari)均已原生支持该属性:
<!-- webkitdirectory 声明选取文件夹,multiple 保持多选兼容 -->
<input type="file" id="folderPicker" webkitdirectory multiple />
<div id="fileSummary"></div>
2. 监听文件选择并提取相对路径
当用户在系统对话框中选中某个文件夹并点击“确认”后,浏览器会启动底层扫描线程,自动递归遍历该文件夹下的所有子文件夹,并将所有扫描到的文件打包成一个平铺的 FileList 集合,绑定在 event.target.files 上。
每个 File 对象除了常规的 name、size、type、lastModified 等字段外,还额外附带了一个极其关键的只读属性:webkitRelativePath。该属性记录了文件相对于所选根文件夹的相对路径,例如 my-project/src/index.js。我们可以利用这个路径还原出本地完整的目录结构:
const folderPicker = document.getElementById('folderPicker');
folderPicker.addEventListener('change', async (event) => {
const fileList = event.target.files;
if (!fileList || fileList.length === 0) {
console.warn('未选择任何文件或文件夹为空');
return;
}
console.log(`成功读取到文件总数: ${fileList.length}`);
// 1. 遍历文件集合,查看文件信息及相对层级
const fileRecords = [];
for (const file of fileList) {
fileRecords.push({
name: file.name,
path: file.webkitRelativePath, // 包含多层子目录的相对路径
size: file.size,
type: file.type
});
}
// 2. 打印前 5 个文件路径作为示例
console.table(fileRecords.slice(0, 5));
});
3. 从相对路径还原多层级目录树结构
由于 event.target.files 返回的是一个拍平的一维数组,如果我们要在页面上呈现类似代码编辑器的多层级目录树,需要按 / 拆分 webkitRelativePath 并递归构建树节点:
function buildDirectoryTree(fileList) {
const root = { name: 'root', type: 'directory', children: {} };
for (const file of fileList) {
// 分割路径:例如 "project/assets/images/logo.png" -> ["project", "assets", "images", "logo.png"]
const segments = file.webkitRelativePath.split('/');
let currentDir = root;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const isFile = (i === segments.length - 1);
if (isFile) {
currentDir.children[segment] = {
type: 'file',
name: segment,
fullPath: file.webkitRelativePath,
fileObject: file
};
} else {
if (!currentDir.children[segment]) {
currentDir.children[segment] = {
type: 'directory',
name: segment,
children: {}
};
}
currentDir = currentDir.children[segment];
}
}
}
return root;
}
- 空文件夹无法被采集:
FileList只记录实际存在内容的文件节点。如果用户选择的本地目录中包含没有任何文件的空子目录,这些空目录在files集合中是完全缺失的,不会生成任何占位条目。 - 路径只读与无写回能力:该方案仅用于读取数据,无法将修改后的内容直接写回用户的原始本地文件。
三、方案二:实现文件夹拖拽批量读取(File and Directory Entries API)
在很多现代化应用(如网盘客户端、在线编译器、图片压缩工具)中,用户期望能够直接将桌面上的一个或多个文件夹拖入网页虚线区域完成上传。然而,若直接读取 event.dataTransfer.files,浏览器只会提供第一层文件的引用,拖入的文件夹不仅无法展开子目录,在很多浏览器中甚至会被忽略或报权限异常。
为了在拖拽场景下完整递归遍历文件夹,必须使用 File and Directory Entries API,通过 DataTransferItem.prototype.webkitGetAsEntry() 获取目录节点。
1. 拖拽区域监听与事件拦截
首先在网页中建立一个拖拽接收区域(Drop Zone),并阻止默认的浏览器文件打开行为:
<div id="dropZone" style="border: 2px dashed #6487A5; padding: 40px; text-align: center;">
将本地文件或文件夹拖拽至此区域
</div>
const dropZone = document.getElementById('dropZone');
// 必须阻止 dragover 和 dragenter 的默认事件,否则 drop 事件不会触发
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.style.backgroundColor = '#EEF4F8';
});
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
dropZone.style.backgroundColor = '#FFFFFF';
});
});
2. 核心陷阱:readEntries 的 100 条分页限制与完整递归代码
通过 entry.createReader() 创建目录读取器后,调用 reader.readEntries() 异步读取内部项。这里存在一个很多初学者都会遭遇的隐藏严重缺陷:WebKit / Chromium 内核出于性能防卡死考虑,单次调用 readEntries() 最多只返回 100 项条目。如果本地文件夹内有 350 个文件,如果只调用一次回调,剩下的 250 个文件将被完全丢失且不会报任何错误。
因此,针对每一个目录,必须使用循环迭代方式多次调用 readEntries(),直到其返回空数组(entries.length === 0),才代表本层目录真正读取完毕。以下是封装好的完整异步递归代码:
// 1. 完整读取单个目录下的所有项(循环分页拉取直到返回空数组)
function readEntriesBatch(dirReader) {
return new Promise((resolve, reject) => {
const allEntries = [];
function readNextBatch() {
dirReader.readEntries((entries) => {
if (!entries || entries.length === 0) {
// 全部批次拉取完毕
resolve(allEntries);
} else {
allEntries.push(...entries);
// 必须继续调用 readEntries 获取下一页数据!
readNextBatch();
}
}, (err) => reject(err));
}
readNextBatch();
});
}
// 2. 将 FileSystemFileEntry 转换为标准 File 对象
function getFileFromEntry(fileEntry) {
return new Promise((resolve, reject) => {
fileEntry.file(
(file) => resolve(file),
(err) => reject(err)
);
});
}
// 3. 递归遍历 FileSystemEntry
async function scanFileSystemEntry(entry) {
const results = [];
if (entry.isFile) {
const file = await getFileFromEntry(entry);
// entry.fullPath 保留了相对整棵树的路径,例如 "/rootFolder/sub/a.txt"
file.fullPath = entry.fullPath;
results.push(file);
} else if (entry.isDirectory) {
const dirReader = entry.createReader();
const subEntries = await readEntriesBatch(dirReader);
// 对该目录下的所有子条目并发执行递归解析
const subPromises = subEntries.map(subEntry => scanFileSystemEntry(subEntry));
const subResults = await Promise.all(subPromises);
for (const res of subResults) {
results.push(...res);
}
}
return results;
}
// 4. 绑定 drop 事件执行入口
dropZone.addEventListener('drop', async (e) => {
const items = e.dataTransfer.items;
if (!items || items.length === 0) return;
const parsePromises = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 使用 webkitGetAsEntry 获取 FileSystemEntry 对象
if (typeof item.webkitGetAsEntry === 'function') {
const entry = item.webkitGetAsEntry();
if (entry) {
parsePromises.push(scanFileSystemEntry(entry));
}
}
}
const groupedFiles = await Promise.all(parsePromises);
const flatFileList = groupedFiles.flat();
console.log('拖拽解析完成,提取到的总文件数:', flatFileList.length);
console.table(flatFileList.map(f => ({ name: f.name, fullPath: f.fullPath, size: f.size })));
});
四、方案三:现代 File System Access API(showDirectoryPicker)
随着 Web 应用越来越走向桌面级与专业化,W3C 与 Chromium 团队推出了全新的 File System Access API。它摒弃了传统输入框和复杂的回调模型,允许网页直接调用系统原生异步方法 window.showDirectoryPicker() 调出文件夹选择器,返回现代化的异步迭代句柄 FileSystemDirectoryHandle。
1. 触发与递归读取代码
async function pickAndReadDirectory() {
// 特性检测
if (!('showDirectoryPicker' in window)) {
alert('当前浏览器不支持 File System Access API,请使用 Chrome 或 Edge 访问。');
return;
}
try {
// 唤起原生文件夹选择窗口
const rootHandle = await window.showDirectoryPicker();
console.log('用户已授权访问文件夹:', rootHandle.name);
// 递归遍历函数
async function collectFilesRecursively(dirHandle, parentPath = '') {
const files = [];
const currentPath = parentPath ? `${parentPath}/${dirHandle.name}` : dirHandle.name;
// 使用异步迭代器遍历目录中的每个条目
for await (const [name, handle] of dirHandle.entries()) {
if (handle.kind === 'file') {
// 通过 handle.getFile() 获取原生 File 对象
const file = await handle.getFile();
file.relativePath = `${currentPath}/${name}`;
file.handle = handle; // 保存句柄供后续写回使用
files.push(file);
} else if (handle.kind === 'directory') {
// 遇到子目录递归进入
const subFiles = await collectFilesRecursively(handle, currentPath);
files.push(...subFiles);
}
}
return files;
}
const allFiles = await collectFilesRecursively(rootHandle);
console.log(`批量读取完成,共获取到 ${allFiles.length} 个文件`);
return allFiles;
} catch (err) {
// 用户取消选择会抛出 AbortError
if (err.name === 'AbortError') {
console.log('用户取消了文件夹选择');
} else {
console.error('读取文件夹失败:', err);
}
}
}
2. 该方案的独特优势:保存与原位写回
与前两种方案仅能以只读方式获取 File 对象不同,通过 showDirectoryPicker 获取到的 FileSystemFileHandle 还具备写回能力。如果用户在网页中修改了某个代码文件或配置文件,可以直接调用句柄将内容写回磁盘,无需触发下载流程:
async function saveBackToFile(fileHandle, newContent) {
// 请求可写流并直接覆写本地磁盘文件
const writableStream = await fileHandle.createWritable();
await writableStream.write(newContent);
await writableStream.close();
console.log('本地文件覆写成功!');
}
五、海量文件读取时的性能与内存控制
当选中的本地文件夹非常庞大,例如包含前端 node_modules、多层工程源码或数千张高清图片时,简单的全部并发读取极易引发网页性能灾难。必须在架构设计层面遵循以下三项优化准则:
1. 元数据先行,避免立即读取文件内容
无论是 FileList 还是 FileSystemHandle,在刚刚建立列表时,浏览器仅仅在内存中创建了轻量级的描述符指针,几乎不占用内存。千万不要在遍历文件夹时立即调用 file.text()、file.arrayBuffer() 或 FileReader。只有当用户在 UI 上点击某个文件需要预览,或进入实质性上传阶段时,再按需按量触发内容读取。
2. 并发限制(Concurrency Pool)控制
如果业务确实需要对所有文件内容进行预分析(例如全量计算文件 MD5 或进行代码静态检测),切勿使用 Promise.all(files.map(f => f.text()))。几千个并发文件 I/O 与字符串解码会导致浏览器内存迅速暴涨至数百兆乃至触发 OOM 崩溃。
推荐采用可控并发池模式,维持固定数量(如 6~10 个)的活跃 Promise:
async function readFilesWithConcurrency(files, concurrency = 6) {
const results = [];
let currentIndex = 0;
async function worker() {
while (currentIndex < files.length) {
const index = currentIndex++;
const file = files[index];
try {
// 按需读取当前文件内容(示例为读取文本)
const text = await file.text();
results[index] = { name: file.name, path: file.webkitRelativePath, length: text.length };
} catch (err) {
results[index] = { name: file.name, error: err.message };
}
}
}
// 启动固定数量的并发工作线程
const workers = Array.from({ length: Math.min(concurrency, files.length) }, () => worker());
await Promise.all(workers);
return results;
}
3. 及时撤销对象 URL(URL.revokeObjectURL)
如果读取的是图片文件夹并在页面上渲染缩略图,开发者常会使用 URL.createObjectURL(file) 生成 blob: 协议地址。需要牢记的是:这些 URL 指向的内存缓冲区在页面刷新前都不会被垃圾回收器回收。在图片加载完成或对应的 DOM 元素被移除时,务必调用 URL.revokeObjectURL(url) 释放内存。
六、方案对比与选型总结
综合以上三种方案的特性、API 成熟度与平台兼容情况,我们可以根据具体的业务诉求进行技术选型:
| 对比维度 | <input webkitdirectory> | Drag & Drop Entries API | File System Access API |
|---|---|---|---|
| 主要交互形式 | 点击按钮弹出原生文件选择器 | 用户直接拖拽文件夹至网页落区 | JS 函数直接调用,弹出系统授权窗口 |
| 浏览器兼容性 | 全平台极高(Chrome/Firefox/Safari/Edge) | 桌面端良好(Chrome/Edge/Firefox/Safari) | 主流 Chromium 浏览器(Chrome/Edge/Opera) |
| 相对路径获取 | 直接通过 file.webkitRelativePath 获取 |
通过 entry.fullPath 递归传递 |
在递归句柄树时自行拼接路径 |
| 分页截断风险 | 无(浏览器底层自动拍平打包) | 有(readEntries 单次限 100 条,须循环拉取) |
无(异步迭代器自动处理流) |
| 本地原位写回 | 不支持(只读) | 不支持(只读) | 支持(createWritable 原位修改) |
总结来说:
- 如果你的需求是通用的表单上传、资源导入工具,追求对各类浏览器最稳妥的兼容性,首选
<input type="file" webkitdirectory>,开发成本最低且表现最为一致。 - 如果页面重视交互体验,需要同时支持文件与文件夹的随意拖拽,应采用 File and Directory Entries API,并务必封装好
readEntries循环分页拉取逻辑以规避 100 个文件的截断漏洞。 - 如果开发的是桌面级 Web 应用(如在线 IDE、离线数据编辑器、工程配置管理系统),且核心目标环境为 Chromium 内核,推荐使用 File System Access API,它不仅结构清爽,还具备持久化句柄与原位写回的完整文件系统管理能力。
JavaScript将图片转换为Base64编码的两种途径
2026-11-05 16:43:05
怎么在 Vue 或 React 项目中正确集成 animate.js
2026-11-05 10:23:38
JS注册事件:addEventListener基础语法与参数详解
2026-11-05 08:44:56
JSP和JS的区别:运行环境与服务端/客户端职责解析
2026-11-04 23:34:36
pdf.js 官方下载地址:GitHub Release 获取预编译包
2026-11-04 23:07:27







