0


文件上传总结:用原生解决前端文件上传操作(单个,多个,大文件切片)

第一章 前言

相信我们很多人在项目中都会实现图片上传这么个功能,并且也会使用FormData和elementUI、antd等框架的upload组件实现,但是一延伸到原理、细节时就很难受,说不出所以然(小编也体验过,哈哈哈哈),所以如下是小编最近总结的点。

  • 用到的前端页面
    <input type="file" @change="fileChange" multiple />
    <button @click="submit">多选提交</button>
    <span v-for="(item, index) in fileList" :key="index">{{ item.name }}</span>
    <button @click="dealFile">处理图片信息</button>

第二章 理解文件上传的对象

2.1 如何利用原生实现

  • 利用input原生标签,type为file形式,multiple为多选,accept可以限制上传的文件类型,@change是使用的方法
<input type="file" @change="fileChange" multiple />

HTML input 标签 | 菜鸟教程

  • 我们可以看到最终返回的数据类型是File对象

一个或多个文件对象组成的数组

2.2 认识理解文件上传的四个对象

2.2.1 file对象

  • 通过指定input标签type属性为file来读取files对象,是一个由一个或多个文件对象组成的数组

2.2.2 blob对象

  • 表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据, 使用构造函数创建。

2.2.3 formData对象

  • 利用它来提交表单、模拟表单提交,最大的优势就是可以上传二进制文件
  • 熟悉formData:

前后端传参:掌握FormData并解决form-data类型传参问题_前端formdata传参-CSDN博客

2.2.4 fileReader对象

  • 构造函数方式实例化一个fileReader对象,readAs()方法将文件对象读取成base64格式或者文本格式
2.2.4.1 了解fileReader对象基本属性
  • 直接使用时
const reader = new FileReader()
console.log('FileReader方法及属性:', reader)
  • 查看方法及属性:

  • FileReader.error(只读属性):读取文件时报的错误,null表示在读取用户所上传文件时没有出错

  • FileReader.readyState(只读属性):加载状态
    状态值描述0还没有加载任何数据1数据正在被加载2已完成全部的读取请求

  • FileReader.result(只读属性):表示文件的内容,仅在读取操作完成之后才有效,数据的格式取决于使用哪个方法(在2.2.4.2中)来启动读取操作

const reader = new FileReader()
console.log('FileReader方法及属性:', reader)
console.log('尝试使用只读属性error:', reader.error)
console.log('尝试使用只读属性readyState:', reader.readyState)
console.log('尝试使用只读属性result:', reader.result)

2.2.4.2 了解 fileReader对象基本方法
  • 我们顺着原型链往下找可以发现,该对象下还有以下四种方法


方法描述FileReader.abort()终止读取操作FileReader.readAsArrayBuffer()开始读取指定文件中的内容,一旦完成,result属性中保存的是被读取文件的ArrayBuffer数据对象FileReader.readAsBinaryString()开始读取指定文件中的内容,一旦完成,result属性中保存的是所读取文件的原始二进制数据FileReader.readAsDataURL()开始读取指定文件中的内容,一旦完成,result属性中是一个

data:URL

格式的

Base64

字符串以表示所读取文件的内容FileReader.readAsText()开始读取指定文件中的内容,一旦完成,result属性中保存的是一个字符串以表示所读取的文件内容

  • 理解 FileReader.abort()的使用
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.abort()
    console.log('走到这了1')
    reader.onload = (res) => {
      console.log('走到这了2')
    }
  })
},

由于 FileReader.abort() 终止了读取操作,所以不会执行onload执行完毕触发的函数中的代码

  • 理解 FileReader.readAsArrayBuffer()的使用
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.readAsArrayBuffer(item)
    reader.onload = (res) => {
      console.log('执行readAsArrayBuffer返回的数据', res)
      console.log('返回的result值为:', res.target.result)
    }
  })
},

  • 理解 FileReader.readAsBinaryString() 的使用
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.readAsBinaryString(item)
    reader.onload = (res) => {
      console.log('执行readAsBinaryString返回的数据', res)
      console.log('返回的result值为:', res.target.result)
    }
  })
},

