在构建企业级 Web 应用时,文件管理器是一个看似简单实则充满挑战的模块。面对大文件上传卡顿、大文件下载导致浏览器崩溃、以及误删不可恢复等痛点,我们需要一套更科学的架构方案。
本文将通过 Vue 2/3 + Spring Boot 的组合,详细拆解如何实现一套具备:排重检测、多线程后台下载、流式下载进度监控、以及回收站机制的文件管理系统。
一、 核心技术原理分析
1. 智能冲突检测上传
在上传文件前,系统会先发起一个“预检请求”(Check Exists)。
- 原理:前端获取文件名,调用后端接口查询路径下是否已有同名文件。
- 交互:若存在冲突,弹出对话框让用户选择“覆盖”或“跳过”,避免误操作。
- 进度监控:利用 Axios 的 onUploadProgress 事件,实时获取已上传字节数,驱动前端 UI 进度条。
2. 动态下载策略(三重机制)
这是本系统的核心亮点,根据文件大小自动切换下载模式:
- 策略 A:Web Worker (中小文件 < 100MB)
- 原理:在主线程之外开启独立线程,通过 XMLHttpRequest 进行下载。
- 优点:完全不阻塞主线程 UI 渲染,进度回传极其精准。
- 限制:需将整个文件读取为 Blob 存入内存,超大文件(如 4GB)会导致浏览器 Tab 页崩溃。
- 策略 B:Service Worker + Streams API (大文件 100MB ~ 2GB)
- 原理:Service Worker 充当浏览器与网络之间的代理。它拦截请求并使用 ReadableStream 边读取后端流边刷入磁盘,通过 postMessage 向主页面同步进度。
- 优点:无需将全量文件塞进内存,支持 GB 级文件的进度条展示。
- 策略 C:传统 <a> 标签 (兜底方案 > 2GB)
- 原理:通过创建隐藏链接触发浏览器自带的下载管理器。
- 优点:最为稳定,由浏览器底层硬扛,不占用前端 JavaScript 内存。
3. 安全删除与回收站机制
- 非物理删除:文件删除时,系统将其移动到隐藏的 .recycle_bin 目录。
- 元数据编码:为了支持“还原”,删除时会将原文件路径进行 Base64 编码并拼接时间戳作为新文件名,确保即使不同路径的同名文件在回收站也能共存。
二、 前端实现步骤与代码
1. 基础设施:Worker 脚本
将这两个脚本放在项目的 public 目录下。
public/downloadWorker.js (Web Worker)
1self.onmessage = function (e) { 2 const { url, filename } = e.data; 3 const xhr = new XMLHttpRequest(); 4 xhr.open('GET', url, true); 5 xhr.responseType = 'blob'; 6 7 xhr.onprogress = (event) => { 8 if (event.lengthComputable) { 9 const percent = Math.floor((event.loaded / event.total) * 100); 10 self.postMessage({ type: 'progress', percent }); 11 } 12 }; 13 14 xhr.onload = function () { 15 if (this.status === 200) { 16 self.postMessage({ type: 'success', blob: this.response, filename }); 17 } else { 18 self.postMessage({ type: 'error', error: '下载失败: ' + this.status }); 19 } 20 }; 21 xhr.send(); 22};
public/service-worker.js (Service Worker)
1self.addEventListener('fetch', (event) => { 2 const url = new URL(event.request.url); 3 if (url.searchParams.has('sw_download')) { 4 event.respondWith(handleDownloadStream(event)); 5 } 6}); 7 8async function handleDownloadStream(event) { 9 const targetUrl = event.request.url.replace(/[?&]sw_download=true/, ''); 10 const response = await fetch(targetUrl); 11 const total = parseInt(response.headers.get('content-length'), 10); 12 let loaded = 0; 13 14 const stream = new ReadableStream({ 15 async start(controller) { 16 const reader = response.body.getReader(); 17 while (true) { 18 const { done, value } = await reader.read(); 19 if (done) break; 20 loaded += value.length; 21 self.clients.matchAll().then(clients => { 22 clients.forEach(c => c.postMessage({ type: 'sw_progress', percent: Math.floor((loaded/total)*100) })); 23 }); 24 controller.enqueue(value); 25 } 26 controller.close(); 27 } 28 }); 29 return new Response(stream, { headers: response.headers }); 30}
2. Vue 组件逻辑核心 (FileManager.vue)
这里展示核心的下载判定逻辑和上传排重逻辑。
1// 下载策略选择器 2downloadFile(row) { 3 const filePath = this.currentPath === '/' ? `/${row.name}` : `${this.currentPath}/${row.name}`; 4 const url = `${BASE_URL}/download?path=${encodeURIComponent(filePath)}`; 5 const fileSize = row.byteSize; // 假设后端返回了字节大小 6 7 if (fileSize < 50 * 1024 * 1024) { 8 // <50MB: Web Worker 模式 9 this.downloadByWebWorker(url, row.name); 10 } else if (fileSize < 1024 * 1024 * 1024 && this.swRegistered) { 11 // 50MB~1GB: Service Worker 模式 12 this.downloadByServiceWorker(url, row.name); 13 } else { 14 // >1GB: 浏览器原生模式 15 this.ordinaryDownload(url, row.name); 16 } 17}, 18 19// 上传排重逻辑 20async customUploadRequest(options) { 21 const file = options.file; 22 // 1. 预检 23 const check = await axios.get(`${BASE_URL}/checkExists`, { params: { path: this.currentPath, filename: file.name } }); 24 let overwrite = false; 25 if (check.data.data) { 26 await this.$confirm('文件已存在,是否覆盖?', '提示').then(() => overwrite = true).catch(() => { throw 'cancel' }); 27 } 28 // 2. 执行上传 29 const fd = new FormData(); 30 fd.append('file', file); 31 fd.append('overwrite', overwrite); 32 await axios.post(`${BASE_URL}/upload`, fd, { 33 onUploadProgress: (p) => { this.progressPercent = Math.round((p.loaded * 100) / p.total); } 34 }); 35}
三、 后端实现步骤与代码
1. 文件存储架构
后端基于 Spring Boot,关键点在于正确设置 HTTP 响应头,以配合前端的流式读取。
2. 核心 Service 实现 (FileService.java)
1@Service 2public class FileService { 3 @Value("${file.upload-path}") 4 private String rootPath; 5 6 private final String RECYCLE_BIN = ".recycle_bin"; 7 8 // 逻辑删除:移动到回收站 9 public void deleteFile(String relativePath, boolean permanent) throws IOException { 10 Path source = Paths.get(rootPath, relativePath); 11 if (permanent) { 12 FileSystemUtils.deleteRecursively(source); 13 } else { 14 // 生成回收站文件名:时间戳_原路径Base64_文件名 15 String encodedPath = Base64.getEncoder().encodeToString(relativePath.getBytes()); 16 String recycleName = System.currentTimeMillis() + "_" + encodedPath + "_" + source.getFileName(); 17 Path dest = Paths.get(rootPath, RECYCLE_BIN, recycleName); 18 Files.move(source, dest, StandardCopyOption.REPLACE_EXISTING); 19 } 20 } 21 22 // 下载 Resource 加载 23 public Resource loadFileAsResource(String relativePath) throws Exception { 24 Path filePath = Paths.get(rootPath, relativePath).normalize(); 25 Resource resource = new UrlResource(filePath.toUri()); 26 if (resource.exists()) return resource; 27 throw new FileNotFoundException("文件不存在"); 28 } 29}
3. 控制器流式响应 (FileController.java)
1@GetMapping("/download") 2public ResponseEntity<Resource> downloadFile(@RequestParam String path, HttpServletRequest request) { 3 try { 4 Resource resource = fileService.loadFileAsResource(path); 5 long length = resource.getFile().length(); 6 7 return ResponseEntity.ok() 8 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"") 9 .header(HttpHeaders.CONTENT_LENGTH, String.valueOf(length)) // 必须返回长度,否则前端没进度条 10 .contentType(MediaType.APPLICATION_OCTET_STREAM) 11 .body(resource); 12 } catch (Exception e) { 13 return ResponseEntity.notFound().build(); 14 } 15}
四、 总结与建议
1. 方案优势
- 不卡顿:多线程 Worker 下载确保用户在下载 GB 级大文件时,页面依然能顺滑点击。
- 内存友好:Service Worker 方案解决了大文件直接读入内存导致浏览器崩溃的陈年旧疾。
- 容错性:回收站机制为误删提供了最后一层保险。
2. 部署注意事项
- HTTPS 是必须的:由于 Service Worker 安全限制,除了 localhost,必须在 HTTPS 环境下才能生效。
- 跨域头 (CORS):如果前后端分离,后端必须暴露 Content-Length 和 Content-Disposition 响应头,否则前端无法获取文件大小和正确的文件名。
- Nginx 配置:若通过 Nginx 转发,需确保 proxy_max_temp_file_size 设置足够大,或关闭代理缓冲以支持流式传输。
这套方案不仅是一个简单的文件管理器,它展示了现代 Web API(Workers, Streams, Service Workers)在处理密集型 I/O 任务时的巨大潜力。
《打造工业级全栈文件管理器:深度解析上传、回收站与三重下载流控技术》 是转载文章,点击查看原文。
