大文件上传的前后端实践:从分片到重组

在实现大文件上传的解决方案时,我们需要考虑的不仅仅是如何将文件从客户端传输到服务器,还包括如何提高上传效率、保证上传过程的可靠性以及如何优化用户体验。以下是实现大文件上传时可以采用的一些策略和技术实现。

1. 文件切片

文件切片是处理大文件上传的一个非常有效的策略。通过将大文件分割成小块(chunk),可以并行上传多个文件块,提高上传效率。此外,如果某个文件块上传失败,只需要重新上传该文件块,而不需要从头开始上传整个文件,这显著提高了上传的可靠性。

  • 前端实现:

使用Blob.prototype.slice方法将文件切割成多个块。 创建一个上传队列,根据网络状况和服务器的承载能力,调整并发上传的块的数量。 监听每个块的上传进度,以便提供给用户实时的上传反馈。

  • 后端实现:

接收上传的文件块,并在服务器端临时存储。 检查所有文件块上传完成后,再将这些块合并成原始文件。 实现机制以处理可能的重复上传,保证文件的完整性。

2. 断点续传

在文件切片的基础上,实现断点续传能进一步提高上传的可靠性。如果上传过程中出现网络断开等问题,可以从上次上传成功的地方继续上传,而不是重新上传整个文件。

  • 前端实现:

在开始上传前,查询服务器,了解哪些文件块已经上传成功。 只上传服务器上不存在的文件块。 实现本地存储机制(如localStorage),记录上传进度,以便于断点续传。

  • 后端实现:

实现接口以允许前端查询已上传的文件块信息。 在文件块上传后,保存其状态(如已上传的块的索引)。 支持从指定的文件块开始合并文件。

3. 压缩与优化

在上传前对文件进行压缩,可以减少需要上传的数据量。对于特定类型的文件(如图片、视频),还可以进行格式转换或质量压缩,以进一步减小文件大小。

  • 前端实现:

使用JavaScript库(如pako用于文本压缩,ffmpeg.wasm用于视频处理)进行文件压缩或格式转换。 优化文件压缩过程,避免阻塞UI线程,提高用户体验(如使用Web Worker)。

  • 后端实现:

在文件上传后,可对文件进行服务器端压缩或格式转换。 提供配置选项,让用户选择是否进行压缩及压缩级别。

4. 优化用户体验

上传大文件可能是一个时间较长的过程,优化用户体验是非常重要的。

提供实时的上传进度反馈。 允许用户暂停、继续或取消上传。 在上传完成后,给予明确的反馈。

5. 安全性考虑

上传大文件时,还需要考虑安全性问题。

  • 对上传的文件进行安全检查,防止恶意软件或病毒上传。
  • 使用HTTPS等加密协议,保证数据在传输过程中的安全。
  • 对文件上传接口进行认证和授权,避免未授权访问。

实战(JS + JAVA)

1. 前端实现

js 复制代码
// 假设后端提供了以下API接口
// 1. POST /upload/chunk 用于上传文件块
// 2. POST /upload/complete 用于通知后端所有文件块上传完成
// 3. GET /upload/check 用于检查文件块上传状态(支持断点续传)
 
// HTML页面中需有一个文件输入<input type="file" id="fileInput">
document.getElementById('fileInput').addEventListener('change', handleFileUpload);
 
async function handleFileUpload(event) {
    const file = event.target.files[0];
    if (!file) {
        return;
    }
 
    const CHUNK_SIZE = 5 * 1024 * 1024; // 每个文件块的大小,这里设为5MB
    const chunkCount = Math.ceil(file.size / CHUNK_SIZE);
 
    for (let i = 0; i < chunkCount; i++) {
        const chunk = file.slice(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('filename', file.name);
        formData.append('chunkIndex', i);
        formData.append('chunkCount', chunkCount);
 
        // 上传前检查文件块是否已上传
        const { uploaded } = await checkChunkStatus(file.name, i);
        if (!uploaded) {
            // 显示进度信息
            const onProgress = (percentage) => {
                console.log(`Chunk ${i + 1}/${chunkCount}: ${percentage}%`);
            };
            await uploadChunk(formData, onProgress); // 上传文件块,监听进度
        }
    }
 
    // 所有块上传完成后,通知服务器合并文件
    await notifyServerComplete(file.name, chunkCount);
}
 
// 检查文件块上传状态
async function checkChunkStatus(filename, chunkIndex) {
    const response = await fetch(`/upload/check?filename=${filename}&chunkIndex=${chunkIndex}`);
    return response.json();
}
 
// 使用XMLHttpRequest以便可以监听上传进度
function uploadChunk(formData, onProgress) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/upload/chunk');
 
        // 监听上传进度事件
        xhr.upload.onprogress = function (event) {
            if (event.lengthComputable) {
                const percentage = (event.loaded / event.total) * 100;
                onProgress(Math.round(percentage));
            }
        };
 
        xhr.onload = function () {
            if (xhr.status === 200) {
                resolve();
            } else {
                reject(`Failed to upload chunk: ${xhr.statusText}`);
            }
        };
 
        xhr.onerror = function () {
            reject('Failed to upload chunk due to a network error');
        };
 
        xhr.send(formData);
    });
}
 
// 通知服务器所有文件块上传完成
async function notifyServerComplete(filename, chunkCount) {
    await fetch('/upload/complete', {
        method: 'POST',
        body: JSON.stringify({ filename, chunkCount }),
        headers: {
            'Content-Type': 'application/json',
        },
    });
}

