欢迎光临
我们一直在努力

教育行业网页应用如何集成文件夹上传组件?

开发者日记:2023年X月X日 星期X 长沙 晴

项目背景 今日正式启动客户的大文件传输系统项目,需求明确:支持20G文件/文件夹上传下载、跨平台(Windows/macOS/Linux)、全浏览器兼容(含IE8)、断点续传、保留文件夹层级结构。后端使用**.NET Core**(而非PHP),数据库改为SQL Server,存储仍用阿里云OSS,前端为Vue3 CLI + WebUploader/H5。客户强调高频文件夹上传场景,需极致优化用户体验。免费开源代码和7*24支持的压力依旧,但技术栈调整带来新挑战。


技术选型与调整

  • 前端框架

    • Vue3 CLI:维持组件化开发,但需适配IE8的Polyfill(如babel-polyfill)。
    • WebUploader:核心上传组件,需深度定制文件夹解析逻辑。
    • H5 File API:现代浏览器备用方案,IE8回退到Flash上传。
  • 后端架构

    • .NET Core:替代PHP,利用IFormFile处理分片,System.Data.SqlClient操作SQL Server。
    • SQL Server:存储文件元数据(路径、分片状态、用户ID)。
  • 核心难点升级

    • 高频文件夹上传:需优化递归解析性能,避免前端卡顿。
    • 断点续传稳定性:SQL Server事务保证分片记录一致性。
    • IE8兼容性:Flash上传需处理跨域问题(crossdomain.xml)。

  • 前端代码优化(Vue3 + WebUploader)

    1. 强化文件夹上传逻辑

    // src/components/FolderUploader.vue
    import { ref, onMounted } from 'vue';
    import WebUploader from 'webuploader';
    import 'webuploader/dist/webuploader.css';

    export default {
    setup() {
    const uploader = ref(null);
    const folderTree = ref([]); // 存储文件夹层级结构

    onMounted(() => {
    // 动态加载Flash(IE8兼容)
    if (!WebUploader.Uploader.support('HTML5')) {
    WebUploader.Uploader.register({
    name: 'flash',
    fn: () => '/assets/Uploader.swf'
    });
    }

    uploader.value = new WebUploader.Uploader({
    swf: '/assets/Uploader.swf',
    server: '/api/upload',
    pick: '#folderPicker',
    chunked: true,
    chunkSize: 4 * 1024 * 1024, // 4MB分片
    formData: {
    fileId: '',
    isDir: false,
    relativePath: '' // 关键:记录文件相对路径
    }
    });

    // 监听文件夹选择(需配合input[directory])
    document.getElementById('folderInput').addEventListener('change', (e) => {
    const items = e.target.files;
    if (items) {
    parseFolder(items); // 递归解析文件夹
    }
    });
    });

    // 递归解析文件夹(兼容现代浏览器)
    const parseFolder = (items) => {
    const tree = [];
    for (let i = 0; i < items.length; i++) {
    const file = items[i];
    const path = file.webkitRelativePath || file.name; // 相对路径
    const segments = path.split('/');

    // 构建树形结构
    let currentLevel = tree;
    segments.slice(0, 1).forEach(segment => {
    let dir = currentLevel.find(item => item.name === segment);
    if (!dir) {
    dir = { name: segment, type: 'directory', children: [] };
    currentLevel.push(dir);
    }
    currentLevel = dir.children;
    });

    // 添加文件节点
    currentLevel.push({
    name: segments.pop(),
    type: 'file',
    file: file,
    relativePath: path
    });
    }
    folderTree.value = tree;
    uploadFolder(tree);
    };

    // 上传文件夹内容
    const uploadFolder = (tree) => {
    tree.forEach(node => {
    if (node.type === 'directory') {
    uploadFolder(node.children); // 递归上传子目录
    } else {
    const formData = uploader.value.option('formData');
    formData.relativePath = node.relativePath;
    uploader.value.addFile(node.file, node.relativePath);
    }
    });
    };

    return { uploader, folderTree };
    }
    };

    2. IE8兼容性处理

    后端代码实现(.NET Core + SQL Server)

    1. 分片上传接口

    // Controllers/UploadController.cs
    using Microsoft.AspNetCore.Mvc;
    using System.Data.SqlClient;
    using System.IO;

    [ApiController]
    [Route("api/upload")]
    public class UploadController : ControllerBase
    {
    private readonly IConfiguration _config;
    public UploadController(IConfiguration config) => _config = config;

    [HttpPost]
    public async Task Upload([FromForm] UploadModel model)
    {
    var fileId = model.FileId ?? Guid.NewGuid().ToString();
    var tempDir = Path.Combine("/tmp/uploads", fileId);
    Directory.CreateDirectory(tempDir);

    // 保存分片
    var chunkPath = Path.Combine(tempDir, $"chunk_{model.ChunkIndex}");
    using (var stream = new FileStream(chunkPath, FileMode.Create))
    {
    await model.File.CopyToAsync(stream);
    }

    // 记录分片状态到SQL Server
    using (var conn = new SqlConnection(_config.GetConnectionString("Default")))
    {
    await conn.OpenAsync();
    var cmd = new SqlCommand(
    @"MERGE INTO UploadProgress AS target
    USING (VALUES (@fileId, @chunkIndex, @totalChunks)) AS source (FileId, ChunkIndex, TotalChunks)
    ON target.FileId = source.FileId AND target.ChunkIndex = source.ChunkIndex
    WHEN NOT MATCHED THEN
    INSERT (FileId, ChunkIndex, TotalChunks, UploadedAt)
    VALUES (source.FileId, source.ChunkIndex, source.TotalChunks, GETDATE());"
    , conn);

    cmd.Parameters.AddWithValue("@fileId", fileId);
    cmd.Parameters.AddWithValue("@chunkIndex", model.ChunkIndex);
    cmd.Parameters.AddWithValue("@totalChunks", model.TotalChunks);
    await cmd.ExecuteNonQueryAsync();
    }

    // 如果是最后一块,合并并上传OSS
    if (model.ChunkIndex == model.TotalChunks 1)
    {
    var finalPath = Path.Combine(tempDir, "final_file");
    using (var output = System.IO.File.Create(finalPath))
    {
    for (int i = 0; i < model.TotalChunks; i++)
    {
    var chunk = System.IO.File.ReadAllBytes(Path.Combine(tempDir, $"chunk_{i}"));
    await output.WriteAsync(chunk, 0, chunk.Length);
    }
    }

    // 上传OSS(需引入阿里云OSS SDK)
    var client = new OSSClient("endpoint", "accessKey", "secretKey");
    await client.PutObjectAsync("bucket-name", $"uploads/{fileId}", finalPath);

    // 清理临时文件
    Directory.Delete(tempDir, true);
    }

    return Ok(new { success = true, fileId });
    }
    }

    public class UploadModel
    {
    public IFormFile File { get; set; }
    public string FileId { get; set; }
    public int ChunkIndex { get; set; }
    public int TotalChunks { get; set; }
    public string RelativePath { get; set; } // 文件夹层级路径
    }

    2. SQL Server表结构

    CREATE TABLE UploadProgress (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    FileId NVARCHAR(64) NOT NULL,
    ChunkIndex INT NOT NULL,
    TotalChunks INT NOT NULL,
    UploadedAt DATETIME2 DEFAULT GETDATE(),
    UNIQUE (FileId, ChunkIndex)
    );


    断点续传实现

    1. 前端恢复逻辑

    // 检查未完成上传
    const checkResume = async (fileId) => {
    const res = await fetch(`/api/upload/progress?fileId=${fileId}`);
    const data = await res.json();

    if (data.completedChunks < data.totalChunks) {
    uploader.value.option('formData', {
    fileId,
    chunk: data.completedChunks
    });
    uploader.value.upload();
    }
    };

    // 本地存储fileId(即使浏览器关闭)
    window.addEventListener('beforeunload', () => {
    if (uploader.value.getFiles().length > 0) {
    localStorage.setItem('lastUploadId', uploader.value.option('formData').fileId);
    }
    });

    2. 后端进度查询

    [HttpGet("progress")]
    public async Task GetProgress(string fileId)
    {
    using (var conn = new SqlConnection(_config.GetConnectionString("Default")))
    {
    await conn.OpenAsync();
    var cmd = new SqlCommand(
    "SELECT COUNT(*) AS Completed FROM UploadProgress WHERE FileId = @fileId", conn);
    cmd.Parameters.AddWithValue("@fileId", fileId);

    var count = (int)await cmd.ExecuteScalarAsync();
    return Ok(new { completedChunks = count });
    }
    }


    今日总结

    • 进展:完成文件夹层级解析和.NET Core分片上传逻辑,IE8兼容性方案验证通过。
    • 问题:
    • WebUploader在IE8下对大文件夹性能较差,需优化DOM操作。
    • SQL Server事务需加强,避免分片记录残留。
    • 明日计划:
    • 实现OSS分片上传(避免本地合并临时文件)。
    • 编写完整的错误处理和日志系统。

    求助:若有熟悉.NET Core文件处理或SQL Server优化的高手,欢迎加入QQ群374992201指导!代码将完全开源回馈社区。


    (注:实际项目需补充安全校验、OSS直传和性能监控代码。)

    设置框架

    安装.NET Framework 4.7.2 https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472 框架选择4.7.2 Alt

    添加3rd引用

    Alt

    编译项目

    Alt

    NOSQL

    NOSQL无需任何配置可直接访问页面进行测试 Alt

    SQL

    使用IIS 大文件上传测试推荐使用IIS以获取更高性能。 Alt

    使用IIS Express

    小文件上传测试可以使用IIS Express Alt

    创建数据库

    Alt

    配置数据库连接信息

    Alt

    检查数据库配置

    Alt

    访问页面进行测试

    Alt 相关参考: 文件保存位置,

    效果预览

    文件上传

    文件上传

    文件刷新续传

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

    文件夹上传

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

    批量下载

    支持文件批量下载 批量下载

    下载续传

    文件下载支持离线保存进度信息,刷新页面,关闭页面,重启系统均不会丢失进度信息。 下载续传

    文件夹下载

    支持下载文件夹,并保留层级结构,不打包,不占用服务器资源。 文件夹下载

    下载完整示例

    下载完整示例

    赞(0)
    未经允许不得转载:171主机测评 » 教育行业网页应用如何集成文件夹上传组件?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址