Vue 大文件上传和断点续传的实现

文件上传的 2 套方案

基于文件流(form-data)

element-ui 框架的上传组件,就是默认基于文件流的。

  • 数据格式:form-data;
  • 传递的数据: file 文件流信息;filename 文件名字

客户端把文件转换为 base64

通过 fileRead.readAsDataURL(file) 转为 base64 字符串后,要用 encodeURIComponent 编译再发送, 发送的数据经由 qs.stringify 处理,请求头添加 "Content-Type": "application/x-www-form-urlencoded"

大文件上传

首先借助 element-ui 搭建下页面。因为要自定义一个上传的实现,所以 el-upload 组件的 auto-upload 要设定为 false;action 为必选参数,此处可以不填值。

<template>
  <div id="app">
    <!-- 上传组件 -->
    <el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
    </el-upload>

    <!-- 进度显示 -->
    <div class="progress-box">
      <span>上传进度:{{ percent.toFixed() }}%</span>
      <el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
    </div>

    <!-- 展示上传成功的视频 -->
    <div v-if="videoUrl">
      <video :src="videoUrl" controls />
    </div>
  </div>
</template>

获取到文件对象并转成 ArrayBuffer 对象

转成 ArrayBuffer 是因为后面要用 SparkMD5 这个库生成 hash 值,对文件进行命名

async handleChange(file) {
  const fileObj = file.raw
  try{
    const buffer = await this.fileToBuffer(fileObj)
    console.log(buffer)
  }catch(e){
    console.log(e)
  }
}

打印 buffer 结果如下图

注意:before-upload 函数和 on-change 函数的参数都有 file,但是 on-change 中的 file 不是 File 对象,要获取File 对象需要通过 file.raw。 这里用到 FileReader 类将 File 对象转 ArrayBuffer 对象,因为是异步过程,所以用 Promise 封装:

// 将 File 对象转为 ArrayBuffer
fileToBuffer(file) {
  return new Promise((resolve, reject) => {
    const fr = new FileReader()
    fr.onload = e => {
      resolve(e.target.result)
    }
    fr.readAsArrayBuffer(file)
    fr.onerror = () => {
      reject(new Error('转换文件格式发生错误'))
    }
  })
}

创建切片

可以通过固定大小或是固定数量把一个文件分成好几个部分,为了避免由于 js 使用的 IEEE754 二进制浮点数算术标准可能导致的误差,我决定用固定大小的方式对文件进行切割,设定每个切片的大小为 2M,即 2M = 21024KB = 21024*1024B = 2097152B。切割文件用到的是 Blob.slice()

// 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
const chunkSize = 2097152,
  chunkList = [], // 保存所有切片的数组
  chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
  suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名

// 根据文件内容生成 hash 值
const spark = new SparkMD5.ArrayBuffer()
spark.append(buffer)
const hash = spark.end()

// 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
let curChunk = 0 // 切片时的初始位置
for (let i = 0; i < chunkListLength; i++) {
  const item = {
    chunk: fileObj.slice(curChunk, curChunk + chunkSize),
    fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
  }
  curChunk += chunkSize
  chunkList.push(item)
}
console.log(chunkList)

选择一个文件后将打印得到诸如下图的结果

发送请求

发送请求可以是并行的或是串行的,这里选择串行发送。每个切片都新建一个请求,为了能实现断点续传,我们将请求封装到函数 fn 里,用一个数组 requestList 来保存请求集合,然后封装一个 send 函数,用于请求发送,这样一旦按下暂停键,可以方便的终止上传,代码如下:

sendRequest() {
  const requestList = [] // 请求集合
  this.chunkList.forEach(item => {
    const fn = () => {
      const formData = new FormData()
      formData.append('chunk', item.chunk)
      formData.append('filename', item.fileName)
      return axios({
        url: '/single3',
        method: 'post',
        headers: { 'Content-Type': 'multipart/form-data' },
        data: formData
      }).then(res => {
        if (res.data.code === 0) { // 成功
          if (this.percentCount === 0) {
            this.percentCount = 100 / this.chunkList.length
          }
          this.percent += this.percentCount // 改变进度
        }
      })
    }
    requestList.push(fn)
  })

  let i = 0 // 记录发送的请求个数
  const send = async () => {
    // if ('暂停') return
    if (i >= requestList.length) {
      // 发送完毕
      return
    }
    await requestList[i]()
    i++
    send()
  }
  send() // 发送请求
},

axios 部分也可以直接写成下面这种形式:

