背景

在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状态来确定下一步操作
  • stateUPLOAD_NEXT且返回了当前上传开始的文件碎片序号currChunk
  • 允许上传,根据currChunk来切割文件并上传文件碎片
  • 上传完所有碎片后,等待服务端发送处理结果
  • 根据处理结果做交互,反馈给用户


如果要确保文件内容绝对唯一,可以对文件内容进行md5的方式,但是如果是大文件,md5的时间会很长,甚至超大文件md5会导致浏览器崩溃,所以这里我采用文件名、文件尺寸、修改时间组合字符串作为唯一标识(uuid);

上传前,先发一个webSocket消息通知服务端,将要开始上传文件,并将文件信息(文件名、uuid、文件碎片总数、数据状态),等待服务端返回信息后,如果返回的信息中 stateUPLOAD_NEXT则可以开始上传文件碎片,返回的信息中还会带上当前碎片序号,提示前端从哪一个碎片开始传,这样就可以防止重复上传碎片;
这里我采用边切割文件边上传的方式,如果用户中途点击暂停或者网络波动导致webSocket关闭了,则后续的操作就会停止,如果要续传,重复以上操作,上传剩余的文件碎片即可;

碎片上传完毕后,等待服务端返回响应信息做后续操作,如果服务端返回的stateUPLOAD_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 (建议在本地运行)
    关键代码贴图:
    前端:
    image.pngimage.png
    服务端:
    image.png

    总结

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