背景
在web端,文件的上传是个很常用的功能,当操作的文件很小时,我们的用户体验是比较流畅的,一旦操作大文件,上传文件的时间就比较长,如果中间发生网络波动或者想中途取消上传,再重新上传整个文件,体验就会很差。
那怎样可以优化体验呢?
我们可以选择分片上传,当出现网络异常上传中断后,重新上传相同文件时,可以避免已上传部分文件的重复上传,提升性能,优化用户体验
实施方案
实现方案分为http和webSocket
- http长连接
- webSocket需要和后端约定好数据格式
这两种方案各有优略,http上传时需要客户端与服务端保持长连接,浪费了带宽和服务器资源;websocket方案只需要前后端约定好数据格式保证工作流的顺利进行,但服务端可以主动推送数据,可以节省带宽和服务器资源;
所以综合考虑,我们采用webSocket方案
前端:Vue@2.6.x + Element-UI@2.15.x
服务端:NodeJS@16.10.0 + nodejs-websocket@1.72
整体思路
前端
前端部分的核心方法是利用Blob.prototype.slice方法,而File对象是特殊类型的Blob,所以可以直接对源文件进行按照固定大小进行切片,我们建议切片的大小为4MB/5MB,由于demo主要是为了展示,我这里将切片设置为50KB,查看效果;具体流程为:
- 上传前我们需要将文件的唯一标识(
uuid)发给服务端做存储 - 然后计算当前文件的碎片总数
- 发送
webSocket消息,通知服务端将要上传文件 - 等待服务端返回响应消息
- 根据消息中的
state状态来确定下一步操作 - 当
state为UPLOAD_NEXT且返回了当前上传开始的文件碎片序号currChunk - 允许上传,根据
currChunk来切割文件并上传文件碎片 - 上传完所有碎片后,等待服务端发送处理结果
- 根据处理结果做交互,反馈给用户
如果要确保文件内容绝对唯一,可以对文件内容进行md5的方式,但是如果是大文件,md5的时间会很长,甚至超大文件md5会导致浏览器崩溃,所以这里我采用文件名、文件尺寸、修改时间组合字符串作为唯一标识(uuid);
上传前,先发一个webSocket消息通知服务端,将要开始上传文件,并将文件信息(文件名、uuid、文件碎片总数、数据状态),等待服务端返回信息后,如果返回的信息中 state 为 UPLOAD_NEXT则可以开始上传文件碎片,返回的信息中还会带上当前碎片序号,提示前端从哪一个碎片开始传,这样就可以防止重复上传碎片;
这里我采用边切割文件边上传的方式,如果用户中途点击暂停或者网络波动导致webSocket关闭了,则后续的操作就会停止,如果要续传,重复以上操作,上传剩余的文件碎片即可;
碎片上传完毕后,等待服务端返回响应信息做后续操作,如果服务端返回的state 为 UPLOAD_SUCCESS 则上传成功,为 UPLOAD_FAIL时代表上传失败;
服务端(nodejs)
服务端的核心思路是用一个数组来维护正在上传的文件队列,用一个索引标记当前正在上传的文件是哪个;上传完毕则将该文件从队列中清除,具体的流程是:
- 服务端开启webSocket服务,等待客户端消息
- 接收到客户端发送的上传文件信息后,查询上传文件队列,通过uuid查询是否存在此文件,如果存在则返回对应的文件信息,否则新建一个文件信息并加入队列(文件信息包含当前碎片序号(currChunk),允许下载的状态
state: UPLOAD_NEXT,文件名,文件碎片总数) - 发送这些信息给客户端,然后标记当前上传文件的索引(方便后面接收客户端的文件碎片后进行文件合并处理)
- 接收到客户端发送的文件碎片
- 依次将文件合并到当前上传文件的buffer中,currChunk递增
- 最后一个分片上传完毕,将文件写入到file文件夹下
- 根据写入结果给前端返回上传结果
demo
项目地址:
前端:https://codesandbox.io/s/file-upload-download-client-yk52wt
服务端:https://codesandbox.io/s/file-operate-websocket-xmn2hz (建议在本地运行)
关键代码贴图:
前端:

服务端:
总结
由于当前demo只关注文件分片上传的功能,一些实际场景的优化这边没做处理,比如:webSocket的心跳维持;多个客户端上传同一文件时服务端如何保持碎片的顺序和唯一性(服务端对已上传的碎片进行MD5,然后维护一个md5数组,每次接收碎片时先查看是否已上传该碎片);在实际应用场景可以按照这个思路去实现
当前的方案只是解决了大文件上传的一个痛点,如果我们想要加快上传速度,还需要支持文件碎片的并行上传,后续会对这块的问题进行深入研究~
