vue分片上传

109 阅读1分钟

因为工作需要这个东西经常要用到,记录下一个小demo


  <div class="file-label" @click="handleUpload">

    <slot>

      <div class="tips-box">

        <el-button>

          上传文件

        </el-button>

        <div class="progress" :style="{ width: uploadProgress + '%' }"></div>

      </div>

    </slot>

    <input type="file" name="file" id="fileid" ref="inputFile" class="file-input" @change="handleInputFileChange" />

  </div>

</template>

<style lang="scss" scoped>

  .file-label {

    display: inline-block;

  }

  


  .file-input {

    display: none;

  }

  


  .tips-box {

    position: relative;

    display: inline-block;

  }

  


  .progress {

    position: absolute;

    left: 0;

    bottom: 0;

    width: 0%;

    height: 3px;

    background-color: #409eff;

  }

</style>

<script>

  import axios from 'axios'

  import BMF from 'browser-md5-file'

  export default {

    name: 'MChunkUpload',

    data() {

      return {

        file: {},

        hashProgress: 0,

        uploadProgress: 0

      }

    },

    props: {

      chunkSize: {

        type: Number,

        default: 1

      },

      requestLimit: {

        type: Number,

        default: 3

      },

      customField: {

        type: Array,

        default: function () {

          return []

        }

      }

    },

    methods: {

      // 点击按钮,模拟点击input[type="file"]

      handleUpload() {

        this.$refs.inputFile.click()

      },

      // 选定文件,触发上传

      handleInputFileChange(event) {

        const file = event.target.files[0]

        const bmf = new BMF()

        bmf.md5(

          file,

          (err, md5) => {

            if (err !== null) console.error(err)

            // 上传文件

            this.chunkFileUpload(file, md5)

          },

          progress => {

            this.hashProgress = Math.ceil(progress * 100)

            this.uploadProgress = 0

            this.$emit('hashProgress', this.hashProgress)

          }

        )

        // 清空,防止文件不能重复上传

        event.target.value = ''

      },

      // 服务器检测文件是否已上传及上传过哪些分片

      async chunkFileCheck(md5, chunks) {

        return this.http().get('/file/check_file', {

          params: {

            hash: md5,

            chunks: chunks

          }

        }).then(r => {

          const result = r.data

          if (result.errno !== 0) {

            console.error(result.errmsg)

            return []

          }

          if (result.data.code === 1) {

            this.$emit('successed', result.data.data)

            this.uploadProgress = 100

            this.$emit('uploadProgress', 100)

            return [...new Array(chunks)].map((it, k) => k)

          }

          return result.data.data.uploaded

        })

      },

      // 计算文件分片数

      async chunkFileUpload(file, hash) {

        let uploadedProgress = 0

        const chunkSize = this.chunkSize * 1024 * 1024

        const fileSize = file.size

  


        // 计算文件分片总数

        const chunks = Math.ceil(fileSize / chunkSize)

  


        // 分片组大小

        const groupSize = this.requestLimit

  


        // 分片索引

        let chunksArray = [...new Array(chunks)].map((it, k) => k)

  


        // 检查文件是否上传过

        const uploaded = await this.chunkFileCheck(hash, chunks)

        chunksArray = chunksArray.filter((it, k) => uploaded.includes(it) === false)

        if (chunksArray.length === 0) {

          return false

        }

        // 分片索引分组,解决浏览器同域名最大并发请求数问题

        const chunksGroups = [...new Array(Math.ceil(chunksArray.length / groupSize))].map((it, k) => chunksArray

          .slice(k * groupSize, k * groupSize + groupSize))

        for (let i = 0; i < chunksGroups.length; i++) {

          let computed = 0

          // 利用promise控制并发请求连接数

          await new Promise((resolve, reject) => {

            const group = chunksGroups[i]

            for (let k = 0; k < group.length; k++) {

              const it = group[k]

              // 分片文件

              const sPos = chunkSize * it

              const chunk = file.slice(sPos, sPos + chunkSize)

              const formData = new FormData()

              // blob组装file对象

              formData.append('file', new File([chunk], file.name, {

                type: file.type

              }), file.name)

              formData.append('hash', hash)

              formData.append('chunks', chunks)

              formData.append('chunk', it)

              // 自定义字段

              if (this.customField.length > 0) {

                this.customField.forEach(field => {

                  formData.append(Object.keys(field)[0], Object.values(field)[0])

                })

              }

              this.http().post('/file/upload_file', formData).then(r => {

                computed += 1

                if (computed === group.length) {

                  resolve('success')

                }

                // 计算上传进度

                this.uploadProgress = Math.ceil((uploadedProgress += 1) / chunks * 100)

                this.$emit('uploadProgress', this.uploadProgress)

  


                const result = r.data

                // 失败,请取消请求

                if (result.errno !== 0) {

                  console.error(result.errmsg)

                  return false

                }

                // 上传完成

                if (result.data.code === 1) {

                  this.$emit('successed', result.data.data)

                  this.uploadProgress = 100

                  this.$emit('uploadProgress', 100)

                  return false

                }

              }).catch(e => {

                reject(e)

              })

            }

          })

        }

      },

      // 请求配置项

      http() {

        const baseURL = process.env.VUE_APP_API_BASEURL

        const apiVersion = process.env.VUE_APP_API_VERSION.replace(/\./g, '_')

        return axios.create({

          baseURL: baseURL + apiVersion + '/',

          timeout: 0,

          withCredentials: true

        })

      }

    }

  }

</script>