一个大三学生的文件管理系统血泪史(前端篇)

各位看官,我是浙江某高校网络工程专业的大三学生,最近在搞一个"史诗级"项目——文件管理系统。为啥说是史诗级?因为光是需求就快把我整秃噜皮了!

项目背景

我们导师说:“小张啊,现在找工作得有作品,你做个文件管理系统吧,要支持10G大文件上传、加密传输、断点续传、文件夹层级保留,还要兼容IE8!”

我:“老师,您这是要让我重写百度网盘吗?”

导师:“不,我要你超越百度网盘!”

我:“…”

技术选型

经过一番挣扎,我选择了:

  • 前端:Vue3 + 原生JS(因为导师说不能用jQuery)
  • 上传组件:WebUploader(兼容性好) + H5(现代浏览器用)
  • 开发工具:VSCode(因为Sublime Text要钱)
  • 数据库:MySQL(因为导师说这个最简单)
  • 服务器:本地Windows 7 + IE9(学校老机器的配置)

前端实现(血泪版)

1. 兼容性处理

首先得解决IE8兼容问题,我写了这么个神器:

// 检测浏览器并给出友好提示
function detectBrowser() {
    const userAgent = navigator.userAgent;
    if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident') > -1) {
        const version = userAgent.match(/(MSIE |rv:)(\d+)/)[2];
        if (version < 9) {
            alert('检测到您使用的是IE' + version + ',本系统需要IE9+或其他现代浏览器!');
            // 偷偷给IE用户显示个可爱图片
            document.body.innerHTML = `
                
                    亲爱的IE用户
                    
                    请使用Chrome/Firefox/Edge/Safari等现代浏览器
                
            `;
        }
    }
}
detectBrowser();

2. 文件上传组件封装

class FileUploader {
    constructor(options) {
        this.chunkSize = options.chunkSize || 5 * 1024 * 1024; // 默认5MB分片
        this.fileInput = document.getElementById(options.inputId);
        this.uploadBtn = document.getElementById(options.uploadBtnId);
        this.progressBar = document.getElementById(options.progressId);
        this.fileList = [];
        this.initEvents();
    }

    initEvents() {
        this.fileInput.addEventListener('change', (e) => this.handleFileSelect(e));
        this.uploadBtn.addEventListener('click', () => this.startUpload());
    }

    handleFileSelect(e) {
        const files = Array.from(e.target.files);
        this.fileList = files.map(file => ({
            file,
            chunks: [],
            uploadedChunks: 0,
            totalChunks: Math.ceil(file.size / this.chunkSize),
            fileId: this.generateFileId()
        }));
        
        // 显示文件列表(简化版)
        console.log('已选择文件:', this.fileList.map(f => f.file.name));
    }

    generateFileId() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0;
            return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
        });
    }

    // 加密函数(简化版)
    encryptData(data) {
        // 实际项目中应该用AES等加密算法
        // 这里只是演示,返回Base64编码
        return btoa(unescape(encodeURIComponent(data)));
    }

    // 分片上传
    async uploadChunk(fileObj, chunkIndex) {
        const file = fileObj.file;
        const start = chunkIndex * this.chunkSize;
        const end = Math.min(file.size, start + this.chunkSize);
        const chunk = file.slice(start, end);
        
        // 加密分片数据
        const encryptedChunk = this.encryptData(chunk);
        
        // 这里应该用XMLHttpRequest或Fetch API发送到后端
        // 但导师说后端要我自己写,所以我只能...
        console.log(`模拟上传分片 ${chunkIndex + 1}/${fileObj.totalChunks}: ${file.name}`);
        
        // 模拟网络延迟
        await new Promise(resolve => setTimeout(resolve, 500));
        
        return { success: true, chunkIndex };
    }

    async startUpload() {
        if (this.fileList.length === 0) {
            alert('请先选择文件!');
            return;
        }

        for (const fileObj of this.fileList) {
            try {
                for (let i = 0; i < fileObj.totalChunks; i++) {
                    const result = await this.uploadChunk(fileObj, i);
                    if (result.success) {
                        fileObj.uploadedChunks++;
                        this.updateProgress(fileObj);
                        
                        // 保存上传进度到localStorage(断点续传)
                        this.saveProgressToStorage(fileObj);
                    }
                }
                console.log(`文件 ${fileObj.file.name} 上传完成!`);
            } catch (error) {
                console.error(`上传文件 ${fileObj.file.name} 时出错:`, error);
            }
        }
    }

    updateProgress(fileObj) {
        const percent = (fileObj.uploadedChunks / fileObj.totalChunks * 100).toFixed(2);
        this.progressBar.style.width = percent + '%';
        this.progressBar.textContent = percent + '%';
    }

    saveProgressToStorage(fileObj) {
        const progressData = {
            fileId: fileObj.fileId,
            fileName: fileObj.file.name,
            uploadedChunks: fileObj.uploadedChunks,
            totalChunks: fileObj.totalChunks,
            fileSize: fileObj.file.size,
            lastModified: fileObj.file.lastModified
        };
        
        localStorage.setItem(`uploadProgress_${fileObj.fileId}`, JSON.stringify(progressData));
    }

    // 从localStorage恢复上传进度
    static restoreProgress() {
        const progressItems = {};
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key.startsWith('uploadProgress_')) {
                const data = JSON.parse(localStorage.getItem(key));
                progressItems[key] = data;
            }
        }
        return progressItems;
    }
}

