大文件分片上传实现分享

694 阅读2分钟

卷一篇水文
自己封装过大文件分片上传,也看了很多写大文件上传的文章,总觉得差那么点意思,文章里面贴的代码也存在以下问题(个人主观):

1.代码复用性太差

2.前端无法控制上传流程

3.耦合度高

  • vue/react框架耦合
  • ...

再抛出几个前端代码实现的观点:

1.大文件上传的核心逻辑

前端核心逻辑就2点:

  1. 把文件切成小文件,或者叫碎片文件
  2. 把碎片文件上传到服务端

2.上传流程控制

没有流程控制,就很不方便,为什么这么说呢,假设我们要上传1000个碎片,每个碎片上传需要1s,用户觉得太久了或者用户选错文件了,想要取消上传,但是没有上传流程控制,就无法通过点击按钮暂停或者取消上传,只能刷新页面或者等待上传接口报错...

3.通过事件回调来减少代码耦合

4.不要封装成Reac/Vue组件

前端组件会包含页面ui,会大大较低逻辑功能的跨项目复用性,所以最好封装成function或者class

基于以上观点,我自己封装了关于大文件上传的class,借助这个class的能力,我们来实现一下大文件上传。

NPM: www.npmjs.com/package/fil…

DEMO-CODE

player.bilibili.com/player.html…

前端

核心实现 - 代码量很少

以下代码就能实现基本的文件分片上传的逻辑

import fileSliceUpload from 'file-slice-upload'

fileSliceUpload()
  .file(yourfile, '1M')  
  .uploadFunc(async(chunk, index, chunks) => {
    const formData = new FormData()
    formData.append('chunkFile', chunk)
    const res = await axios
    .post('http://localhost/sendChunkFile', formData)
    return res.data.success
  })
  .start()

在核心实现的基础上进行扩展,用react + antd完整实现整个功能。

import { Button, Upload, Modal, Progress, message } from 'antd'
import { useRef, useState } from 'react'
import fileSliceUpload from 'file-slice-upload'
import axios from 'axios'
import md5 from './md5';

type Props = {
  vis: boolean,
  onClose: () => void
}
  
  function FileUpload(props: Props) {
    const { onClose, vis } = props
    const file = useRef<File>()
    const upload = useRef<any>()
    const [percent, setPer] = useState(0)
    const onOk = async () => {
      if (file.current) {
        const hash = await md5(file.current, (p) => setPer(Math.ceil(30 * p)))
        if(!file.current) return 
        upload.current = fileSliceUpload(1)
          .file(file.current, '1M')
          .uploadFunc(async(chunk, index, chunks) => {
            const formData = new FormData()
            formData.append('chunkFile', chunk)
            formData.append('hash', hash)
            formData.append('all', `${chunks.length}`)
            const res = await axios
            .post('http://120.25.173.175:9876/sendChunkFile', formData)
            return res.data.success
          })
          .on('progress', (e) => setPer(Math.ceil(30 + 49 * e.done / e.all)))
          .on('finish', async ({ chunks }) => {
            const formData = new FormData()
            formData.append('hash', hash)
            formData.append('fileName', file.current!.name)
            formData.append('all', `${chunks.length}`)
            const res = await axios
            .post('http://120.25.173.175:9876/mergeChunkFile', formData)
            setPer(100)
            message.success('上传成功')
            window.open('http://120.25.173.175:9876/getFile/' + res.data.fileName)
          })
          .start()
      }
    }
    return <Modal onCancel={() => {
        file.current = undefined
        upload.current?.stop()
        onClose()
      }} cancelText="取消" okText="上传" onOk={onOk} visible={props.vis}>
      <div>
        演示服务器带宽小, 请选择10m左右大小的做测试文件
      </div>
      <Upload 
        onChange={(e) =>{ 
          setPer(0)
          upload.current?.stop()
          file.current = e.file as unknown as File
        }}
        onDrop={() =>{ 
          setPer(0)
          file.current = undefined 
        }}
        maxCount={1} 
        showUploadList={{
          showRemoveIcon: false
        }}
        beforeUpload={() => false}>
        <Button>选择文件</Button>
      </Upload>
      <Progress percent={percent} status="active" />
    </Modal>
  }

function App() {
  const [vis, setVis] = useState(false)
  return (
    <div className="App">
      <Button onClick={() => setVis(!vis)} >上传</Button>
      { vis && <FileUpload vis={vis} onClose={() => setVis(false)} /> }
    </div>
  )
}

export default App

在file-slice-upload的帮助下还能快速实现断点续传等需求...

后端

由于我不是node后端,就不班门弄斧了,我只是自己为了测试功能,实现了一下后端逻辑,勉强能用。

上述demo前后端已部署在线上, 欢迎体验, 地址

demo前后端源码: github.com/zhao-huo-lo…