[2023年1月1日] [星期一] [天气]

作为一名在上海独自打拼的个人开发者,最近接到了一个颇具挑战性的项目需求——大文件上传系统的开发。这可不是个简单的活儿,客户要求系统得支持 20G 左右的大文件传输,不仅要能上传单个文件,还得支持文件夹上传,并且文件夹传输时必须完整保留层级结构,这对技术的要求可不低。

操作系统方面,得兼容 Windows、macOS 和 Linux 这三大主流系统;浏览器支持范围更是广泛,从古老的 IE8 到微软 Edge、火狐 Firefox、谷歌 Chrome、苹果 Safari 以及欧朋 Opera 等所有主流浏览器都要完美适配,这无疑增加了开发的复杂度。

在技术选型上,前端我选用了当下热门的 vue3 cli 框架,它强大的组件化和响应式特性能够大大提高开发效率。后端则采用 PHP,凭借其广泛的服务器支持和丰富的库函数,处理业务逻辑和文件操作再合适不过。数据库用的是 MySQL,稳定可靠且易于维护。存储方面,客户要求支持主流云服务,像阿里云、华为云、腾讯云、百度云、微软云和亚马逊云都在考虑范围内,这为后续的扩展和部署提供了极大的灵活性。

为了实现高效的文件上传,我选用了百度开源的 WebUploader 组件。这个组件功能强大,支持分片上传和断点续传,能够有效应对大文件上传时可能出现的网络中断问题。然而,在实际开发过程中,我发现网上能找到的代码大多只实现了基本的上传功能,文件夹上传功能要么缺失,要么不够完善,无法满足客户保留文件夹层级结构的需求。这让我有些犯难,只能自己深入研究并加以改进。

在前端实现文件夹上传功能时,我通过监听文件夹拖放事件,递归遍历文件夹内的所有文件和子文件夹,构建出完整的文件树结构,并将其与文件数据一起发送到后端。以下是一段简化后的前端代码示例:




import WebUploader from 'webuploader';

export default {
  mounted() {
    const uploader = WebUploader.create({
      swf: 'path/to/Uploader.swf', // Flash 文件路径,用于兼容旧浏览器
      server: '/upload.php', // 后端接收文件接口
      pick: {
        id: '#filePicker',
        multiple: true
      },
      accept: {
        title: 'Files',
        extensions: '*',
        mimeTypes: '*'
      },
      chunked: true, // 开启分片上传
      chunkSize: 2 * 1024 * 1024, // 每片大小 2MB
      threads: 3, // 上传并发数
      formData: {
        // 可在此处添加额外的表单数据
      }
    });

    // 监听文件夹拖放事件
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.webkitdirectory = true; // 启用文件夹选择(仅适用于部分浏览器)
    fileInput.style.display = 'none';
    document.body.appendChild(fileInput);

    const filePicker = document.getElementById('filePicker');
    filePicker.addEventListener('click', () => {
      fileInput.click();
    });

    fileInput.addEventListener('change', (e) => {
      const files = e.target.files;
      const fileTree = {};
      // 递归构建文件树
      const buildFileTree = (files, parentPath = '') => {
        for (let i = 0; i < files.length; i++) {
          const file = files[i];
          const path = parentPath? `${parentPath}/${file.webkitRelativePath || file.name}` : file.webkitRelativePath || file.name;
          if (file.isDirectory) {
            // 如果是文件夹,继续递归
            const dirReader = file.createReader();
            dirReader.readEntries((entries) => {
              const subFiles = Array.from(entries).filter(entry =>!entry.isDirectory).map(entry => ({
                name: entry.name,
                size: entry.size,
                type: entry.type,
                webkitRelativePath: path + '/' + entry.name
              }));
              const subDirs = Array.from(entries).filter(entry => entry.isDirectory);
              subDirs.forEach(dir => {
                const dirReader = dir.createReader();
                dirReader.readEntries((subEntries) => {
                  const subSubFiles = Array.from(subEntries).filter(entry =>!entry.isDirectory).map(entry => ({
                    name: entry.name,
                    size: entry.size,
                    type: entry.type,
                    webkitRelativePath: path + '/' + dir.name + '/' + entry.name
                  }));
                  // 合并文件列表并上传
                  const allFiles = [...subFiles,...subSubFiles];
                  allFiles.forEach(f => {
                    uploader.addFiles({
                      name: f.name,
                      size: f.size,
                      type: f.type,
                      relativePath: f.webkitRelativePath
                    });
                  });
                });
              });
              // 处理当前文件夹下的文件
              subFiles.forEach(f => {
                uploader.addFiles({
                  name: f.name,
                  size: f.size,
                  type: f.type,
                  relativePath: f.webkitRelativePath
                });
              });
            });
          } else {
            // 如果是文件,添加到上传队列
            uploader.addFiles({
              name: file.name,
              size: file.size,
              type: file.type,
              relativePath: path
            });
          }
        }
      };
      buildFileTree(files);
    });
  }
};

后端部分,我使用 PHP 接收前端上传的文件分片,并将其合并成完整的文件。同时,根据前端传递的文件树信息,在服务器上重建文件夹的层级结构。以下是一段简单的后端代码示例:

 'success','message' => 'File uploaded successfully']);
} else {
    echo json_encode(['status' => 'success','message' => 'Chunk uploaded successfully']);
}
?>

除了上传功能,客户还要求实现文件下载功能,并且同样要支持文件夹下载。这部分功能我还在进一步完善中,计划通过将文件夹打包成 ZIP 文件供用户下载。

目前,这个项目已经取得了一定的进展,但仍然面临着诸多挑战。特别是在兼容性和性能优化方面,还需要进行大量的测试和调整。我深知自己一个人的力量有限,非常希望能够得到大神们的免费指导。要是能有高手愿意帮我把代码写好并调试好,让我能够直接交给客户使用,那将是我莫大的荣幸。欢迎各位大神加入 QQ 群 374992201 交流指导,一起攻克这个技术难题!

安装环境

PHP:7.2.14
Alt

调整块大小

Alt

NOSQL

NOSQL不需要任何配置,可以直接访问测试
Alt

SQL

创建数据库

您可以直接复制脚本进行创建
Alt
Alt

配置数据库连接

Alt

安装依赖

Alt

访问页面进行测试

Alt

数据表中的数据

Alt

效果预览

文件上传

文件上传

文件刷新续传

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

文件夹上传

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

免费下载示例

点击下载完整示例

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