目录
第一章 前言
相信我们很多人在项目中都会实现图片上传这么个功能,并且也会使用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 />
- 我们可以看到最终返回的数据类型是File对象
一个或多个文件对象组成的数组
2.2 认识理解文件上传的四个对象
2.2.1 file对象
- 通过指定input标签type属性为file来读取files对象,是一个由一个或多个文件对象组成的数组
2.2.2 blob对象
- 表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 通常是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示不可变的类似文件对象的原始数据, 使用构造函数创建。
2.2.3 formData对象
- 利用它来提交表单、模拟表单提交,最大的优势就是可以上传二进制文件。
- 熟悉formData:
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的使用
第四章 实战应用
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)
})
}
}
}
- 多文件上传两种方法可能会涉及到的问题:
- 当上传数量过多时(上百上千时),处理一条数据发一个请求,造成频繁的调用接口,对服务器会有一定的影响;用户频繁上传大量的图片,服务器需要处理和存储这些图片,会消耗大量的带宽和存储空间,这可能导致服务器负载增加,影响了服务器的性能表现。此外,图片上传过程中的网络传输也会占用服务器的网络资源,对其他用户的访问速度可能会有所影响。
- 但时当一次性上传或者打包上传特别大的文件时,又有可能出现上传文件过大,后端处理文件的时间太长,但是前端设置的响应时间没有这么长,到了时间之后前端就会报错,那么这上传又如何处理
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,具体情况具体解析)
第五章 源码
参数说明:
- 单个/多个上传
- 分片上传