注意:该信息是原始的二进制文件信息

  • 理解 FileReader.readAsDataURL() 的使用
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.readAsDataURL(item)
    reader.onload = (res) => {
      console.log('执行readAsDataURL返回的数据', res)
      console.log('返回的result值为:', res.target.result)
    }
  })
},

  • 理解 FileReader.readAsText() 的使用
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.readAsText(item)
    reader.onload = (res) => {
      console.log('执行readAsText返回的数据', res)
      console.log('返回的result值为:', res.target.result)
    }
  })
},

注意:小编这里识别的是txt文档中的文字,如果上传的是图片,会说乱码

2.2.4.3 了解fileReader对象基本事件
  • 查看回调事件


事件方法描述FileReader.onabort()处理abort事件,该事件在读取操作被中断时触发FileReader.onerror()处理error事件,该事件在读取操作发生错误时触发FileReader.onload()处理load事件,该事件在读取操作完成时触发FileReader.onloadstart()处理loadstart事件,该事件在读取操作开始时触发FileReader.onloadend()处理loadend事件,该事件在读取操作结束时(成功或失败)触发FileReader.onprogress()处理progress事件,该事件正在读取时触发

  • 使用方法:
dealFile () {
  this.fileList.forEach(item => {
    const reader = new FileReader()
    reader.onabort = (res) => {
      console.log('读取中断了onabort', res)
    }
    reader.onerror = (res) => {
      console.log('读取发生错误了onerror', res)
    }
    reader.onload = (res) => {
      console.log('读取完成了onload', res)
    }
    reader.onloadstart = (res) => {
      console.log('读取开始了onloadstart', res)
    }
    reader.onloadend = (res) => {
      console.log('读取结束了onloadend', res)
    }
    reader.onprogress = (res) => {
      console.log('读取进行中onprogress', res)
    }
  })
},
  • 输出结果

  • 由上可知执行顺序为

1、onloadstart -> onprogress -> onload -> onloadend

2、当使用 reader.abort() 方法中断读取时会执行onabort

3、当读取错误时会执行onerror

第三章 理解四个对象的使用

3.1 file与blob对象的使用

3.1.1 file对象转blob对象

fileToBlob (file) {
   // new Blob([文件二进制流], 文件类型)
   const blob = new Blob([file], { type: file.type }) // 直接利用Blob的方法
   console.log('blob', blob)
   return blob
},

3.1.2 blob对象转file对象

// blob二进制流转file二进制流注意要再携带参数文件名
blobToFile (blob, fileName) {
  // new File([blob二进制流], 文件名, 文件类型)
  const file = new File([blob], fileName, { type: blob.type })
  console.log('file', file)
},

3.2 formData的使用

前后端传参:掌握FormData并解决form-data类型传参问题_前端formdata传参-CSDN博客

第四章 实战应用

4.1 单文件上传

  • 利用input元素的accept属性限制上传文件的类型,multiple限制能否多选

  • 完整代码:

     -- html部分——
    
<template>
  <div>
    <input type="file" @change="fileChange"/>
    // 该按钮支持单个文件上传与多个文件上传
    <button @click="submit">多选提交</button>
    <span v-for="(item, index) in fileList" :key="index">{{ item.name }}</span>
  </div>
</template>
     -- js部分——
<script>
import axios from 'axios'
export default {
  data () {
    return {
      fileList: [] // 定义空数组存储多个文件
    }
  },
  components: {
  },
  methods: {
    fileChange (e) {
      // 一个由不同文件对象组成的对象
      console.log('文件对象e,文件方法:e.target.files', e, e.target.files)
      // 单文件上传
      this.fileList = []
      this.fileList.push(e.target.files[0])
    },
    async submit () {
      const _formdata = new FormData()
      // 循环fileList,每次都创建一个formdata对象上传
      this.fileList.forEach(async item => {
        // 转二进制流形式上传
        const blob = new Blob([item], { type: item.type })
        console.log('blob', blob)
        _formdata.append('files', blob, item.name)
      })
      axios({
        url: '/api/upload',
        method: 'POST',
        headers: {
          'Content-type': 'multipart/form-data'
        },
        data: _formdata
      }).then(response => {
        console.log(response)
      }).catch(err => {
        console.log(err)
      })
    }
  }
}
    -- 页面展示:

  • 上传成功(只选择一个文件上传即可)

  • 传参

