你有没有遇到过这种情况:上传一个视频或者几十张图片,传到一半网络断了,或者不小心退出了页面,回来发现之前的进度全没了,只能从头开始重新传。
对于小文件,重新传一次也无所谓。但对于几十MB甚至上百MB的视频文件,重复上传既浪费时间又消耗流量。今天聊聊小程序中如何实现大文件上传,以及断点续传这个关键能力。
小程序的上传能力和传统网页或者App相比,有几个天然的限制。
第一个限制是请求超时。小程序的wx.request和wx.uploadFile默认超时时间通常是60秒。一个100MB的视频在上传速度较慢的网络条件下,很可能超过这个时间。
第二个限制是内存。小程序运行在微信的容器里,可用的内存比原生App要少。如果一次性把整个文件读到内存中再上传,大文件可能导致小程序闪退。
第三个限制是后台运行。用户切出去回个消息,小程序可能被挂起,上传任务就被中断了。
这些问题都需要在上传方案中逐一处理。
分片上传是解决大文件上传问题的通用思路。把一个大文件切成很多小块,一小块一小块地上传,全部传完后再通知服务器把这些小块拼起来。
实现步骤大致如下。
第一步,用户选择文件。用wx.chooseMedia或wx.chooseMessageFile获取文件对象,拿到文件的临时路径和大小。
第二步,计算分片。确定每个分片的大小,比如每片1MB。根据文件总大小计算出需要分成多少片。
const CHUNK_SIZE = 1024 * 1024 // 每片1MB function calcChunks(fileSize) { const chunkCount = Math.ceil(fileSize / CHUNK_SIZE) return chunkCount }
第三步,读取分片数据。小程序的文件系统API可以读取指定范围的数据。使用wx.getFileSystemManager和readFile方法,通过position和length参数读取指定的字节范围。
const fs = wx.getFileSystemManager() fs.readFile({ filePath: tempFilePath, position: startByte, length: CHUNK_SIZE, success: (res) => { // res.data 就是这一片的二进制数据 uploadChunk(chunkIndex, res.data) } })
第四步,逐片上传。每读取一个分片,就用wx.request将这个分片的数据上传到服务器。服务器需要支持接收分片数据,并记录哪些分片已经上传成功。
第五步,合并分片。所有分片上传完成后,向服务器发送一个合并请求,服务器将这些分片按顺序拼回完整的文件。
断点续传是在分片上传的基础上,增加了一个断点记录的能力。如果上传中途中断了,下次只需要从失败的地方继续上传,不需要从头开始。
实现断点续传的关键是保存每片的上传状态。
// 记录每个分片的上传状态 const uploadProgress = { fileId: 'unique_file_id', totalChunks: 20, uploadedChunks: [0, 1, 2, 5], // 已上传的分片索引 uploadingChunk: 3 // 正在上传的 } // 保存到本地 wx.setStorageSync(`upload_${fileId}`, uploadProgress)
上传中断后,用户再次上传同一个文件时,先从本地存储读取之前的进度,跳过已经上传成功的分片,从第一个未上传的分片开始继续。
服务器端也需要配合,提供一个接口让小程序查询哪些分片已经上传过了:
// 检查已上传的分片 function checkUploadedChunks(fileId, totalChunks) { wx.request({ url: 'https://api.example.com/upload/status', data: { fileId, totalChunks }, success: (res) => { const uploadedChunks = res.data.uploadedChunks // 从第一个缺失的分片开始继续上传 const startIndex = findFirstMissingChunk(uploadedChunks, totalChunks) resumeUpload(startIndex) } }) }
上传大文件是一个耗时操作,用户等待期间需要明确的反馈。
进度条是最基本的。记录已上传的分片数量和总分片数量,计算出百分比。用户需要知道大概还要等多久。
function updateProgress(chunkIndex, totalChunks) { const percent = Math.floor((chunkIndex + 1) / totalChunks * 100) this.setData({ uploadPercent: percent }) }
后台上传是一个可选但非常实用的功能。用户开始上传后可以继续浏览小程序的其他页面,上传在后台继续进行。微信小程序支持在app.js中维护一个全局的上传任务管理器,上传任务的生命周期不绑定在单个页面上。
需要注意的是,用户完全退出小程序后,上传任务会被销毁。这时应该保存进度,下次进入时提醒用户有未完成的上传任务。
// 在app.js中监听小程序进入后台 onHide() { if (this.uploadingTask) { // 保存当前进度 saveUploadProgress() // 可以选择显示一个本地通知(如果用户开启了消息订阅) } }
第一个坑是分片大小选择不合理。分片太小会导致请求数量过多,每个请求都有额外的网络开销。分片太大会增加超时风险和内存压力。实践中1MB到2MB是比较合适的选择,但需要根据实际的网络环境和服务器配置调整。
第二个坑是文件读取的方式。使用fs.readFile直接读取整个大文件到内存是有风险的。更好的做法是使用数组缓冲区接口,或者考虑使用小程序的UploadTask特性,这个接口本身就支持分片上传,但文档较少,需要仔细阅读官方文档。
第三个坑是并发上传的控制。如果一次性把所有分片同时发送出去,可能会占满网络带宽,反而导致每个请求都变慢。通常建议控制并发数量在3到5个,串行和并行结合的方式效率最高。
第四个坑是服务器合并分片的时机。如果服务器在收到所有分片后立即合并,大文件的合并操作本身也需要时间,这个过程中如果服务器重启或超时,整个上传就会失败。更稳健的做法是:服务器收到分片后先存为临时文件,最后通过异步任务队列来合并,上传接口直接返回“已接收,正在处理”。
不是所有项目都需要实现这套方案。
如果上传的文件都很小,比如只有几百KB的用户头像,直接使用wx.uploadFile就够了,没必要搞分片。
如果用户群体主要使用WiFi网络,网络稳定,断线概率低,分片上传的收益也不明显。
但如果是以下场景,强烈建议实现断点续传:视频上传类小程序、大文件分享工具、企业办公类小程序需要上传报表或扫描件、用户可能在移动网络环境下使用的任何应用。
小程序官方提供的API中,wx.uploadFile本身不支持分片和断点续传,需要开发者自己实现。好消息是社区已经有不少成熟的封装方案和开源代码可以参考,不需要从零开始造轮子。