// 使用示例
document.addEventListener('DOMContentLoaded', () => {
    const uploader = new FileUploader({
        inputId: 'fileInput',
        uploadBtnId: 'uploadBtn',
        progressId: 'progressBar'
    });
    
    // 恢复上传进度
    const progress = FileUploader.restoreProgress();
    console.log('恢复的上传进度:', progress);
});

3. Vue3组件封装




export default {
  name: 'FileUploader',
  data() {
    return {
      files: [],
      chunkSize: 5 * 1024 * 1024, // 5MB
      uploadProgress: {}
    };
  },
  methods: {
    handleFileChange(e) {
      const files = Array.from(e.target.files);
      
      // 处理文件夹上传(WebKit browsers)
      if (files.length === 0 && e.target.webkitEntries) {
        const entries = Array.from(e.target.webkitEntries);
        this.processDirectoryEntries(entries);
        return;
      }
      
      this.files = files.map(file => ({
        file,
        name: file.name,
        size: file.size,
        uploaded: false,
        uploadedSize: 0
      }));
    },
    
    processDirectoryEntries(entries) {
      // 实际项目中需要递归处理文件夹结构
      // 这里简化处理,只取文件
      const files = [];
      const processEntry = (entry) => {
        if (entry.isFile) {
          entry.file(file => {
            files.push(file);
            if (files.length === entries.length) {
              this.files = files.map(file => ({
                file,
                name: file.name,
                size: file.size,
                uploaded: false,
                uploadedSize: 0
              }));
            }
          });
        }
      };
      
      entries.forEach(processEntry);
    },
    
    async startUpload() {
      if (this.files.length === 0) {
        alert('请先选择文件或文件夹!');
        return;
      }
      
      for (const fileObj of this.files) {
        try {
          await this.uploadFile(fileObj);
          fileObj.uploaded = true;
        } catch (error) {
          console.error(`上传文件 ${fileObj.name} 时出错:`, error);
        }
      }
    },
    
    async uploadFile(fileObj) {
      const file = fileObj.file;
      const totalChunks = Math.ceil(file.size / this.chunkSize);
      
      for (let i = 0; i < totalChunks; i++) {
        const start = i * this.chunkSize;
        const end = Math.min(file.size, start + this.chunkSize);
        const chunk = file.slice(start, end);
        
        // 加密分片(实际项目中应该用更安全的加密方式)
        const encryptedChunk = this.simpleEncrypt(chunk);
        
        // 这里应该调用后端API上传分片
        // 但导师说后端要我自己写,所以我只能...
        console.log(`上传分片 ${i + 1}/${totalChunks}: ${file.name}`);
        
        // 模拟网络延迟
        await new Promise(resolve => setTimeout(resolve, 300));
        
        fileObj.uploadedSize = end;
        this.updateProgress();
      }
    },
    
    simpleEncrypt(data) {
      // 超级简单的"加密"(实际项目不能用!)
      return data; // 这里应该返回加密后的数据
    },
    
    updateProgress() {
      const totalUploaded = this.files.reduce((sum, file) => sum + file.uploadedSize, 0);
      const totalSize = this.files.reduce((sum, file) => sum + file.size, 0);
      const percent = Math.round((totalUploaded / totalSize) * 100);
      
      this.$refs.progressBar.style.width = percent + '%';
      this.$refs.progressBar.nextElementSibling.textContent = percent + '%';
    },
    
    restoreUpload() {
      // 从localStorage恢复上传进度
      const progressItems = {};
      for (let i = 0; i < localStorage.length; i++) {
        const key = localStorage.key(i);
        if (key.startsWith('uploadProgress_')) {
          const data = JSON.parse(localStorage.getItem(key));
          progressItems[key] = data;
        }
      }
      
      console.log('恢复的上传进度:', progressItems);
      alert('上传进度已恢复(控制台查看详情)');
    },
    
    formatFileSize(bytes) {
      if (bytes === 0) return '0 Bytes';
      const k = 1024;
      const sizes = ['Bytes', 'KB', 'MB', 'GB'];
      const i = Math.floor(Math.log(bytes) / Math.log(k));
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }
  },
  mounted() {
    // 检测浏览器兼容性
    this.detectBrowser();
  },
  methods: {
    detectBrowser() {
      const userAgent = navigator.userAgent;
      if (userAgent.indexOf('MSIE') > -1 || userAgent.indexOf('Trident') > -1) {
        const version = userAgent.match(/(MSIE |rv:)(\d+)/)[2];
        if (version < 9) {
          alert(`检测到您使用的是IE${version},本系统需要IE9+或其他现代浏览器!`);
          document.body.innerHTML = `
            <div style="text-align:center; padding:50px;">
              <h1>亲爱的IE用户</h1>
              <img src="https://http.cat/418.jpg" alt="IE已淘汰">
              <p>请使用Chrome/Firefox/Edge/Safari等现代浏览器</p>
            </div>
          `;
        }
      }
    }
  }
};