4.2 多文件上传

  • 将多个文件放到一个数组内,然后循环这个数组内的文件对象,利用formdata实现;可以每处理一次,然后调用一次接口,上传一个文件;也可以将数组中的文件对象都处理好之后,调用接口上传所有文件
  • 注意:该接口的实现需要与后端商量好可以如何上传
  • html部分——
<template>
  <div>
    // 支持批量上传
    <input type="file" @change="fileChange" multiple />
    // 该按钮支持单个文件上传与多个文件上传
    <button @click="submit">多选提交</button>
    <span v-for="(item, index) in fileList" :key="index">{{ item.name }}</span>
  </div>
</template>
  • 方法一:遍历数组中的文件对象,利用formdata处理值,每处理一次,调用一次方法
<script>
import axios from 'axios'
export default {
  data () {
    return {
      fileList: [] // 定义空数组存储多个文件
    }
  },
  components: {
  },
  methods: {
    fileChange (e) {
      // 一个由不同文件对象组成的对象
      console.log('文件对象e,文件方法:e.target.files', e, e.target.files)
      // 检测e.target.files是否有多个文件
      if (e.target.files.length > 1) {
        // 如果上传了多个文件将其合并
        this.fileList = [...this.fileList, ...e.target.files]
        // this.fileList = this.fileList.concat(e.target.files)
      } else {
        this.fileList.push(e.target.files[0])
      }
    },
    async submit () {
      // 循环fileList,每次都创建一个formdata对象上传
      this.fileList.forEach(async item => {
        const _formdata = new FormData()
        const blob = new Blob([item], { type: item.type })
        console.log('blob', blob)
        _formdata.append('files', blob, item.name)
        axios({
          url: '/api/upload',
          method: 'POST',
          headers: {
            'Content-type': 'multipart/form-data'
          },
          data: _formdata
        }).then(response => {
          console.log(response)
        }).catch(err => {
          console.log(err)
        })
      })
    }
  }
}

  • 方法二:遍历数组中的文件对象,利用formdata处理值,利用formdata.append将每一次处理的值添加到formdata对象中,最后上传所有文件
<script>
import axios from 'axios'
export default {
  data () {
    return {
      fileList: [] // 定义空数组存储多个文件
    }
  },
  components: {
  },
  methods: {
    fileChange (e) {
      // 一个由不同文件对象组成的对象
      console.log('文件对象e,文件方法:e.target.files', e, e.target.files)
      // 检测e.target.files是否有多个文件
      if (e.target.files.length > 1) {
        // 如果上传了多个文件将其合并
        this.fileList = [...this.fileList, ...e.target.files]
        // this.fileList = this.fileList.concat(e.target.files)
      } else {
        this.fileList.push(e.target.files[0])
      }
    },
    async submit () {
      const _formdata = new FormData()
      // 循环fileList,每次都创建一个formdata对象上传
      this.fileList.forEach(async item => {
        // 转二进制流形式上传
        const blob = new Blob([item], { type: item.type })
        console.log('blob', blob)
        _formdata.append('files', blob, item.name)
      })
      axios({
        url: '/api/upload',
        method: 'POST',
        headers: {
          'Content-type': 'multipart/form-data'
        },
        data: _formdata
      }).then(response => {
        console.log(response)
      }).catch(err => {
        console.log(err)
      })
    }
  }
}

  • 多文件上传两种方法可能会涉及到的问题:
  1. 当上传数量过多时(上百上千时),处理一条数据发一个请求,造成频繁的调用接口,对服务器会有一定的影响;用户频繁上传大量的图片,服务器需要处理和存储这些图片,会消耗大量的带宽和存储空间,这可能导致服务器负载增加,影响了服务器的性能表现。此外,图片上传过程中的网络传输也会占用服务器的网络资源,对其他用户的访问速度可能会有所影响。
  2. 但时当一次性上传或者打包上传特别大的文件时,又有可能出现上传文件过大,后端处理文件的时间太长,但是前端设置的响应时间没有这么长,到了时间之后前端就会报错,那么这上传又如何处理