axios.post('/single3', formData, {
  headers: { 'Content-Type': 'multipart/form-data' }
})

所有切片发送成功后

根据后端接口需要再发送一个 get 请求并把文件的 hash 值传给服务器,我们定义一个 complete 方法来实现,这里假定发送的为视频文件

const complete = () => {
  axios({
    url: '/merge',
    method: 'get',
    params: { hash: this.hash }
  }).then(res => {
    if (res.data.code === 0) { // 请求发送成功
      this.videoUrl = res.data.path
    }
  })
}

这样就能在文件发送成功后在页面浏览到发送的视频了。

断点续传

首先是暂停按钮文字的处理,用了一个过滤器,如果 upload 值为 true 则显示“暂停”,否则显示“继续”:

filters: {
  btnTextFilter(val) {
    return val ? '暂停' : '继续'
  }
}

当按下暂停按钮,触发 handleClickBtn 方法

handleClickBtn() {
  this.upload = !this.upload
  // 如果不暂停则继续上传
  if (this.upload) this.sendRequest()
}

在发送切片的 send 方法的开头添加 if (!this.upload) return,这样只要 upload 这个变量为 false 就不会继续上传了。为了在暂停完后可以继续发送,需要在每次成功发送一个切片后将这个切片从 chunkList 数组里删除 this.chunkList.splice(index, 1)

代码汇总

<template>
  <div id="app">
    <!-- 上传组件 -->
    <el-upload action drag :auto-upload="false" :show-file-list="false" :on-change="handleChange">
      <i class="el-icon-upload"></i>
      <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
      <div class="el-upload__tip" slot="tip">大小不超过 200M 的视频</div>
    </el-upload>

    <!-- 进度显示 -->
    <div class="progress-box">
      <span>上传进度:{{ percent.toFixed() }}%</span>
      <el-button type="primary" size="mini" @click="handleClickBtn">{{ upload | btnTextFilter}}</el-button>
    </div>

    <!-- 展示上传成功的视频 -->
    <div v-if="videoUrl">
      <video :src="videoUrl" controls />
    </div>
  </div>
</template>

<script>
  import SparkMD5 from "spark-md5"
  import axios from "axios"

  export default {
    name: 'App3',
    filters: {
      btnTextFilter(val) {
        return val ? '暂停' : '继续'
      }
    },
    data() {
      return {
        percent: 0,
        videoUrl: '',
        upload: true,
        percentCount: 0
      }
    },
    methods: {
      async handleChange(file) {
        if (!file) return
        this.percent = 0
        this.videoUrl = ''
        // 获取文件并转成 ArrayBuffer 对象
        const fileObj = file.raw
        let buffer
        try {
          buffer = await this.fileToBuffer(fileObj)
        } catch (e) {
          console.log(e)
        }

        // 将文件按固定大小(2M)进行切片,注意此处同时声明了多个常量
        const chunkSize = 2097152,
          chunkList = [], // 保存所有切片的数组
          chunkListLength = Math.ceil(fileObj.size / chunkSize), // 计算总共多个切片
          suffix = /\.([0-9A-z]+)$/.exec(fileObj.name)[1] // 文件后缀名

        // 根据文件内容生成 hash 值
        const spark = new SparkMD5.ArrayBuffer()
        spark.append(buffer)
        const hash = spark.end()

        // 生成切片,这里后端要求传递的参数为字节数据块(chunk)和每个数据块的文件名(fileName)
        let curChunk = 0 // 切片时的初始位置
        for (let i = 0; i < chunkListLength; i++) {
          const item = {
            chunk: fileObj.slice(curChunk, curChunk + chunkSize),
            fileName: `${hash}_${i}.${suffix}` // 文件名规则按照 hash_1.jpg 命名
          }
          curChunk += chunkSize
          chunkList.push(item)
        }
        this.chunkList = chunkList // sendRequest 要用到
        this.hash = hash // sendRequest 要用到
        this.sendRequest()
      },

      // 发送请求
      sendRequest() {
        const requestList = [] // 请求集合
        this.chunkList.forEach((item, index) => {
          const fn = () => {
            const formData = new FormData()
            formData.append('chunk', item.chunk)
            formData.append('filename', item.fileName)
            return axios({
              url: '/single3',
              method: 'post',
              headers: { 'Content-Type': 'multipart/form-data' },
              data: formData
            }).then(res => {
              if (res.data.code === 0) { // 成功
                if (this.percentCount === 0) { // 避免上传成功后会删除切片改变 chunkList 的长度影响到 percentCount 的值
                  this.percentCount = 100 / this.chunkList.length
                }
                this.percent += this.percentCount // 改变进度
                this.chunkList.splice(index, 1) // 一旦上传成功就删除这一个 chunk,方便断点续传
              }
            })
          }
          requestList.push(fn)
        })

        let i = 0 // 记录发送的请求个数
        // 文件切片全部发送完毕后,需要请求 '/merge' 接口,把文件的 hash 传递给服务器
        const complete = () => {
          axios({
            url: '/merge',
            method: 'get',
            params: { hash: this.hash }
          }).then(res => {
            if (res.data.code === 0) { // 请求发送成功
              this.videoUrl = res.data.path
            }
          })
        }
        const send = async () => {
          if (!this.upload) return
          if (i >= requestList.length) {
            // 发送完毕
            complete()
            return
          }
          await requestList[i]()
          i++
          send()
        }
        send() // 发送请求
      },

      // 按下暂停按钮
      handleClickBtn() {
        this.upload = !this.upload
        // 如果不暂停则继续上传
        if (this.upload) this.sendRequest()
      },

      // 将 File 对象转为 ArrayBuffer
      fileToBuffer(file) {
        return new Promise((resolve, reject) => {
          const fr = new FileReader()
          fr.onload = e => {
            resolve(e.target.result)
          }
          fr.readAsArrayBuffer(file)
          fr.onerror = () => {
            reject(new Error('转换文件格式发生错误'))
          }
        })
      }
    }
  }
