在前端开发中,文件上传是一个常见需求。随着浏览器 API 的不断进化,现在我们可以实现更灵活的文件交互方式。本文将总结出一个支持文件选择、文件夹选择、拖拽上传、粘贴上传的完整示例,涵盖现代文件操作的主流场景。
一、功能特性
1. 文件/文件夹选择方式
传统文件选择:通过按钮触发input[type=”file”]和multiple,支持多选文件
文件夹选择:使用webkitdirectory属性选择整个文件夹(需 Chrome 等 webkit 内核浏览器)
2. 拖拽上传
可拖拽文件 / 文件夹到指定区域
拖拽状态可视化(背景色变化)
支持跨窗口拖拽(如从文件资源管理器拖入浏览器)
3. 粘贴上传
支持Ctrl+V粘贴系统剪贴板中的文件(需浏览器支持 Clipboard API)
可粘贴单个文件或整个文件夹
4. 文件管理
实时显示已选文件列表
支持文件大小格式化(自动转换为 B/KB/MB 等单位)
点击文件条目直接下载
控制台输出文件数组
二、效果演示
三、完整代码实现
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>文件选择/拖拽/粘贴示例</title> <style> body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } .area { width: 100%; height: 200px; border: 2px dashed #ccc; border-radius: 8px; display: flex; align-items: center; justify-content: center; background-color: #f9f9f9; transition: background-color 0.3s; cursor: pointer; } .area:hover { background-color: #f0f0f0; } .area.dragover { background-color: #e0e0e0; border-color: #007BFF; } .file-list { margin-top: 20px; } .file-item { padding: 8px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; cursor: pointer; } .file-item:hover { background-color: #f8f9fa; } .file-item:last-child { border-bottom: none; } .file-name { max-width: 600px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .file-size { color: #666; font-size: 0.9em; margin-left: 10px; } .button-group { margin-bottom: 15px; } button { padding: 8px 15px; margin-right: 10px; border: none; border-radius: 4px; background-color: #007BFF; color: white; cursor: pointer; } button:hover { background-color: #0056b3; } </style> </head> <body> <h3>文件选择/拖拽/粘贴示例</h3> <div class="button-group"> <button id="selectFilesBtn">选择文件</button> <button id="selectFolderBtn">选择文件夹</button> </div> <input type="file" name="fileInput" id="fileInput" multiple style="display: none;"> <input type="file" name="folderInput" id="folderInput" webkitdirectory directory multiple style="display: none;"> <div class="area">拖放文件或文件夹到这里(支持使用ctrl+v粘贴所复制的文件)</div> <div class="file-list"></div> <script> const doms = { fileInput: document.getElementById('fileInput'), folderInput: document.getElementById('folderInput'), selectFilesBtn: document.getElementById('selectFilesBtn'), selectFolderBtn: document.getElementById('selectFolderBtn'), area: document.querySelector('.area'), fileList: document.querySelector('.file-list') } // 存储当前文件列表 let currentFiles = []; // 显示文件列表 function displayFiles() { console.log('获取到的文件:', currentFiles); doms.fileList.innerHTML = ''; currentFiles.forEach((file, index) => { const item = document.createElement('div'); item.className = 'file-item'; item.innerHTML = ` <span class="file-name">${file.name}</span> <span class="file-size">${formatFileSize(file.size)}</span> `; item.setAttribute('data-index', index); item.addEventListener('click', handleFileClick); doms.fileList.appendChild(item); }); } // 处理文件点击事件 function handleFileClick(e) { const index = parseInt(e.currentTarget.getAttribute('data-index')); const file = currentFiles[index]; if (file) { const url = URL.createObjectURL(file); const a = document.createElement('a'); a.href = url; a.download = file.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); // 释放URL对象以避免内存泄漏 setTimeout(() => { URL.revokeObjectURL(url); }, 100); } } // 格式化文件大小 function formatFileSize(bytes) { if (bytes < 1024) { return parseFloat(bytes).toFixed(2) + "B"; } else if (bytes < 1024 * 1024) { return (bytes / 1024).toFixed(2) + "KB"; } else if (bytes < 1024 * 1024 * 1024) { return (bytes / (1024 * 1024)).toFixed(2) + "MB"; } else if (bytes < 1024 * 1024 * 1024 * 1024) { return (bytes / (1024 * 1024 * 1024)).toFixed(2) + "GB"; } else if (bytes < 1024 * 1024 * 1024 * 1024 * 1024) { return (bytes / (1024 * 1024 * 1024 * 1024)).toFixed(2) + "TB"; } else { return (bytes / (1024 * 1024 * 1024 * 1024 * 1024)).toFixed(2) + "PB"; } } // 递归读取文件夹并扁平化文件列表 function readFileList(entry) { return new Promise((resolve, reject) => { if (entry.isFile) { entry.file(file => resolve(file)); } else if (entry.isDirectory) { const reader = entry.createReader(); reader.readEntries(entries => { const promises = entries.map(childEntry => readFileList(childEntry)); Promise.all(promises).then(files => resolve(files.flat())); }); } else { resolve([]); } }); } // 处理文件入口 async function processEntries(items) { const promises = items.map(item => { // 处理拖拽/粘贴的文件系统条目 const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null; if (entry) return readFileList(entry); return []; }); currentFiles = (await Promise.all(promises)).flat(); displayFiles(); } // 文件选择按钮点击事件 doms.selectFilesBtn.addEventListener('click', () => { doms.fileInput.click(); }); // 文件夹选择按钮点击事件 doms.selectFolderBtn.addEventListener('click', () => { doms.folderInput.click(); }); // 文件选择事件 doms.fileInput.addEventListener('change', (e) => { currentFiles = Array.from(e.target.files); displayFiles(); }); // 文件夹选择事件 doms.folderInput.addEventListener('change', async (e) => { currentFiles = Array.from(e.target.files); displayFiles(); }); // 拖拽事件 doms.area.addEventListener('dragenter', (e) => { e.preventDefault(); doms.area.classList.add('dragover'); }); doms.area.addEventListener('dragover', (e) => { e.preventDefault(); doms.area.classList.add('dragover'); }); doms.area.addEventListener('dragleave', (e) => { e.preventDefault(); doms.area.classList.remove('dragover'); }); doms.area.addEventListener('drop', async (e) => { e.preventDefault(); doms.area.classList.remove('dragover'); if (e.dataTransfer.items) { processEntries(Array.from(e.dataTransfer.items)); } }); // 粘贴事件 document.addEventListener('paste', async (e) => { if (e.clipboardData.items) { processEntries(Array.from(e.clipboardData.items)); } }); </script> </body> </html>
四、总结
方式 | 触发方式 | 技术实现 | 兼容性 |
---|---|---|---|
文件选择 | 点击 “选择文件” 按钮 | input[type=”file”] + multiple属性 | 基础功能 |
文件夹选择 | 点击 “选择文件夹” 按钮 | webkitdirectory属性 | webkit引擎 |
拖拽上传 | 拖放文件到虚线区域 | 拖拽事件(drop)+ 递归解析 | 标准 File API 支持 |
粘贴上传 | 复制文件后按Ctrl+V | 粘贴事件(paste) + 剪贴板数据读取 + 递归解析 | 需浏览器支持 Clipboard API |