4.3就是如何解决上述两个问题中的方法之一:分片上传

4.3 切片上传

4.3.1 如何处理多个图片

  • 当用户上传大量图片时,如果我们使用4.2中了两种方法会出现的问题已经了解了,那么我们如何解决呢,如何处理这批量的图片呢?
  • 小编给出如下方法:

input标签设置multiple上传大量文件 -> 上传时将每一个文件的二进制流封装成一个promise对象 -> 将每一个promise对象push到一个数组中 -> 利用promise.all确定数组中promise的状态值都是成功状态,promise.all为成功态才会执行代码 -> 遍历promise数组获取里面的二进制流文件 -> 利用jszip组件将所有的文件打包成压缩包的二进制流 -> (前端可以利用file-saver下载查看压缩包内容) -> 最后将改压缩包的二进制流进行分片上传

  • 处理多个图片的代码:

html部分——

<template>
  <div>
    <input type="file" @change="fileChange" multiple/>
    <button @click="imageCompress">压缩图片</button>
    <span v-for="(item, index) in fileList" :key="index">{{ item.name }}</span>
  </div>
</template>

js部分——

<script>
import axios from 'axios'
import JSZip from 'jszip'
// import FileSaver from 'file-saver'

export default {
  data () {
    return {
      fileList: [], // 定义空数组存储多个文件
      filesPromises: [] // 存放二进制文件流的promise数组
    }
  },
  components: {
  },
  methods: {
    // 利用promise处理多个图片
    // 将每一个文件的二进制流封装成一个promise对象
    dealFiles (file) {
      return new Promise((resolve, reject) => {
        resolve({ file })
      })
    },
    // 图片压缩
    imageCompress () {
      // 利用promise.all
      Promise.all(this.filesPromises).then(async (files) => {
        // 定义一个JSZip实例
        const zip = new JSZip()
        // 遍历数据 遍历promise数组获取里面的二进制流文件
        files.forEach(async (item, index) => {
          const { file } = item
          console.log('每一个二进制流文件数据为:', file)
          // 添加需要压缩的文件,二进制流的形式
          await zip.file(file.name, file)
        })
        // 下载压缩包
        zip.generateAsync({ type: 'blob' }).then((content) => {
            console.log('压缩包的二进制流信息', content)
            // 下载上面压缩的压缩包
            FileSaver.saveAs(content, '合并的内容' + '.zip')
        })
      }).catch(err => {
        console.log(err)
      })
    },
    fileChange (e) {
      // 一个由不同文件对象组成的对象
      console.log('文件对象e,文件方法:e.target.files', e, e.target.files)
      // 检测e.target.files是否有多个文件
      if (e.target.files.length > 1) {
        this.fileList = [...this.fileList, ...e.target.files]
        // this.fileList = this.fileList.concat(e.target.files)
      } else {
        this.fileList.push(e.target.files[0])
      }
      // 处理文件信息成promise对象
      // 每一个promise对象push到一个数组
      this.fileList.forEach(item => {
        this.filesPromises.push(this.dealFiles(item))
      })
      console.log('存放文件的列表', this.fileList, this.filesPromises)
    }
  }
}
</script>

页面效果——

上传多个文件:

将每一个promise对象push到一个数组中 :

查看处理后的数据信息:

**利用file-saver插件中的方法下载的内容: **

**成功利用jszip插件中的方法将上传的图片打压成压缩包: **

4.3.2 实现切片上传

  • 切片上传的核心就是利用二进制流中的size(文件大小)的slice方法,切割数据流,将每一段二进制流数据发送请求给后端,当发送完成后,由后端合并,最终返回前端想要的数据

html部分——

<template>
  <div>
    <input type="file" @change="fileChange" multiple/>
    <button @click="shardingSubmit">分片提交</button>
    <button @click="imageCompress">压缩图片</button>
    <span v-for="(item, index) in fileList" :key="index">{{ item.name }}</span>
    <div>
      上传进度:{{ precent }}%
    </div>
  </div>
</template>

js部分—— (先点击压缩图片按钮,再点击分片提交)

注意:一定要与后端沟通好传参的格式

<script>
import axios from 'axios'
import JSZip from 'jszip'
// import FileSaver from 'file-saver'

