拖拽表单设计器-大文件分片断点上传、拖拽选择文件、切片上传、断点续传、上传并发数控制

效果展示

在这里插入图片描述

技术关键词

前端:vue3+element-plus+axios
后端:node+express+multer/php

完整代码

github完整代码

拖拽选择文件

拖拽上传是利用HTML5特性dragover实现拖拽上传,有很多现成的例子

<label class="split-upload-file" @dragover="fileDragOver" @drop="fileDrop">
    <input
      ref="inputEl"
      style="display: none"
      type="file"
      multiple="multiple"
      @change="onFileChange"
    />
    <i class="icon-upload"></i>
    <span>将文件拖放到此处以上传或<b>选择文件</b></span>
  </label>
const tempFileList=ref([])
const fileDragOver = e => {
    e.preventDefault()
  }
  const fileDrop = e => {
    e.preventDefault()
    //const file=e.dataTransfer.files
    onFileChange(e.dataTransfer.files,'drag')
  }
  const onFileChange= async (evt,type)=>{
    let file = evt
    if (type !== 'drag') {
      file = evt.target && evt.target.files
    }
    for (let i = 0; i < file.length; i++) {
      const data = file[i]
      let src = ''
      // 先将获取到的文件暂存起来,可实现选择文件后自动上传或手动点击击再上传
      tempFileList.value.push({
        size: data.size, // 大小
        progress: 0, // 上传进度
        speed: '', // 上传速度
        remainingTime: '', //剩余时间预估
        status: 0, //上传状态
        name: data.name,
        src: src, // 预览用的src
        type: data.type,
        file: data,
        source: null // 用于取消上传
      })
    }
    await confirmUpload() // 进入按指定的并发数上传文件
  }

任务并发控制

限定每次请求只能进行固定次数的异步任务,每请求成功一个则新增一个,确保并发的数量固定。

const maxParallelUploads = 3 //同时上传并发数
const confirmUpload = async () => {
  const queue = tempFileList.value.filter(
    (item) => item.status === 0
  ) // 创建一个文件队列
  const promises = []

  for (const file of queue) {
    // 如果当前运行的任务数达到最大并行上传数,则等待一个任务完成
    while (promises.length >= maxParallelUploads) {
      await Promise.race(promises) // 等待最先完成的一个任务
      promises.shift() // 移除已完成的任务
    }
    // 添加新的上传任务到promises数组
    promises.push(axiosUpload(file))
  }
  // 等待剩余的任务完成
  await Promise.all(promises)
}

分块上传

思路

分块上传就是前端将大文件拆成一个个小的文件,发送给后端。后端接收完所有分片小文件后将后片小文件合并成新的文件。

在实现分片上传前,需思考和实现下面问题:

  • 如何获取用户选择的文件
  • 获取到的大文件如何进行分片(重点)
  • 怎么区分多个不同的大文件
  • 如何将每个小分片的数据发送给后端
  • 如何通知后端合并文件
const chunkSize = 20 * 1024 * 1024 // 每片大小
let cancelToken
const axiosUpload=async(currentFile)=>{
  const file=currentFile.file
  const chunkCount = Math.ceil(file.size / chunkSize) // 总片数
  const fileMd5 = await getFileMD5(file, chunkSize, chunkCount)//计算md5
  const source = axios.CancelToken.source()
  cancelToken = source.token
  currentFile.source = source
  for (let i = 0; i < chunkCount; i++) {
    const { chunk } = getChunkInfo(file, i, chunkSize)//获取当前片
    const params = {
      chunkNumber: i + 1,
      chunkSize: chunkSize, // 每片限制
      currentChunkSize: chunk.size, // 当前分片大小
      totalSize: file.size, // 文件总大小
      identifier: fileMd5, // md5值
      filename: file.name, // 文件名
      totalChunks: chunkCount, // 总片数
      file: chunk // 每片文件
    }
    await uploadChunk(params, currentFile)
  }
}
const uploadChunk = (params, currentFile) => {
  return new Promise((resolve, reject) => {
    const formData = new FormData()
    for (const i in params) {
      formData.append(i, params[i])
    }
    const config = {
      headers: { 'Content-Type': 'multipart/form-data' },
      timeout: 0, // `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
      onUploadProgress: (progressEvent: any) => {
        //上传进度相关信息
      },
      cancelToken
    }
    axios.post('/api/upload',formData, config)
      .then(()=>{})
      .catch(()=>{})
  })
}
//根据每片大小获取每片的内容
const getChunkInfo = (file, currentChunk, chunkSize) => {
  const start = currentChunk * chunkSize
  const end = Math.min(file.size, start + chunkSize)
  const chunk = file.slice(start, end)
  return { chunk }
}

秒传

实现秒传就是在上传之前,先调取后端接口,根据md5值判断文件是否已存在。使用SparkMD5进行md5计算:

import SparkMD5 from 'spark-md5'
const getFileMD5 = (file, chunkSize, chunkCount) => {
  return new Promise((resolve, reject) => {
    let currentChunk = 0
    const startTime = new Date().getTime()
    const spark = new SparkMD5.ArrayBuffer()
    const fileReader = new FileReader()
    loadNextChunk()
    fileReader.onload = function (e) {
      spark.append(e.target?.result)
      if (currentChunk < chunkCount) {
        currentChunk++
        loadNextChunk()
      } else {
        const md5 = spark.end()
        console.log(
          `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms`
        )
        resolve(md5)
      }
    }
    fileReader.onerror = function () {
      reject('文件读取错误')
    }

    function loadNextChunk() {
      const { chunk } = getChunkInfo(file, currentChunk, chunkSize)
      fileReader.readAsArrayBuffer(chunk)
    }
  })
}

后端

后端示例仅供演示

<?php
//PHP服务端接收示例代码
header("Access-Control-Allow-Origin: *");
$file = $_FILES['file'];
$chunkNumber = $_POST['chunkNumber'];
$totalChunks = $_POST['totalChunks'];
$filename = $_POST['filename'];

$public_dir = 'upload';
$dir = '/';
$all_dir = $public_dir . $dir;
  if (!is_dir($all_dir)) mkdir($all_dir, 0777, true);
       $saveFileName = $all_dir . '/' . $filename . '__' . $chunkNumber;
        move_uploaded_file($file['tmp_name'], $saveFileName);
        $res['code'] = 0;
        $res['msg'] = 'error';
        $res['file_path'] = '';
        if ($chunkNumber === $totalChunks) {
            $blob = '';
            for ($i = 1; $i <= $totalChunks; $i++) {
                $blob .= file_get_contents($all_dir . '/' . $filename . '__' . $i);
            }
            file_put_contents($all_dir . '/' . $filename, $blob);
            for ($i = 1; $i <= $totalChunks; $i++) {
                @unlink($all_dir . '/' . $filename . '__' . $i);
            }
            if (file_exists($all_dir . '/' . $filename)) {
                $res['code'] = 2;
                $res['msg'] = 'success';
                $res['file_path'] = $dir . '/' . $filename;
            }
       } else {
            if (file_exists($all_dir . '/' . $filename . '__' . $chunkNumber)) {
                $res['code'] = 1;
                $res['msg'] = 'waiting';
                $res['file_path'] = '';
            }
      }
      echo json_encode($res);
?>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值