前端获取本地文件目录内容

前端获取本地文件目录内容

一、核心原理说明

由于浏览器的 "沙箱安全机制",前端 JavaScript 无法直接访问本地文件系统,必须通过用户主动授权(如选择目录操作)才能获取文件目录内容。目前主流实现方案基于两种 API:传统 File API(兼容性优先)和现代 FileSystem Access API(功能优先),以下将详细介绍两种方案的实现流程、代码示例及适用场景。

二、方案一:基于 File API 实现(兼容性首选)

1. 方案概述

通过隐藏的 <input type="file"> 标签(配置 webkitdirectorydirectory 属性)触发用户选择目录操作,用户选择后通过 files 属性获取目录下所有文件的元数据(如文件名、大小、相对路径等)。该方案兼容几乎所有现代浏览器(包括 Chrome、Firefox、Safari 等),但仅支持 "一次性获取选中目录内容",无法递归遍历子目录或修改文件。

2. 完整使用示例

2.1 HTML 结构(含 UI 交互区)
xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>File API 目录访问示例</title>
    <!-- 引入 Tailwind 简化样式(也可自定义 CSS) -->
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        .file-item { display: flex; align-items: center; padding: 8px; border-bottom: 1px solid #eee; }
        .file-icon { margin-right: 8px; font-size: 18px; }
        .file-info { flex: 1; }
        .file-size { color: #666; font-size: 14px; }
    </style>
</head>
<body class="p-8 bg-gray-50">
    <div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow">
        <h2 class="text-2xl font-bold mb-4">File API 目录内容获取</h2>
        <!-- 触发按钮(隐藏原生 input) -->
        <button id="selectDirBtn" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
            选择本地目录
        </button>
        <input type="file" id="dirInput" webkitdirectory directory style="display: none;">
        
        <!-- 文件列表展示区 -->
        <div class="mt-4 border rounded-lg max-h-80 overflow-y-auto">
            <div id="fileList" class="p-4 text-center text-gray-500">
                请选择目录以查看文件列表
            </div>
        </div>
    </div>

    <script>
        // 2.2 JavaScript 逻辑实现
        const dirInput = document.getElementById('dirInput');
        const selectDirBtn = document.getElementById('selectDirBtn');
        const fileList = document.getElementById('fileList');

        // 1. 点击按钮触发原生 input 选择目录
        selectDirBtn.addEventListener('click', () => {
            dirInput.click();
        });

        // 2. 监听目录选择变化,处理文件数据
        dirInput.addEventListener('change', (e) => {
            const selectedFiles = e.target.files; // 获取选中目录下的所有文件(含子目录文件)
            if (selectedFiles.length === 0) {
                fileList.innerHTML = '<div class="p-4 text-center text-gray-500">未选择任何文件</div>';
                return;
            }
            
            // 3. 解析文件数据并渲染到页面
            let fileHtml = '';
            Array.from(selectedFiles).forEach(file => {
                // 判断是否为目录(通过 type 为空且 size 为 0 间接判断)
                const isDir = file.type === '' && file.size === 0;
                // 获取文件在目录中的相对路径(webkitRelativePath 为非标准属性,但主流浏览器支持)
                const relativePath = file.webkitRelativePath || file.name;
                // 格式化文件大小(辅助函数)
                const fileSize = isDir ? '---' : formatFileSize(file.size);

                fileHtml += `
                    <div class="file-item">
                        <span class="file-icon ${isDir ? 'text-yellow-500' : 'text-gray-400'}">
                            ${isDir ? '📁' : '📄'}
                        </span>
                        <div class="file-info">
                            <div class="font-medium">${file.name}</div>
                            <div class="text-xs text-gray-500">${relativePath}</div>
                        </div>
                        <div class="file-size text-sm">${fileSize}</div>
                    </div>
                `;
            });

            fileList.innerHTML = fileHtml;
        });

        // 辅助函数:格式化文件大小(Bytes → KB/MB/GB)
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const units = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`;
        }
    </script>
</body>
</html>

3. 关键特性与限制

  • 优势:兼容性强(支持 Chrome 15+、Firefox 4+、Safari 6+),无需额外依赖,实现简单。

  • 限制

  1. 无法直接识别 "目录" 类型,需通过 typesize 间接判断;

  2. 仅能获取选中目录下的 "扁平化文件列表",无法递归获取子目录结构;

  3. 无文件读写能力,仅能获取元数据。

三、方案二:基于 FileSystem Access API 实现(功能优先)

1. 方案概述

FileSystem Access API 是 W3C 正在标准化的现代 API(目前主要支持 Chromium 内核浏览器,如 Chrome 86+、Edge 86+),提供 "目录选择、递归遍历、文件读写、持久化权限" 等更强大的能力。通过 window.showDirectoryPicker() 直接请求用户授权,授权后可主动遍历目录结构,支持复杂的文件操作。

2. 完整使用示例

2.1 HTML 结构(含子目录遍历功能)
xml 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>FileSystem Access API 目录访问示例</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        .dir-tree-item { padding: 4px 0 4px 16px; border-left: 1px solid #eee; }
        .dir-header { display: flex; align-items: center; cursor: pointer; padding: 4px 0; }
        .dir-icon { margin-right: 8px; }
        .file-meta { color: #666; font-size: 14px; margin-left: 8px; }
    </style>
</head>
<body class="p-8 bg-gray-50">
    <div class="max-w-4xl mx-auto bg-white p-6 rounded-lg shadow">
        <h2 class="text-2xl font-bold mb-4">FileSystem Access API 目录遍历</h2>
        <!-- 触发目录选择按钮 -->
        <button id="openDirBtn" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
            打开并遍历目录
        </button>
        
        <!-- 目录树展示区 -->
        <div class="mt-4 border rounded-lg p-4 max-h-80 overflow-y-auto">
            <div id="dirTree" class="text-gray-500">
                请点击按钮选择目录
            </div>
        </div>
    </div>

    <script>
        // 2.2 JavaScript 逻辑实现(含递归遍历)
        const openDirBtn = document.getElementById('openDirBtn');
        const dirTree = document.getElementById('dirTree');

        openDirBtn.addEventListener('click', async () => {
            try {
                // 1. 检查浏览器兼容性
                if (!window.showDirectoryPicker) {
                    alert('您的浏览器不支持该功能,请使用 Chrome 或 Edge 浏览器');
                    return;
                }

                // 2. 请求用户选择目录(获取 DirectoryHandle 对象)
                const dirHandle = await window.showDirectoryPicker({
                    mode: 'read', // 权限模式:read(只读)/ readwrite(读写)
                    startIn: 'documents' // 默认打开目录(可选:documents、downloads 等)
                });

                // 3. 递归遍历目录结构并渲染
                dirTree.innerHTML = '<div class="text-center text-gray-500">正在读取目录...</div>';
                const treeHtml = await renderDirectoryTree(dirHandle, 0);
                dirTree.innerHTML = treeHtml;

            } catch (err) {
                // 捕获用户取消选择或权限拒绝错误
                if (err.name === 'AbortError') {
                    dirTree.innerHTML = '<div class="text-center text-gray-500">用户取消选择</div>';
                } else {
                    dirTree.innerHTML = `<div class="text-center text-red-500">错误:${err.message}</div>`;
                    console.error('目录访问失败:', err);
                }
            }
        });

        /**
         * 递归渲染目录树
         * @param {DirectoryHandle} handle - 目录/文件句柄
         * @param {number} depth - 目录深度(用于缩进)
         * @returns {string} 目录树 HTML
         */
        async function renderDirectoryTree(handle, depth) {
            const isDir = handle.kind === 'directory';
            const indent = 'margin-left: ' + (depth * 16) + 'px;'; // 按深度缩进
            let itemHtml = '';

            if (isDir) {
                // 处理目录:添加展开/折叠功能
                itemHtml += `
                    <div class="dir-header" style="${indent}" onclick="toggleDir(this)">
                        <span class="dir-icon text-yellow-500">📁</span>
                        <span class="font-medium">${handle.name}</span>
                        <span class="file-meta">(目录)</span>
                    </div>
                    <div class="dir-children" style="display: none;">
                `;

                // 遍历目录下的所有子项(递归)
                for await (const childHandle of handle.values()) {
                    itemHtml += await renderDirectoryTree(childHandle, depth + 1);
                }

                itemHtml += '</div>'; // 闭合 dir-children

            } else {
                // 处理文件:获取文件大小等元数据
                const file = await handle.getFile();
                const fileSize = formatFileSize(file.size);
                itemHtml += `
                    <div style="${indent} display: flex; align-items: center; padding: 4px 0;">
                        <span class="dir-icon text-gray-400">📄</span>
                        <span>${handle.name}</span>
                        <span class="file-meta">${fileSize}</span>
                    </div>
                `;
            }

            return itemHtml;
        }

        // 目录展开/折叠切换(全局函数,用于 HTML 内联调用)
        function toggleDir(el) {
            const children = el.nextElementSibling;
            children.style.display = children.style.display === 'none' ? 'block' : 'none';
            el.querySelector('.dir-icon').textContent = children.style.display === 'none' ? '📁' : '📂';
        }

        // 复用文件大小格式化函数(同方案一)
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const units = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return `${(bytes / Math.pow(k, i)).toFixed(2)} ${units[i]}`;
        }
    </script>
</body>
</html>

3. 关键特性与限制

  • 优势
  1. 直接识别 "目录 / 文件" 类型(通过 handle.kind);

  2. 支持递归遍历目录结构,可实现 "目录树" 交互;

  3. 提供文件读写能力(通过 fileHandle.createWritable());

  4. 可请求持久化权限(handle.requestPermission()),下次访问无需重新授权。

  • 限制:兼容性差,仅支持 Chromium 内核浏览器,Firefox 和 Safari 暂不支持。

四、两种方案对比分析

对比维度 方案一(File API) 方案二(FileSystem Access API)
浏览器兼容性 强(支持所有现代浏览器) 弱(仅 Chromium 内核浏览器)
目录识别能力 间接判断(依赖 type 和 size) 直接识别(handle.kind)
目录遍历能力 仅扁平化列表,无递归支持 支持递归遍历,可构建目录树
文件操作能力 仅读取元数据,无读写能力 支持文件读写、删除等完整操作
权限持久化 不支持(每次刷新需重新选择) 支持(可请求持久化权限)
交互体验 依赖隐藏 input,体验较基础 原生 API 调用,体验更流畅
适用场景 兼容性优先的简单目录查看需求 现代浏览器下的复杂文件管理需求

五、注意事项与最佳实践

  1. 安全合规:无论哪种方案,都必须通过 "用户主动操作" 触发授权(如点击按钮),禁止自动触发目录选择,否则浏览器会拦截操作。

  2. 错误处理:需捕获 "用户取消选择"(AbortError)和 "权限拒绝"(PermissionDeniedError)等错误,避免页面展示异常。

  3. 兼容性适配:可通过 "特性检测" 实现方案降级,例如:

javascript 复制代码
if (window.showDirectoryPicker) {
    // 使用方案二(FileSystem Access API)
} else {
    // 使用方案一(File API)
}
  1. 性能优化:遍历大量文件时(如超过 1000 个文件),建议使用 "分页加载" 或 "虚拟滚动",避免一次性渲染导致页面卡顿。

  2. 隐私保护:不建议存储用户本地文件路径等敏感信息,仅在前端临时处理文件数据,避免隐私泄露风险。

相关推荐
晴空雨4 分钟前
💥 React 容器组件深度解析:从 Props 拦截到事件改写
前端·react.js·设计模式
Marshall35728 分钟前
前端水印防篡改原理及实现
前端
阿虎儿20 分钟前
TypeScript 内置工具类型完全指南
前端·javascript·typescript
IT_陈寒29 分钟前
Java性能优化实战:5个立竿见影的技巧让你的应用提速50%
前端·人工智能·后端
chxii1 小时前
6.3Element UI 的表单
javascript·vue.js·elementui
张努力1 小时前
从零开始的开发一个vite插件:一个程序员的"意外"之旅 🚀
前端·vue.js
远帆L1 小时前
前端批量导入内容——word模板方案实现
前端
Codebee1 小时前
OneCode3.0-RAD 可视化设计器 配置手册
前端·低代码
深兰科技1 小时前
深兰科技:搬迁公告,我们搬家了
javascript·人工智能·python·科技·typescript·laravel·深兰科技