.file-uploader {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.upload-area {
  margin: 20px 0;
  padding: 20px;
  border: 2px dashed #ccc;
  text-align: center;
}

.progress-container {
  margin: 20px 0;
  height: 30px;
  background: #f0f0f0;
  border-radius: 15px;
  position: relative;
}

.progress-bar {
  height: 100%;
  background: #4CAF50;
  border-radius: 15px;
  width: 0%;
  transition: width 0.3s;
}

.progress-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  color: #333;
}

.file-list {
  margin-top: 30px;
}

.file-list ul {
  list-style: none;
  padding: 0;
}

.file-list li {
  padding: 8px;
  border-bottom: 1px solid #eee;
}

项目趣事

  1. IE8兼容大战:为了兼容IE8,我尝试了各种polyfill,最后发现还是直接显示"请升级浏览器"更实际

  2. 文件夹上传:WebUploader对文件夹上传支持不好,我研究了半天发现原来要用webkitdirectory属性

  3. 断点续传:本来想用IndexedDB存储上传进度,发现IE不支持,最后改用localStorage(虽然有大小限制)

  4. 加密传输:研究了AES加密,发现实现起来太复杂,最后用了简单的Base64编码(导师说这不算真正的加密)

求助与展望

现在前端部分勉强能跑,但后端完全没头绪。群里的小伙伴们也都在喊"后端大佬救命"!

在此我郑重声明:

  1. 不提供后端代码(因为我也不会)
  2. 欢迎大佬加入QQ群374992201指导
  3. 加群送1-99元红包(老板说这是营销策略)
  4. 推荐工作成功者送20%项目提成(虽然现在还没项目)

最后,如果哪位师哥师姐愿意收我为徒,教我后端开发,我愿意:

  • 每天给您请安
  • 帮您写前端代码
  • 毕业设计可以挂您名字
  • 未来第一年工资分您10%

(联系方式:QQ群374992201,群里找我"求带的小张")

将组件复制到项目中

示例中已经包含此目录
image

引入组件

image

配置接口地址

接口地址分别对应:文件初始化,文件数据上传,文件进度,文件上传完毕,文件删除,文件夹初始化,文件夹删除,文件列表
参考:http://www.ncmem.com/doc/view.aspx?id=e1f49f3e1d4742e19135e00bd41fa3de
image

处理事件

image

启动测试

image

启动成功

image

效果

image

数据库

image

效果预览

文件上传

文件上传

文件刷新续传

支持离线保存文件进度,在关闭浏览器,刷新浏览器后进行不丢失,仍然能够继续上传
文件续传

文件夹上传

支持上传文件夹并保留层级结构,同样支持进度信息离线保存,刷新页面,关闭页面,重启系统不丢失上传进度。
文件夹上传

下载示例

点击下载完整示例

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