vue+大文件断点续传

根据部门的业务需求,需要在网络状态不良的情况下上传很大的文件(1G+)。
其中会遇到的问题:
1,文件过大,超出服务端的请求大小限制;
2,请求时间过长,请求超时;
3,传输中断,必须重新上传导致前功尽弃。
解决方案实现思路,拿到文件,保存文件唯一性标识,切割文件、分片上传、文件MD5验证、断点续传、手动重试上传。

 

鉴于过往有使用过webupload文件上传组件的经验,于是此次采用的是Plupload作为替换。Plupload是一款由著名的web编辑器TinyMCE团队开发的上传组件,简单易用且功能强大。

Plupload有以下功能和特点

  1. 拥有多种上传方式:HTML5、flash、silverlight以及传统的<input type=”file” />。Plupload会自动侦测当前的环境,选择最合适的上传方式,并且会优先使用HTML5的方式。所以你完全不用去操心当前的浏览器支持哪些上传方式,Plupload会自动为你选择最合适的方式。
  2. 支持以拖拽的方式来选取要上传的文件
  3. 支持在前端压缩图片,即在图片文件还未上传之前就对它进行压缩
  4. 可以直接读取原生的文件数据,这样的好处就是例如可以在图片文件还未上传之前就能把它显示在页面上预览
  5. 支持把大文件切割成小片进行上传,因为有些浏览器对很大的文件比如几G的一些文件无法上传。

环境

  • vue2.x
  • webpack3.x
  • axios

代码

npm安装plupload,文件引入组件,

1

2

3

4

5

6

7

8

9

10

11

12

<uploader browse_button="upload_area"

                :max_retries="3"

                :url="action"

                :headers="headers"

                chunk_size="10MB"

                drop_element="upload_area"

                @disableBrowse="!loading"

                :BeforeUpload="beforeUpload"

                :ChunkUploaded="chunkUploaded"

                :FilesAdded="filesAdded"

                :StateChanged="stateChanged"

                @inputUploader="inputUploader" />

  

初始化方法filesAdded(),每次上传前清空队列的其他文件,保证上传的一致性。其次对文件类型进行判断过滤fileType(),文件进入时进行总md5一次fileMd5(),然后进入文件分片chunkCheckStatus(),每个分片都要进行md5并与后台进行校验fileMd5(),确保文件在中断后继续上传的准确性。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

filesAdded (up, files) {

      // 删除上传队列中其他文件,只保留最近上传的文件

      let fileLen = files.length, that = this

      if (fileLen > 1) {

        files = files.splice(0, fileLen - 1)// 清空上传队列

      }

      files.forEach((f) => {

        f.status = -1

        that.dataForm.file = f

        that.fileType(f.getNative())

        if (that.loading) {

          that.computeStatus = true

          that.progress = 0

          // 文件分片

          let chunkSize = 2097152, // Read in chunks of 2MB

            chunks = Math.ceil(f.size / chunkSize)

          that.fileMd5(f.getNative(), (e, md5) => {

            that.dataForm.md5 = md5

            if (that.loading == true) {

              that.count = 0

              that.chunkCheckStatus(md5, that.dataForm.fileName, (uploader, dataList) => {

                that.uploading = uploader

                if (that.uploading == true) {

                  for (let chunk = 1; chunk <= chunks; chunk++) {

                    that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => {

                      that.fileMd5(chunkFile, (e, blockMd5) => {

                        that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5)

                      })

                    })

                  }

                else {

                  // 去重

                  that.progress = 0

                  for (let chunk = 1; chunk <= chunks; chunk++) {

                    let status = 0

                    dataList.some((item) => {

                      if (item.chunk == chunk) {

                        status = 1

                        return false

                      }

                    })

                    if (status == 0) {

                      that.fileChunkFile(f.getNative(), chunk, (e, chunkFile) => {

                        that.fileMd5(chunkFile, (e, blockMd5) => {

                          that.PostFile(up, chunkFile, chunk, chunks, md5, blockMd5)

                        })

                      })

                    }

                  }

                }

              })

            }

          })

        }

      })

    }

  