在这段代码中,xhr.upload.onprogress 事件处理器被用来监听文件上传的进度。当一个文件(或文件块)正在通过 XMLHttpRequest (xhr)上传到服务器时,这个事件处理器会被周期性地调用,提供关于当前上传进度的实时信息。具体来说,event、event.loaded 和 event.total 这几个部分扮演了关键的角色:

  • event: 在这个上下文中,event 是一个 ProgressEvent 对象,它提供了关于正在进行的文件上传进度的信息。这个对象包含了多个属性,其中 loaded 和 total 是我们特别关心的。
  • event.loaded: 这个属性表示到目前为止已经上传的字节数。每次 onprogress 事件被触发时,event.loaded 会更新,以反映已上传的数据量。
  • event.total: 这个属性表示整个上传任务的总字节数。在文件上传的场景中,这通常是当前正在上传的文件(或文件块)的总大小。event.total 的值在整个上传过程中保持不变。 通过 event.loaded 和 event.total,我们可以计算出当前上传进度的百分比:

const percentage = (event.loaded / event.total) * 100; 这里,我们首先计算 event.loaded 除以 event.total 的值,这个比值代表了上传进度的小数形式(例如,0.5 表示上传了50%)。然后,我们将这个小数乘以100,得到一个百分比值。使用 Math.round(percentage) 可以将这个百分比值四舍五入到最接近的整数,以便于更加人性化地展示上传进度(比如在进度条或进度提示中)。

此外,if (event.lengthComputable) 这个条件检查确保了只有当上传进度的信息是可计算的(即 event.total 已知)时,我们才计算和展示进度百分比。这是一个好习惯,因为在某些情况下,可能无法预先知道总的上传大小,导致进度信息不可用。

2. 后端实现

java 复制代码
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
 
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collections;
 
@RestController
public class FileUploadController {
 
    private static final String UPLOAD_DIR = "/path/to/upload/dir";
 
    @PostMapping("/upload/chunk")
    public ResponseEntity<?> uploadChunk(@RequestParam("file") MultipartFile file,
                                         @RequestParam("filename") String filename,
                                         @RequestParam("chunkIndex") int chunkIndex,
                                         @RequestParam("chunkCount") int chunkCount) {
        // 存储上传的文件块
        File chunkFile = new File(UPLOAD_DIR, filename + "-" + chunkIndex);
        try {
            file.transferTo(chunkFile);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
        return ResponseEntity.ok().build();
    }
 
    @GetMapping("/upload/check")
    public ResponseEntity<?> checkChunkStatus(@RequestParam("filename") String filename,
                                              @RequestParam("chunkIndex") int chunkIndex) {
        File chunkFile = new File(UPLOAD_DIR, filename + "-" + chunkIndex);
        boolean uploaded = chunkFile.exists();
        return ResponseEntity.ok(Collections.singletonMap("uploaded", uploaded));
    }
 
    @PostMapping("/upload/complete")
    public ResponseEntity<?> completeUpload(@RequestBody CompleteUploadRequest request) {
        // 合并文件块
        try {
            mergeChunks(request.filename, request.chunkCount);
        } catch (IOException e) {
            e.printStackTrace();
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
        return ResponseEntity.ok().build();
    }
 
    private void mergeChunks(String filename, int chunkCount) throws IOException {
        File outputFile = new File(UPLOAD_DIR, filename);
        try (OutputStream mergeFile = new BufferedOutputStream(new FileOutputStream(outputFile))) {
            byte[] buffer = new byte[1024];
            for (int i = 0; i < chunkCount; i++) {
                File chunkFile = new File(UPLOAD_DIR, filename + "-" + i);
                try (InputStream chunkFileStream = new BufferedInputStream(new FileInputStream(chunkFile))) {
                    int bytesRead;
                    while ((bytesRead = chunkFileStream.read(buffer)) > 0) {
                        mergeFile.write(buffer, 0, bytesRead);
                    }
                }
                Files.deleteIfExists(chunkFile.toPath()); // 删除处理过的文件块
            }
        }
    }
 
    static class CompleteUploadRequest {
        public String filename;
        public int chunkCount;
    }
}

总结

实现大文件上传是一个综合性的工程,涉及到前端的文件处理、网络传输优化,以及后端的文件接收、存储和安全处理等多个方面。通过文件切片、断点续传、文件压缩以及用户体验的优化,可以有效地提高大文件上传的效率和可靠性,为用户提供更加流畅和友好的上传体验。同时,安全性措施也不可忽视,以保证整个上传过程的安全可靠。

相关推荐
叶浩成52012 分钟前
vue2+a-table——实现框选选中功能——js技能提升
开发语言·前端·javascript
迂 幵13 分钟前
vue 提交表单抹除字段为空的数据
前端·javascript·vue.js
Justinc.15 分钟前
CSS3_过渡(八)
前端·css·css3
番茄小酱00116 分钟前
vue3树形结构如何实现右击弹框显示
前端·javascript·vue.js
qq_5443291716 分钟前
关于写React的一些反思和总结
前端·react.js·前端框架
MarcoPage20 分钟前
第二十一课 Vue组件实用示例
前端·javascript·vue.js
Bennett_G21 分钟前
掌握Electron工具链:在Windows操作系统上无缝开发MacOS软件
前端·javascript·macos·electron
獨枭42 分钟前
使用阿里云远程访问 Synology Web Station 的指南
前端·阿里云·云计算
zqwang8881 小时前
Vue3.5正式上线,父传子props用法更丝滑简洁
前端·javascript·vue.js
清灵xmf1 小时前
为什么 Vue3 封装 Table 组件丢失 expose 方法呢?
开发语言·前端·javascript·封装·eltable