首页 > 建站教程 > JS、jQ、TS >  js 原生文件系统 API(File System Access API):showDirectoryPicker方法的使用正文

js 原生文件系统 API(File System Access API):showDirectoryPicker方法的使用

一、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>