文件md5方法,这里使用了SparkMD5,import SparkMD5 from 'spark-md5'

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

fileMd5 (file, callback) {

      let that = this

      var blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,

        file = file,

        chunkSize = 2097152, // Read in chunks of 2MB

        chunks = Math.ceil(file.size / chunkSize),

        currentChunk = 0,

        spark = new SparkMD5.ArrayBuffer(),

        fileReader = new FileReader()

      fileReader.onload = function (e) {

        console.log('read chunk nr', currentChunk + 1, 'of', chunks)

        spark.append(e.target.result) // Append array buffer

        currentChunk++

        if (currentChunk < chunks) {

          loadNext()

        else {

          let blockMd5 = ''

          blockMd5 = spark.end()

          callback(null, blockMd5)

        }

      }

      fileReader.onerror = function () {

        callback('oops, something went wrong.')

      }

      function loadNext () {

        var start = currentChunk * chunkSize,

          end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize

        fileReader.readAsArrayBuffer(blobSlice.call(file, start, end))

      }

      loadNext()

    }

  

文件分片上传方法,验证总分片信息后,把每个分片进行md5加密并上传校验,这里有写进度条相关的控制,不一一展示

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

chunkCheckStatus (md5, fileName, callback) {

      this.$http({

        url: this.$http.adornUrl('/biz/upload/getFileBlockStatus'),

        method: 'get',

        params: this.$http.adornParams({

          md5: md5,

          fileName: fileName

        })

      }).then(({ data }) => {

        if (data && data.code === 0) {

          if (data.list != null) {

            this.uploading = false

            this.chunkCheckData = []

            data.list.map((item, index) => {

              if (item.isUpload == true) {

                this.count++

                this.chunkCheckData.push(item)

              }

            })

            callback(this.uploading, this.chunkCheckData)

            return

          }

          this.uploading = true

          callback(this.uploading)

        else {

          this.$message.error(data.msg)

          this.loading = false

          this.computeStatus = false

          return false

        }

      })

    }

  

详细的配置信息可以参考我写的这篇文章:http://blog.ncmem.com/wordpress/2019/08/09/vue%e5%a4%a7%e6%96%87%e4%bb%b6%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0/ 

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
断点续传是指在文件上传过程中,如果上传中断,可以从上传中断的地方开始继续上传,避免重新上传整个文件。在使用 Vue、Element、Axios 和 Qs 实现断点续传时,可以按照以下步骤进行操作: 1. 前端页面部分 在前端页面中,可以使用 Element 组件库来实现上传文件的功能。例如可以使用 el-upload 组件来上传文件,代码如下: ``` <template> <el-upload class="upload-demo" action="yourUploadUrl" :auto-upload="false" :on-change="handleChange" :data="uploadData" :file-list="fileList" :http-request="uploadFile" > <el-button slot="trigger" size="small" type="primary">选取文件</el-button> <el-button size="small" type="success" :disabled="!fileList.length" @click="submitUpload">上传到服务器</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </template> ``` 在模板中,el-upload 组件的属性值中,我们需要设置以下内容: - `action` 属性:设置上传文件的地址。 - `auto-upload` 属性:设置是否自动上传。 - `on-change` 属性:设置文件上传状态改变时的回调函数。 - `data` 属性:设置上传文件时需要携带的参数。 - `file-list` 属性:设置已经上传的文件列表。 - `http-request` 属性:设置文件上传的函数,我们在这里定义上传文件的逻辑。 接下来,我们需要在 data 函数中定义 fileList 和 uploadData 对象,代码如下: ``` data() { return { fileList: [], uploadData: { chunkSize: 1024 * 1024, // 文件切片大小 chunks: 0, // 切片总数 chunkIndex: 0, // 当前切片编号 fileSize: 0, // 文件大小 fileName: '', // 文件名 fileMd5: '', // 文件md5值 uploadUrl: '', // 上传接口地址 }, } }, ``` 在这里,我们需要定义上传文件时需要携带的参数。其中,chunkSize 表示每个切片的大小,chunks 表示总共需要切片的数量,chunkIndex 表示当前上传的切片编号,fileSize 表示文件的大小,fileName 表示文件的名称,fileMd5 表示文件的 md5 值,uploadUrl 表示上传接口的地址。 然后,我们需要在 methods 函数中定义 handleChange、uploadFile 和 submitUpload 函数。其中,handleChange 函数用来监听文件上传状态的改变,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。代码如下: ``` methods: { handleChange(file) { this.fileList = [file] this.uploadData.fileSize = file.size this.uploadData.fileName = file.name this.uploadData.fileMd5 = 'md5' this.uploadData.uploadUrl = 'uploadUrl' this.uploadData.chunks = Math.ceil(file.size / this.uploadData.chunkSize) }, async uploadFile(file) { const index = this.uploadData.chunkIndex++ const startByte = index * this.uploadData.chunkSize const endByte = Math.min((index + 1) * this.uploadData.chunkSize, this.uploadData.fileSize) const chunkFile = file.slice(startByte, endByte) const formData = new FormData() formData.append('file', chunkFile) formData.append('chunkIndex', index) formData.append('fileName', this.uploadData.fileName) formData.append('fileMd5', this.uploadData.fileMd5) const { data } = await this.$axios.post(this.uploadData.uploadUrl, formData, { headers: { 'Content-Type': 'multipart/form-data' } }) if (index === this.uploadData.chunks - 1) { console.log('upload success') } else { this.uploadFile(file) } }, async submitUpload() { if (!this.fileList.length) return this.uploadData.chunkIndex = 0 await this.uploadFile(this.fileList[0].raw) } }, ``` 在这里,handleChange 函数将上传文件的基本信息保存到 uploadData 对象中,uploadFile 函数用来上传文件,submitUpload 函数用来提交上传请求。其中,uploadFile 函数是核心部分,它通过循环上传每个切片,如果上传成功,则继续上传下一个切片,否则重新上传当前切片。 2. 后端接口部分 在后端接口中,需要实现文件上传的逻辑。由于是断点续传,所以需要实现上传切片、合并切片的功能。例如可以使用 Node.js 和 Express 框架来实现上传文件的功能,代码如下: ``` const express = require('express') const multer = require('multer') const path = require('path') const fs = require('fs') const app = express() app.use(express.static('public')) app.post('/upload', multer({ dest: 'uploads/' }).single('file'), (req, res) => { const { fileName, fileMd5, chunkIndex } = req.body const chunkFile = req.file const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, `${fileName}-${chunkIndex}`) if (!fs.existsSync(chunkDir)) { fs.mkdirSync(chunkDir) } fs.renameSync(chunkFile.path, filePath) res.json({ code: 0, message: '上传成功', }) }) app.post('/merge', (req, res) => { const { fileName, fileMd5, chunks } = req.body const chunkDir = path.join(__dirname, `./uploads/${fileMd5}`) const filePath = path.join(chunkDir, fileName) const chunkPaths = [] for (let i = 0; i < chunks; i++) { chunkPaths.push(path.join(chunkDir, `${fileName}-${i}`)) } let ws = fs.createWriteStream(filePath) chunkPaths.forEach((chunkPath) => { let rs = fs.createReadStream(chunkPath) rs.on('end', () => { fs.unlinkSync(chunkPath) }) rs.pipe(ws, { end: false }) }) ws.on('close', () => { fs.rmdirSync(chunkDir) res.json({ code: 0, message: '上传成功', }) }) }) app.listen(3000, () => { console.log('server is running at http://localhost:3000') }) ``` 在这里,我们实现了两个接口,一个是上传切片的接口 `/upload`,一个是合并切片的接口 `/merge`。其中,上传切片的接口会将上传的切片保存到指定的目录下,并返回上传成功的消息;合并切片的接口会将上传的所有切片合并成一个完整的文件,并删除上传的切片。注意,在合并切片的过程中,需要使用 fs.createWriteStream 和 fs.createReadStream 将切片合并成一个完整的文件。 以上就是使用 Vue、Element、Axios 和 Qs 实现文件上传的断点续传的完整流程。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值