export default {
  data () {
    return {
      fileList: [], // 定义空数组存储多个文件
      filesPromises: [], // 存放二进制文件流的promise数组
      compreeContent: null
    }
  },
  components: {
  },
  methods: {
    // 分片上传
    async shardingSubmit () {
      const size = 1024 * 1024
      // 注意 ======== 从这开始时小编传一个文件做的切片上传测试
      // const fileData = this.fileList[0]
      // console.log('fileData', fileData)
      // // const fileName = fileData.name.split('.')[0]
      // const blob = new Blob([fileData], { type: fileData.type })
      // 注意 ======== 到这结束,获取blob二进制数据流的过程
      const blob = this.compreeContent
      const blobSize = blob.size
      let current, i
      console.log('blob', blob, blob.size)
      for (current = 0, i = 0; current < blobSize; current += size, i++) {
        const _formdata = new FormData()
        // 一次添加1M大小的切片  注意添加时的说明一般为文件名,后端接收后按照文件名标识拼接
        // 传参格式(与后端沟通好)
        _formdata.append('file', blob.slice(current, current + size))
        _formdata.append('chunkIndex', i + 1)
        _formdata.append('chunkTotal', Math.ceil(blobSize / size))
        _formdata.append('name', `片段${Math.ceil(blobSize / size)}`)
        // 这里可以通过传参动态解决
        _formdata.append('suffix', 'zip')
        // _formdata.append('suffix', 'png')
        axios({
          url: '/api/part',
          method: 'POST',
          headers: {
            'Content-type': 'multipart/form-data'
          },
          data: _formdata
        }).then(response => {
          console.log(response)
          // 计算当前上传进度,展示到页面
          // 小编只是实现了分片上传的逻辑,进度条未处理好
          this.precent = Math.ceil(((current) / blobSize) * 100)
        }).catch(err => {
          console.log(err)
        })
      }
    },
    // 利用promise处理多个图片
    // 将每一个文件的二进制流封装成一个promise对象
    dealFiles (file) {
      return new Promise((resolve, reject) => {
        resolve({ file })
      })
    },
    // 图片压缩/文件
    imageCompress () {
      // 利用promise.all
      Promise.all(this.filesPromises).then(async (files) => {
        // 定义一个JSZip实例
        const zip = new JSZip()
        // 遍历数据 遍历promise数组获取里面的二进制流文件
        files.forEach(async (item, index) => {
          const { file } = item
          console.log('每一个二进制流文件数据为:', file)
          // 添加需要压缩的文件,二进制流的形式
          await zip.file(file.name, file)
        })
        // 下载压缩包
        zip.generateAsync({ type: 'blob' }).then((content) => {
            console.log('压缩包的二进制流信息', content)
            this.compreeContent = content
            // 下载上面压缩的压缩包
            // FileSaver.saveAs(content, '合并的内容' + '.zip')
        })
      }).catch(err => {
        console.log(err)
      })
    },
    fileChange (e) {
      // 一个由不同文件对象组成的对象
      console.log('文件对象e,文件方法:e.target.files', e, e.target.files)
      // 检测e.target.files是否有多个文件
      if (e.target.files.length > 1) {
        this.fileList = [...this.fileList, ...e.target.files]
        // this.fileList = this.fileList.concat(e.target.files)
      } else {
        this.fileList.push(e.target.files[0])
      }
      // 处理文件信息成promise对象
      // 每一个promise对象push到一个数组
      this.fileList.forEach(item => {
        this.filesPromises.push(this.dealFiles(item))
      })
      console.log('存放文件的列表', this.fileList, this.filesPromises)
    }
  }
}
</script>

后端合并结果:(这里只是demo,具体情况具体解析)

第五章 源码

batch_file_upload: 文件上传(单个、多个、分片)代码

参数说明:

  • 单个/多个上传

  • 分片上传

标签: 前端 vue javascript

本文转载自: https://blog.csdn.net/qq_45796592/article/details/135886130
版权归原作者 ❆VE❆ 所有, 如有侵权,请联系我们删除。

“文件上传总结:用原生解决前端文件上传操作(单个,多个,大文件切片)”的评论:

还没有评论