</script>

<style scoped>
  .progress-box {
    box-sizing: border-box;
    width: 360px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-top: 10px;
    padding: 8px 10px;
    background-color: #ecf5ff;
    font-size: 14px;
    border-radius: 4px;
  }
</style>

效果如下图:

One More Thing

FormData

这里发送数据用到了 FormData,如果编码类型被设为 "multipart/form-data",它会使用和表单一样的格式。

FormData.append()

会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键。该方法可以传递 3 个参数,formData.append(name, value, filename),其中 filename 为可选参数,是传给服务器的文件名称, 当一个 Blob 或 File 被作为第二个参数的时候, Blob 对象的默认文件名是 "blob"。 File 对象的默认文件名是该文件的名称。

到此这篇关于Vue 大文件上传和断点续传的实现的文章就介绍到这了,更多相关Vue 大文件上传和断点续传内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • vue+element+oss实现前端分片上传和断点续传

    纯前端实现: 切片上传 断点续传 .断点续传需要在切上上传的基础上实现 前端之前上传OSS,无需后端提供接口.先上完整代码,直接复制,将new OSS里的参数修改成自己公司OSS相关信息后可用,如遇问题,请继续往下看. oss官方文档 https://help.aliyun.com/document_detail/111268.html?spm=a2c4g.11186623.6.1111.5a583a07LknRUO 代码允许所需环境:vue + element + ali-oss 安装ali-

  • 基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能

    1. 前言 之前公司要在管理系统中做一个全局上传插件,即切换各个页面的时候,上传界面还在并且上传不会受到影响,这在vue这种spa框架面前并不是什么难题.然而后端大佬说我们要实现分片上传.秒传以及断点续传的功能,听起来头都大了. 很久之前我写了一篇webuploader的文章,结果使用起来发现问题很多,且官方团队不再维护这个插件了, 经过多天调研及踩雷,最终决定基于vue-simple-uploader插件实现该功能,在项目中使用起来无痛且稳定. 如果你只是想实现基本的(非定制化的)上传功能,直

  • Vue 大文件上传和断点续传的实现

    文件上传的 2 套方案 基于文件流(form-data) element-ui 框架的上传组件,就是默认基于文件流的. 数据格式:form-data: 传递的数据: file 文件流信息:filename 文件名字 客户端把文件转换为 base64 通过 fileRead.readAsDataURL(file) 转为 base64 字符串后,要用 encodeURIComponent 编译再发送, 发送的数据经由 qs.stringify 处理,请求头添加 "Content-Type"

  • Vue+Node实现大文件上传和断点续传

    目录 源代码 Blob.slice 切片 初始化文件内容 FormData.append() 大文件上传 断点续传 代码 创建切片 源码 worker 线程通讯的逻辑 断点续传 秒传 源代码 断点续传.分片上传.秒传.重试机制 文件上传是开发中的难点, 大文件上传及断点续传 难点中的细节及核心技术点. element-ui 框架的上传组件,是默认基于文件流的. 数据格式:form-data: 传递的数据: file 文件流信息:filename 文件名字 通过 fileRead.readAsDa

  • Electron中实现大文件上传和断点续传功能

    Electron官网的描述:Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库. Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的. 从官网的描述我们可以简单的概括,Electron是开源的框架,可以使用h5来开发跨平台pc桌面应用,这样前端开发这可以开发桌面应用了.由于它是基于Chromium和Node.js开发的,所以在Ele

  • 如何基于js管理大文件上传及断点续传详析

    目录 前言 前端结构 后端结构(node + express) 基于FormData实现文件上传 基于BASE64实现文件上传 BASE64具体方法 前端生成文件名传给后端 上传进度管控 大文件上传 服务端代码(大文件上传+断点续传) 总结 前言 前端小伙伴们平常在开发过程中文件上传是经常遇到的一个问题,也许你能够实现相关的功能,但是做完后回想代码实现上是不是有点"力不从心"呢?你真的了解文件上传吗?如何做到大文件上传以及断电续传呢,前后端通讯常用的格式,文件上传进度管控,服务端是如何

  • Vue+NodeJS实现大文件上传的示例代码

    目录 整体思路 项目演示 前端界面 文件切片 hash计算 查询切片状态 切片上传(断点续传) 文件总体上传进度 合并文件 优化 请求并发数控制 hash值计算优化 常见的文件上传方式可能就是new一个FormData,把文件append进去以后post给后端就可以了.但如果采用这种方式来上传大文件就很容易产生上传超时的问题,而且一旦失败还得从新开始,在漫长的等待过程中用户还不能刷新浏览器,不然前功尽弃.因此这类问题一般都是通过切片上传. 整体思路 将文件切成多个小文件 hash计算,需要计算一

  • springboot大文件上传、分片上传、断点续传、秒传的实现

    对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送.接收都是不可取,很容易导致内存问题.所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率. 本文是基于 springboot + vue 实现的文件上传,本文主要介绍服务端实现文件上传的步骤及代码实现,vue的实现步骤及实现请移步本人的另一篇文章 vue 大文件分片上传 - 断点续传.并发上传 上传分步: 本人分析上传总共分为: 检查文件是否已上传,如已上传可实现秒传 创建临时文件(._tmp

  • Java实现FTP批量大文件上传下载篇1

    本文介绍了在Java中,如何使用Java现有的可用的库来编写FTP客户端代码,并开发成Applet控件,做成基于Web的批量.大文件的上传下载控件.文章在比较了一系列FTP客户库的基础上,就其中一个比较通用且功能较强的j-ftp类库,对一些比较常见的功能如进度条.断点续传.内外网的映射.在Applet中回调JavaScript函数等问题进行详细的阐述及代码实现,希望通过此文起到一个抛砖引玉的作用. 一.引子 笔者在实施一个项目过程中出现了一种基于Web的文件上传下载需求.在全省(或全国)各地的用

  • asp.net大文件上传解决方案实例代码

    以ASP.NET Core WebAPI 作后端 API ,用 Vue 构建前端页面,用 Axios 从前端访问后端 API ,包括文件的上传和下载. 准备文件上传的API #region 文件上传 可以带参数 [HttpPost("upload")] public JsonResult uploadProject(IFormFile file, string userId) { if (file != null) { var fileDir = "D:\\aaa"

  • 前端大文件上传与下载(分片上传)的详细过程

    目录 一.问题 二.解决 1.第一步选择文件 2.校验文件是否符合规范 3.文件切片上传 4.分片上传注意点 5.大文件下载 总结 一.问题 日常业务中难免出现前端需要向后端传输大型文件的情况,这时单次的请求不能满足传输大文件的需求,就需要用到分片上传 业务需求为:用户可以上传小于20G的镜像文件,并进显示当前上传进度 前端:vue3.x+Element Plus组件+axios 二.解决 解决思路简单为前端选择文件后读取到文件的基本信息,包括:文件的大小.文件格式等信息,用于前端校验,校验完成

  • Jquery和BigFileUpload实现大文件上传及进度条显示

    实现方法:用到了高山来客 的bigfileupload组件,用高山来客的方法,弹出一个模式窗口,然后不停刷新获取进度,始终觉得体验感不好,于是想到用jquery来实现无刷新进度显示,因为提交页面后, 不能让其刷新页面,而是要不断地通过ajax获取progress.aspx返回的进度信息,所以用到了jquery.form的ajaxform提交.ajaxform提交后如果执行提交后的事件,比如在数据库里插入记录,还在调试中. 1.用到了jquery 的 progressbar .form.MultF

随机推荐