简易文件切片上传

简易文件切片上传

  • axios 并发控制
  • 文件切片
  • 文件唯一性校验spark-md5
  • 拖拽上传
<template>
  <div class="b-upload">
    <input
      v-show="false"
      type="file"
      ref="upload"
      :multiple="multiple"
      @change="handleChange"
    />
    <div>
      <slot>
        <!-- 默认内容 -->
        <div
          :class="['drag-box', isEnter ? 'is-enter' : '']"
          v-if="drag"
          @click="handleUpload"
          @drop.prevent="handleDrop"
          @dragover.prevent="handleDragOver"
          @dragleave="handleDragLeave"
        ></div>
        <BButton v-else>
          文件上传
        </BButton>
      </slot>
      &nbsp; &nbsp; &nbsp;
      <BButton @click.native.stop="handleBtn">{{
        isUpload ? '暂停' : '继续'
      }}</BButton>
      <BButton v-if="isErrorReUpload" @click="checkIsError">错误重传</BButton>
    </div>
  </div>
</template>

<script>
import BButton from '../../b-button'
import axios from 'axios'
import SparkMd5 from 'spark-md5'
export default {
  name: 'BUpload',
  components: { BButton },
  props: {
    action: {
      type: String,
      default: '',
    },
    headers: {
      type: Object,
      default: () => ({}),
    },
    data: {
      type: Object,
      default: () => ({}),
    },
    multiple: {
      type: Boolean,
      default: true,
    },
    autoUpload: {
      type: Boolean,
      default: true,
    },
    showList: {
      type: Boolean,
      default: true,
    },
    type: {
      type: String,
      default: 'parallel', // parallel 并行 serial 串行
    },
    drag: {
      type: Boolean,
      default: true,
    },
    showStop: {
      type: Boolean,
      default: false,
    },
  },
  data: () => ({
    file: {},
    complete: 0,
    source: {},
    isEnter: false,
    isUpload: true,
    isContinue: false,
    isError: false,
    isErrorReUpload: false,
    errorRequest: [],
    total: 0,
    current: 0,
    chunkLis: [],
    requestList: [],
    hash: '',
    suffix: '',
    img: '',
    // type: 'parallel', // parallel 并行 serial 串行
    // 并发部分
    max: 5,
    currentReqNumber: 0,
    startIndex: 0,
    blockQueue: [],
    // 每个请求的退出状态
    sources: [],
    // 每个请求的上传状态
    completes: [],
    // 成功的请求
    successReq: [],
    //已经处理的请求
    doneReq: 0,
    reqTotal: 0,

    // 时间统计
    timer: 0,
  }),
  computed: {
    process() {
      return parseInt((this.current / this.total) * 100 || 0) + '%'
    },
  },
  mounted() {
    // 给默认插槽添加点击事件
    // document.body.addEventListener
    // console.log(this.$slots.default[0].elm)
    if (this.$slots.default) {
      this.$slots.default[0].elm.addEventListener('click', this.handleUpload)
    }
  },
  methods: {
    // 点击上传
    handleUpload() {
      this.$refs.upload.click()
    },
    // 拖拽上传核心
    handleDrop(e) {
      if (this.isEnter) {
        this.file = e.dataTransfer.files[0]
        // 获取到文件,进行上传
        this.handleChange()
      }
      this.file = e.dataTransfer.files[0]
    },
    // 进入之后一直执行...
    handleDragEnter() {
      console.log('handleDragEnter')
    },
    // 推拽进入
    handleDragOver() {
      this.isEnter = true
      console.log('handleDragOver')
    },
    // 拖拽离开
    handleDragLeave() {
      this.isEnter = false
      console.log('handleDragLeave')
    },
    // 单文件不分段上传
    async handleChangeSingle() {
      // 文件改变事件
      this.$emit('change', this.$refs.upload.files)
      // 存在上传地址,确认自动上传
      if (this.action && this.autoUpload) {
        this.source = axios.CancelToken.source()

        const forms = new FormData()

        // 多文件上传需要后端配合
        for (let f of this.$refs.upload.files) {
          forms.append('file', f)
        }
        // 附加数据
        for (let i in this.data) {
          forms.append(i, this.data[i])
        }
        try {
          // 413 ,请求体太大,服务器进行请求体限制
          const result = await axios({
            method: 'post',
            url: this.action,
            headers: this.headers,
            data: forms,
            // 退出标记
            cancelToken: this.source.token,
            // 进度
            onUploadProgress: (progressEvent) => {
              this.complete =
                ((progressEvent.loaded / progressEvent.total) * 100) | 0
              this.$emit('progress', this.complete)
            },
          })
          this.$refs.upload.value = ''
          this.$emit('success', result.data)
        } catch (error) {
          this.$emit('error', error)
        }
      }
    },
    // 取消上传
    handleStop() {
      this.source.cancel('取消上传')
      this.source = axios.CancelToken.source()
    },
    // TODO 分段上传,断点续传,暂停
    async handleChange() {
      // 格式判断
      // 文件切片 固定数量,固定大小
      // 进行处理(后台所需数据格式)
      // 并行上传,断点续传
      // 记录失败部分,进行重新上传
      const file = this.$refs.upload.files[0] || this.file
      if (file) {
        // 重置上次的请求

        this.current = 0
        this.completes = []
        this.timer = Date.now()
        this.$emit('change', this.$refs.upload.files)
        const fBuffer = await this.fileParse(file, 'buffer')
        const spark = new SparkMd5.ArrayBuffer()
        spark.append(fBuffer)
        this.hash = spark.end()

        this.suffix = /.([0-9a-zA-Z]+)$/.exec(file.name)[0]

        this.chunkList = this.createFileChunk(file, this.hash, this.suffix)
        this.createRequestList()
        await this.sendRequest()
        // this.$refs.upload.value = ''
      }
    },

    // 暂停|继续请求
    async handleBtn() {
      this.isUpload = !this.isUpload
      // 取消请求
      if (!this.isUpload) {
        console.log('停止上传')

        // 无法进行取消
        this.requestList.map((req) => {
          if (req.status === false) {
            console.log('req: ', req.index)
            console.log('this.sources[req.index]: ', this.sources[req.index])
            this.sources[req.index].cancel('取消请求')
          }
        })
      }
      // 重新设置请求状态
      if (this.isUpload) {
        this.requestList.map((req) => {
          if (req.status === false) {
            this.sources[req.index] = axios.CancelToken
          }
        })
      }

      if (this.isUpload && this.type === 'serial') {
        console.log('重新开始-串行')
        this.isError = true
        await this.checkIsError()
      }
      if (this.isUpload && this.type === 'parallel') {
        // 接着上一次请求进行执行
        console.log('重新开始-并行')
        this.isError = true
        // this.startIndex = this.current

        // 去处理错误的请求
        await this.checkIsError()
      }
    },
    // TODO 错误处理
    async checkIsError() {
      this.isErrorReUpload = false
      this.isUpload = true
      const errorList = this.requestList.filter((req) => req.status === false)
      this.reqTotal = errorList.length
      // 重置请求状态
      this.requestList.map((req) => {
        if (req.status === false) {
          this.sources[req.index] = axios.CancelToken
        }
      })
      if (errorList.length > 0 && this.type === 'serial') {
        console.log('串行错误处理')
        this.doneReq = 0

        for (let req of errorList) {
          // 断点续传
          if (!this.isUpload) return
          if (this.current >= this.requestList.length) {
            this.current = this.requestList.length
            // 全部传完了
            return
          }
          await req.request()
        }
      } else if (errorList.length > 0 && this.type === 'parallel') {
        console.log('并行错误处理')
        // 失败的总条数
        this.doneReq = 0
        this.blockQueue = []
        // this.requestList = errorList
        for (let i = 0; i < errorList.length; i++) {
          if (this.currentReqNumber >= this.max) {
            await new Promise((res) => this.blockQueue.push(res))
          }
          errorList[i].request()
        }
      } else {
        // 没有错误去合并
        this.fileMerge()
      }

      // this.sendRequest()
    },
    // 合并文件请求
    async fileMerge() {
      // 请求合并
      const { data } = await axios({
        url: 'http://localhost:5000/assets/fileMerge',
        method: 'post',
        data: {
          fileName: `${this.data.filePath}/${this.hash}${this.suffix}`,
        },
      })
      this.$emit('success', data)
      // this.img = data.data.filePath
      this.resetFile()
      console.log('upload', Date.now() - this.timer)
    },
    // 发送请求,串行(serial)|并行(parallel)
    async sendRequest() {
      // 串行请求
      this.reqTotal = this.requestList.length
      if (this.type === 'serial') {
        console.log('sendRequest串行请求')
        const send = async () => {
          // 断点续传
          if (!this.isUpload) return
          if (this.current >= this.requestList.length) {
            this.current = this.requestList.length
            // 全部传完了
            return
          }
          await this.requestList[this.current].request()
          send()
        }
        send()
      } else if (this.max > 0 && this.type === 'parallel') {
        console.log('sendRequest并行请求')
        // 并行请求
        for (let i = 0; i < this.requestList.length; i++) {
          if (this.currentReqNumber >= this.max) {
            await new Promise((res) => this.blockQueue.push(res))
          }
          this.requestList[i].request()
          // console.log('this.requestList[i]: ', this.requestList[i].index)
        }
      }
    },
    // 将切片的数据批量处理为请求数组
    createRequestList() {
      // 请求处理
      this.chunkList.map((params, index) => {
        // 取消集合
        this.sources[index] = axios.CancelToken.source()
        const request = async () => {
          // 数据处理
          const fd = new FormData()
          for (let i in params) {
            fd.append(i, params[i])
          }
          try {
            // 当前请求数增加
            this.currentReqNumber++
            const result = await axios({
              url: this.action,
              method: 'post',
              data: fd,
              headers: this.headers,
              cancelToken: this.sources[index].token,
              onUploadProgress: (p) => {
                // TODO 当前index进度
                this.completes[index] = ((p.loaded / p.total) * 100) | 0
                this.requestList[index].process =
                  ((p.loaded / p.total) * 100) | 0

                this.$emit('progress', [this.process, this.completes])
              },
            })
            // TODO 秒传直接100
            // this.completes[index] = 100
            // this.requestList[index].process = 100
            // this.$emit('progress', this.completes)
            // 进度
            this.current++
            // 完成度
            this.doneReq++
            // 上传完成的去除
            this.chunkList.splice(index, 1)
            this.requestList[index].status = true

            // 所有请求全部完成
            if (this.current >= this.total) {
              //  全部完成 没有错误
              // console.log('全部完成--没有错误--')
              this.isError = false
              this.isErrorReUpload = false
              // TODO 测试后端
              this.fileMerge()
              this.$emit('progress', [this.process, this.completes])
            }
            return result
          } catch (error) {
            console.log('error: ', '开始出错了', error)
            this.doneReq++
            this.isUpload = false
            this.isErrorReUpload = true
          } finally {
            // 并发处理
            if (this.type === 'parallel') {
              this.currentReqNumber--
              if (this.blockQueue.length > 0) {
                this.blockQueue[0]()
                this.blockQueue.shift()
              }
            }

            // 全部完成
            // TODO this.requestList.length 不应该固定 改为请求总量reqTotal
            if (this.doneReq >= this.reqTotal && this.isError) {
              console.log('全部完成--去处理错误--')
              this.isError = false
              this.isErrorReUpload = true
              // 自动进入一次就可以了,失败手动上传
              this.checkIsError()
              this.$emit('progress', [this.process, this.completes])
            }
          }
        }
        this.requestList.push({ index, status: false, process: 0, request })
      })
    },
    // 文件切片 type size number
    createFileChunk(file, hash, suffix, size = 1024 * 1024) {
      // size 默认 1024 一般为5m
      // file 文件
      // hash 根据文件内容生成唯一标志
      // suffix 文件后缀
      const chunkList = []
      const total = Math.ceil(file.size / size)
      this.total = total
      let index = 0
      for (let i = 0; i < total; i++) {
        index = i * size
        let chunk = file.slice(index, (i + 1) * size)
        // 根据后台所需参数进行拼接
        chunkList.push({
          index: i,
          chunk,
          hash,
          fileName: `${this.data.filePath}/${hash}/${hash}_${i}${suffix}`,
        })
      }
      return chunkList
    },
    // 文件解析 base64 buffer
    fileParse(file, type = 'base64') {
      return new Promise((resolve) => {
        const fileRead = new FileReader()
        if (type === 'base64') {
          fileRead.readAsDataURL(file)
        } else if (type === 'buffer') {
          fileRead.readAsArrayBuffer(file)
        }
        fileRead.onload = (e) => {
          resolve(e.target.result)
        }
      })
    },
    // 重置
    resetFile() {
      this.complete = 0
      this.source = {}
      this.isUpload = true
      this.errorRequest = []
      // this.total = 0
      // this.current = 0
      this.chunkLis = []
      this.requestList = []
      this.hash = ''
      this.suffix = ''
      this.img = ''
      // parallel 并行 serial 串行
      // this.type = 'parallel'
      // 并发部分
      this.max = 5
      this.currentReqNumber = 0
      this.startIndex = 0
      this.blockQueue = []
      // 每个请求的退出状态
      this.sources = []
      // 每个请求的上传状态
      // this.completes = []
      // 成功的请求
      this.successReq = []
      // 已经处理的
      this.doneReq = 0

      // 重置文件
      this.$refs.upload.value = ''
    },
  },
}
</script>
<style scoped>
.b-upload {
}
.drag-box {
  width: 300px;
  height: 300px;
  border: 4px dashed #ccc;
  border-radius: 20px;
  transition: all 0.3s;
}
.is-enter {
  border-color: #409eff;
}
.chunk-box {
  margin: 0;
  padding: 0;
  list-style: none;
  width: 200px;
  display: flex;
  flex-wrap: wrap;
}
.chunk-box li {
  position: relative;
  width: 20px;
  height: 20px;
  background-color: #f0f0f0;
}
.chunk-box li div {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 20px;
  height: 0px;
  transition: all 0.3s;
  background-color: greenyellow;
}
</style>
··

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值