大文件分片上传+断点续传(react)同时限制附件大小及视频时长

js部分代码各类框架都适用,稍作改动即可,当前项目为react+Ant Design框架代码

                 <Dragger
                    {...uploadVideoProps}
                    fileList={videoList}
                    maxCount={1}
                    customRequest={customRequest}
                    accept={".mp4,.avi"}>
                    <p className="ant-upload-drag-icon">
                      <CloudUploadOutlined style={{ color: "rgba(0,0,0,.5)" }} />
                    </p>
                    <p className="ant-upload-text">
                      <a>上传视频</a>
                    </p>
                  </Dragger>
 import SparkMD5 from 'spark-md5';
import { Upload } from "antd";
const { Dragger } = Upload;
 // 一 、第一个函数(file前置处理函数)
  const customRequest = async (params) => {
    setcount(0)
    setisOpen(false)
    setVideoList([]);
    const { file } = params // 拿到file
    const { size, type, name } = file // 解构出可能用到的几个属性
    setVideoList([{ name: name }])
    //获取文件md5值(文件的md5值是根据文件内容生成的一个单独的标示,需要安装并引入一个插件:import SparkMD5 from 'spark-md5')
    const result = await fileParse(file, 'buffer') // fileParse转换函数,将file转成buffer格式数据,
    const spark = new SparkMD5.ArrayBuffer()
    spark.append(result)
    const hash = spark.end() // 获取到的文件md5值
    // 文件切片上传
    const partSize = 20 * 1024 * 1024 // 考虑到前后端上传效率,切成10M/片,这个可根据项目实际需要自己决定
    const totalChunks = Math.ceil(file.size / partSize);//分片总数
    //2  32   100
    const share = size <= partSize ? 1 : size / partSize // 判断如果文件<=20M,不切片(或者不调用分片上传),超过按照10M每份切
    let cur = 0
    const partList = [] // 存储文件切片的数组
    for (let i = 0; i < share; i++) {
      const obj = {
        file: file.slice(cur, cur + partSize), // 调用file.slice方法进行file的切片,第一个参数从哪里开始切,第二个参数切到哪里
        id: i // 每一个切片的单独标示,后期续传时用与去除已上传部分的切片
      }
      partList.push(obj) // 把切的每一片统一放到一个数组里
      cur += partSize
    }
    console.log(totalChunks, '文件大小');
    console.log(partList, '分片总数');
    //调用断点续传
    register(partList, hash, name, totalChunks); // 此函数往下看👇
  }

  // 二 、 第二个函数(注册上传任务函数 -- 调用第一个接口,注册上传任务接口)
  const register = (partList, hash, fileName, totalChunks) => {
    // 1.调第一个接口,上传任务注册接口,参数需要将文件的md5传过去,后台专门根据此文件开启一个上传任务(一个文件对应一个上传任务)
    demoUpload({ fileMd5: hash })
      .then((res) => {
        console.log(res);
        const { fileObject, partResultList, taskStatus } = res.data // uploadId是与这个文件对应的上传任务的id标示,status是此文件的状态,如已上传部分切片||从未进行上传,这是第一次||已经上传完了还未进行合并,partResultList是一个数组,里面放着已经上传的切片的id,如果是从未进行上传,就是空数组
        const mergeParame = { hash, fileName }
        sessionStorage.setItem("isRemove", 1)
        if (taskStatus == 'MERGE') { // 1.已全部上传,未合并,直接调合并函数
          setisOpen(true)
          complete(mergeParame)
        } else if (taskStatus == 'PART') { // 2.已上传部分,根据后台返回的partResultList字段(已上传切片id的数组)筛选,去除已上传部分,把剩下的切片组成一个新数组,如果筛选出来的新数组长度是0说明切片都已上传,直接合并,否则调用上传切片函数
          const newPartList = []
          setisOpen(true)
          let newArr = partResultList.map(obj => obj.chunk);
          partList.forEach((item) => {
            if (!newArr.includes(item.id)) {
              newPartList.push(item)
            }
          })
          if (newPartList.length === 0) {
            complete(mergeParame)
          } else {
            console.log(newPartList, 'newPartList,续传数组');
            newRequestArr(newPartList, hash, mergeParame, totalChunks)
          }
        } else if (taskStatus == 'NOT_UPLOADED') { // 3.从未上传过此文件,调用上传切片函数
          setisOpen(true)
          newRequestArr(partList, hash, mergeParame, totalChunks)
        } else if (taskStatus == 'UPLOADED') {  //已合并已上传
          message.success('上传成功')
          setcount(100)
          setisOpen(false)
          setVideoList([{ ...fileObject, name: fileObject.fileName }])
        }
      })
  }

  // 三 、 第三个函数(上传切片函数 -- 调用第二个接口,上传文件切片接口)
  const newRequestArr = (partList, hash, mergeParame, totalChunks) => {
    let i = 0
    const fn = () => {
      const formData = new FormData() // 通过form将file切片传给后台
      formData.append('chunk', partList[i].id);
      formData.append('file', partList[i].file);
      formData.append('chunks', totalChunks);
      formData.append('sourceFileMd5', hash);
      partUpload(formData).then((res) => { //分片上传接口
        // console.log(res,'分片上传接口');
        if (res.code == '-1') {
          message.error('网络异常请,请重试!')
          setisOpen(false)
          sessionStorage.setItem("isRemove", 0)
          setcount(0)
          setVideoList([]);
        }
        if (sessionStorage.getItem("isRemove") == 1) {
          if (res.status == 1) { // 如果不等于-1,则会停止递归调用上传函数,至于在哪里把他置为false,看你在哪里点击触发的暂停或者删除上传函数了,自己决定
            setcount((res.data.chunk / (totalChunks - 1)) * 96)
            i += 1
            if (i < partList.length) {
              fn()
            } else {
              complete(mergeParame) // 如果是最后一个切片,则调用合并函数
            }
          } else {
            setisOpen(false)
            sessionStorage.setItem("isRemove", 0)
            setcount(0)
            setVideoList([]);
            message.error('上传失败,请重试!')
            return
          }
        }


      })
    }
    fn()
  }

  // 四 、 第四个函数(文件合并函数 -- 调用第三个接口,文件合并接口)
  const complete = (mergeParame) => {
    // console.log(mergeParame);
    let params = {
      fileMd5: mergeParame.hash,
      fileName: mergeParame.fileName
    }
    fileMerge(params).then((res) => {//合并分片接口
      const { status, data } = res
      if (res.status == 1) {
        message.success('上传成功')
        setcount(100)
        setVideoList([{ ...data, name: data.fileName }])
        setisOpen(false)
        return
      } else {
        setisOpen(false);
        setcount(0);
        setVideoList([]);
        message.error("上传失败,请重试!");
        return;
      }
    })
  }

  // 转换函数
  const fileParse = (file, type = 'base64') => {
    return new Promise((res) => {
      const fileReader = new FileReader()
      /* fileReader.readAsArrayBuffer() // 转成buffer格式数据
        fileReader.readAsBinaryString() // 转成二进制格式数据
        fileReader.readAsDataURL() // 转成base64格式数据
        解析过程是异步,所以需要调用onload事件的e.target.result获取转换后的结果 
      */
      switch (type) {
        case 'base64':
          fileReader.readAsDataURL(file)
          break
        case 'buffer':
          fileReader.readAsArrayBuffer(file)
          break
        case 'binary':
          fileReader.readAsBinaryString(file)
          break
        default:
          break
      }
      fileReader.onload = (e) => {
        res(e.target?.result)
      }
    })
  }


  // 大视频上传
  const uploadVideoProps = {
    name: "file",
    action: apiHost + "/zjkc/file/uploadFile",
    multiple: true,
    showUploadList: true,
    accept: ".mp4",
    async beforeUpload(file) {
      console.log(file);
      if (file) {
        const isLt2G = file.size / 1024 / 1024 / 1024 < 2;
        // console.log(file,'+++ 视频大小++++');
        if (!['video/mp4'].includes(file.type)) {
          message.warning('请上传.mp4格式视频');
          return Upload.LIST_IGNORE;
        }
        if (!isLt2G) {
          message.warning('视频大小不超过2GB');
          return Upload.LIST_IGNORE;
        }
        const maxDuration = 15 * 60;
        const videoDuration = await checkVideoDuration(file, maxDuration)
        if (!videoDuration) {
          message.warning('上传的视频应在15分钟内');
          return Upload.LIST_IGNORE;
        }

        return true
      }
    }
  //视频时长--用于限制上传时长
  const checkVideoDuration = (file, t) => {
    return new Promise((resolve, reject) => {
      const videoUrl = URL.createObjectURL(file)
      const videoObj = document.createElement('video')
      videoObj.preload = 'metadata'
      videoObj.src = videoUrl
      videoObj.onloadedmetadata = () => {
        URL.revokeObjectURL(videoUrl)
        let times = Math.round(videoObj.duration)
        console.log(times, 'times')
        if (parseFloat(times) > t) {
          resolve(false);
        } else {
          resolve(true);
        }
      }
    });
  }

  • 7
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个使用React函数式组件实现断点续传、秒传和分片上传的示例代码,同时还实现了上传过的分片不再上传的功能。 ``` import React, { useState } from 'react'; import axios from 'axios'; function Upload() { const [selectedFile, setSelectedFile] = useState(null); const [uploaded, setUploaded] = useState(false); const [progress, setProgress] = useState(0); const handleFileSelect = (event) => { setSelectedFile(event.target.files[0]); }; const handleUpload = async () => { if (!selectedFile) { alert('请选择文件'); return; } const fileSize = selectedFile.size; const fileName = selectedFile.name; // 下面这段代码可以判断文件是否已经上传过,如果已经上传过, // 则直接返回,不需要再次上传,实现秒传功能 const res = await axios.get(`/check-file/${fileName}`); if (res.data && res.data.isUploaded) { setUploaded(true); return; } // 计算需要分片的总数 const chunkSize = 2 * 1024 * 1024; // 每个分片大小为2MB const totalChunks = Math.ceil(fileSize / chunkSize); // 生成上传任务ID const taskId = Math.random().toString(36).slice(2); // 创建上传任务 await axios.post('/create-task', { taskId, fileName, fileSize, totalChunks, }); // 获取已上传分片列表 const uploadedChunksRes = await axios.get(`/get-uploaded-chunks/${taskId}`); const uploadedChunks = uploadedChunksRes.data; // 上传上传过的分片 let uploadedChunksCount = 0; for (let i = 0; i < totalChunks; i++) { if (uploadedChunks.includes(i)) { uploadedChunksCount += 1; continue; } const start = i * chunkSize; const end = Math.min(start + chunkSize, fileSize); const chunk = selectedFile.slice(start, end); const formData = new FormData(); formData.append('file', chunk, fileName); formData.append('taskId', taskId); formData.append('chunkIndex', i); await axios.post('/upload-chunk', formData, { onUploadProgress: (progressEvent) => { const loaded = progressEvent.loaded; const total = progressEvent.total; const percent = Math.round((loaded / total) * 100); setProgress(percent); }, }); uploadedChunksCount += 1; // 每上传10个分片,就更新一次数据库中的上传进度 if (uploadedChunksCount % 10 === 0) { await axios.post('/update-progress', { taskId, progress: Math.round((uploadedChunksCount / totalChunks) * 100), }); } } // 标记文件上传完成 await axios.post('/upload-finish', { taskId, }); setUploaded(true); }; return ( <div> <input type="file" onChange={handleFileSelect} /> <button onClick={handleUpload}>上传</button> {progress > 0 && progress < 100 && ( <p>上传进度:{progress}%</p> )} {uploaded && ( <p>上传完成</p> )} </div> ); } export default Upload; ``` 在上面的代码中,我们首先通过`useState`来管理组件状态。当用户选择了文件后,我们会通过`handleFileSelect`函数将文件信息保存到`selectedFile`状态中。然后,当用户点击上传按钮时,我们会根据文件大小文件名等信息,计算出需要分片的总数,并生成一个上传任务ID。接下来,我们会先检查文件是否已经上传过,如果已经上传过,则直接返回,实现秒传功能。如果文件没有上传过,则创建一个上传任务,并获取已经上传过的分片列表。然后,我们会循环上传每个未上传过的分片。每上传完10个分片,就会更新一次数据库中的上传进度。最后,当所有分片上传完成后,我们会标记文件上传完成,并显示上传完成的提示信息。 需要注意的是,在上面的代码中,我们使用了`axios`库来发起HTTP请求,但是具体的上传断点续传逻辑需要配合后端实现。此外,我们还需要在后端实现获取已上传分片列表的接口,并将已上传分片保存到数据库中,以便前端在上传时进行判断。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值