云盘类产品处理不好大文件上传,页面卡顿、请求超时几乎不可避免。单次 FormData 上传大文件时,服务端容易超时,客户端也可能因内存压力而崩溃。分片上传的思路是把文件按固定大小切块,逐片传输,最后合并;秒传则是在上传前先判断文件是否已经存在,重复内容直接跳过。Vue 3 的组合式 API 很适合把这套流程封装成可复用逻辑。下面先梳理整体实现路径。

分片上传的流程与分片大小选择
分片上传并不是简单把文件切开就可以,需要前端与后端约定好每片的序号、总片数、文件标识和上传地址。前端通常用 Blob.slice 生成切片,每个切片对应一个 File 或 Blob 对象。切片后可以逐片执行上传接口,但直接串行上传大文件会非常慢,因此需要并发控制。每片上传成功后,后端记录该分片,全部完成后再调用合并接口生成完整文件。
分片大小直接影响并发数量和请求开销。5MB 是常见选择,但也要看服务端限制和网络状况。分片太小会导致请求数量过多,HTTP 头开销变大;分片太大又容易触发网关超时。一般 1MB 到 10MB 都可以接受。另一个要注意的是,分片总片数不能过多,浏览器同时并发请求数量有限,建议每批 3 到 6 个。下面的代码展示了基础的切片逻辑。
const CHUNK_SIZE = 5 * 1024 * 1024 // 5MB
function createChunks(file, chunkSize = CHUNK_SIZE) {
const chunks = []
let start = 0
while (start < file.size) {
const end = Math.min(start + chunkSize, file.size)
chunks.push(file.slice(start, end))
start = end
}
return chunks
}
这里用 while 循环从 0 开始截取文件片段,end 位置不能超过文件总大小。返回的数组可以通过 FormData 追加为 file 字段传给后端。需要注意的是,切片操作本身不消耗大量内存,因为 Blob.slice 返回的是新的 Blob 引用,真正读取数据是在上传时进行的。
秒传的核心是文件哈希预检
秒传的本质是拿文件的唯一标识去服务端查询。这个标识通常使用文件内容的哈希值,比如 MD5、SHA-1 或 SHA-256。只要两个文件内容完全一致,它们的哈希值就相同,服务端可以据此判断是否已经存储过。前端计算哈希不能只用文件名或大小,因为不同文件可能有相同大小,文件名也不可靠。
浏览器中计算大文件哈希会阻塞主线程,建议放到 Web Worker 中执行,或者使用 FileReader 分片读取累加哈希。常见做法是使用 spark-md5 库,它支持增量计算,可以配合分片逻辑一起使用。下面是一个简化的异步哈希计算示例。
import SparkMD5 from 'spark-md5'
function computeHash(file) {
return new Promise((resolve, reject) => {
const chunkSize = 2 * 1024 * 1024
const totalChunks = Math.ceil(file.size / chunkSize)
let current = 0
const spark = new SparkMD5.ArrayBuffer()
const reader = new FileReader()
reader.onload = (e) => {
spark.append(e.target.result)
current += 1
if (current < totalChunks) {
loadNext()
} else {
resolve(spark.end())
}
}
reader.onerror = reject
function loadNext() {
const start = current * chunkSize
const end = Math.min(start + chunkSize, file.size)
reader.readAsArrayBuffer(file.slice(start, end))
}
loadNext()
})
}
拿到哈希后,前端调用秒传检查接口。如果返回文件已存在,就可以直接把文件元数据写入当前用户的云盘目录,秒传完成;如果不存在,再进入分片上传阶段。这样同一个文件被多个用户保存时,只需要在后端存储一份,后续用户全部走秒传。需要注意的是,服务端需要记录哈希与文件存储路径的映射关系,并根据引用计数做清理策略。
Vue 3 中封装可复用的上传逻辑
在 Vue 3 项目里,推荐用组合式函数封装分片上传。把文件状态、哈希计算、上传进度和并发控制都放到一个 useChunkUpload 函数中。这样做的好处是组件只负责调用 startUpload 和展示进度,具体逻辑可以独立维护。首先定义响应式状态,例如上传进度、上传状态和分片缓存。
上传前需要先确定文件是否已经完整上传过,所以流程可以分为三步:计算哈希、预检、分片上传与合并。每一步都可能出现异步等待,因此函数内部需要处理好错误恢复。例如哈希计算失败时,可以提示用户重试;网络请求失败时,需要保存已成功的分片序号,以便断点续传。下面的组合式函数展示了核心结构。
import { ref, computed } from 'vue'
export function useChunkUpload(file) {
const chunks = createChunks(file)
const uploadedChunks = ref(new Set())
const status = ref('idle') // idle | hashing | checking | uploading | done
const progress = computed(() => {
if (chunks.length === 0) return 0
return Math.round((uploadedChunks.value.size / chunks.length) * 100)
})
async function startUpload() {
status.value = 'hashing'
const hash = await computeHash(file)
status.value = 'checking'
const exists = await checkFileExists(hash)
if (exists) {
status.value = 'done'
return { hash, exists: true }
}
status.value = 'uploading'
await uploadChunksWithConcurrency(chunks, ({ chunkIndex }) => {
uploadedChunks.value.add(chunkIndex)
})
await mergeFile(hash)
status.value = 'done'
return { hash, exists: false }
}
return { startUpload, progress, status }
}
这里的 createChunks、computeHash、checkFileExists、uploadChunksWithConcurrency 和 mergeFile 分别对应各个阶段。实际项目中,上传分片时还需要携带 uploadId 或 hash,让服务端知道这批分片属于哪个文件。进度通过 computed 根据已上传分片数量自动计算,避免手动维护。
断点续传和并发控制的设计
断点续传是分片上传的重要补充。用户刷新页面或网络中断后,已上传的分片不应该重新传。前端可以用 localStorage 记录已经成功的分片序号,重新进入页面时先读取这些记录,只上传缺失的分片。服务端也需要提供查询已传分片的接口,防止本地记录丢失。
并发控制方面,不建议一次性上传全部分片。浏览器对同一域名的并发连接数有限制,过多请求会互相阻塞。可以用一个简单的 Promise 调度器,每次只执行固定数量的上传任务,一个完成后立即启动下一个。下面分别给出断点记录和并发控制的简化实现。
const STORAGE_KEY = 'uploaded_chunks'
function loadUploadedChunks() {
try {
return new Set(JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'))
} catch {
return new Set()
}
}
function saveUploadedChunks(uploadedChunks) {
localStorage.setItem(STORAGE_KEY, JSON.stringify([...uploadedChunks]))
}
async function uploadWithConcurrency(chunks, onChunkDone, concurrency = 3) {
const pending = [...chunks.keys()]
let currentIndex = 0
const workers = Array.from({ length: concurrency }, async () => {
while (currentIndex < pending.length) {
const chunkIndex = pending[currentIndex++]
await uploadChunk(chunks[chunkIndex], chunkIndex)
onChunkDone(chunkIndex)
}
})
await Promise.all(workers)
}
uploadWithConcurrency 使用了 currentIndex 作为共享索引,多个 worker 协程会依次领取任务。这种方式实现简单,适合大多数前端场景。如果分片数量非常多,可以考虑增加错误重试机制,对外层请求失败单独重试 2 到 3 次,避免整个上传流程直接中断。
进度聚合除了在 computed 中计算百分比,还需要同步到界面。上传状态下,已上传分片数除以总分片数就是整体进度。这里要注意,如果采用并发上传,多个分片完成的时间不同,进度更新会比较平滑。秒传场景下进度可以直接显示 100%,因为文件没有实际传输。
接口约定与边界场景处理
前端实现离不开后端配合。一个完整的分片上传接口通常包括预检、上传分片、合并三个主要动作。预检接口根据哈希返回文件是否已存在;上传分片接口接收 hash、chunkIndex 和分片文件;合并接口在所有分片上传完成后触发服务端合并。请求可以用 FormData 或二进制流,字段命名需要前后端保持一致。
除了正常流程,边界场景也需要处理。例如分片上传到一半时后端存储失败,需要返回明确错误码让前端跳过重传;合并时如果缺失分片,合并接口应返回未上传的分片序号,前端补传后再次合并。服务端还需要设置分片过期时间,超过一定时间未合并的临时分片应当清理,避免浪费空间。文件哈希计算比较耗时,后端的哈希映射表要建索引,查询速度直接影响秒传体验。
在实际项目中,还可以把哈希计算迁移到 Web Worker,避免在 computeHash 期间页面交互卡顿。用户选择文件后立即显示文件名和大小,同时异步计算哈希。秒传成功后要更新云盘文件列表,不需要走上传进度动画。把这些问题考虑清楚,Vue 3 云盘应用的分片上传和秒传功能就能比较稳定地落地。