一、File System Access API 的优势
异步迭代:通过for await...of逐个处理文件,不阻塞主线程(传统input[type=file]的webkitdirectory会同步解析所有文件元数据,并在返回FileList前阻塞主线程)
按需加载:只在需要时才读取文件元数据,避免一次性加载全部
低内存占用:通过文件句柄(FileHandle)而非完整File对象存储,内存占用降低 90%+
递归可控:可随时中断扫描过程,支持取消操作
注意点:必须在https下才会生效
二、showDirectoryPicker的具体使用(在线预览:showDirectoryPicker使用示例)
<template> <div class="massive-folder-uploader"> <el-button type="primary" @click="selectFolder" :disabled="isProcessing" > <i class="el-icon-folder-opened"></i> 选择大型文件夹 </el-button> <!-- 进度展示 --> <div v-if="isProcessing" class="processing-status"> <el-progress :percentage="progress" stroke-width="3" ></el-progress> <div class="status-text"> 已发现 {{ fileCount }} 个文件 | 正在扫描: {{ currentPath }} </div> <el-button type="text" @click="abortScan" size="small" > 取消扫描 </el-button> </div> <!-- 结果展示 --> <div v-if="!isProcessing && fileCount > 0" class="result"> <el-alert title="扫描完成" type="success" :closable="false" > 共发现 {{ fileCount }} 个文件,可开始上传 </el-alert> <el-button type="success" @click="startUpload" style="margin-top: 10px;" > 开始上传 </el-button> </div> </div> </template> <script> export default { data() { return { isProcessing: false, progress: 0, fileCount: 0, currentPath: '', fileList: [], // 存储文件句柄 abortController: null, // 配置 supported: false, // 是否支持File System Access API maxFilesToScan: 20000, // 最大扫描文件数上限 fileTypes: ['image/jpeg', 'image/png', 'image/tiff'] // 只处理这些类型 }; }, mounted() { // 检测浏览器是否支持File System Access API this.supported = 'showDirectoryPicker' in window; if (!this.supported) { this.$alert('您的浏览器不支持大型文件夹处理,请使用Chrome 86+或Edge 86+浏览器', '浏览器不支持', { confirmButtonText: '确定' }); } }, methods: { // 选择文件夹(核心方法) async selectFolder() { if (!this.supported) return; try { // 重置状态 this.isProcessing = true; this.progress = 0; this.fileCount = 0; this.fileList = []; this.abortController = new AbortController(); // 请求用户选择文件夹 const directoryHandle = await window.showDirectoryPicker({ mode: 'read', signal: this.abortController.signal }); // 异步递归扫描文件夹(非阻塞) await this.scanDirectory(directoryHandle, ''); // 扫描完成 this.isProcessing = false; this.progress = 100; this.$message.success(`扫描完成,共发现 ${this.fileCount} 个文件`); } catch (err) { if (err.name !== 'AbortError') { console.error('扫描失败', err); this.$message.error(`扫描失败: ${err.message || '未知错误'}`); } this.isProcessing = false; } }, // 递归扫描文件夹(异步非阻塞) async scanDirectory(directoryHandle, parentPath) { const entries = []; // 异步迭代文件夹内容(关键:不会一次性加载所有内容) for await (const entry of directoryHandle.values()) { // 检查是否需要终止 if (this.abortController.signal.aborted) { throw new Error('扫描已取消'); } const currentPath = parentPath ? `${parentPath}/${entry.name}` : entry.name; this.currentPath = currentPath; if (entry.kind === 'file') { // 处理文件 const file = await entry.getFile(); // 过滤文件类型 if (this.fileTypes.includes(file.type)) { this.fileList.push({ name: entry.name, path: currentPath, size: file.size, type: file.type, handle: entry, // 保留文件句柄,后续可直接读取 file // 原始文件对象(可选,按需加载) }); this.fileCount++; // 更新进度(简单估算) this.progress = Math.min(95, Math.round((this.fileCount / this.maxFilesToScan) * 100)); // 达到上限停止扫描 if (this.fileCount >= this.maxFilesToScan) { this.$message.warning(`已达到最大扫描上限 ${this.maxFilesToScan} 个文件`); return; } } } else if (entry.kind === 'directory') { // 递归扫描子文件夹(使用setTimeout避免栈溢出) await new Promise(resolve => setTimeout(resolve, 0)); await this.scanDirectory(entry, currentPath); } // 每处理100个文件释放一次主线程 if (this.fileCount % 100 === 0) { await new Promise(resolve => requestIdleCallback(resolve)); } } }, // 取消扫描 abortScan() { if (this.abortController) { this.abortController.abort(); this.isProcessing = false; this.$message.info('已取消扫描'); } }, // 开始上传(使用之前实现的并发上传逻辑) startUpload() { // 这里可以调用之前优化的并行上传方法 // 注意:对于10000+文件,建议分批次上传(如每批500个) this.$emit('files-ready', this.fileList); } } }; </script> <style scoped> .processing-status { margin-top: 15px; padding: 10px; border: 1px solid #e4e7ed; border-radius: 4px; } .status-text { margin: 10px 0; font-size: 14px; color: #606266; } .result { margin-top: 15px; } </style>