上传
为什么常用FormData对象来上传file
还可以用base64, 见下文。
Content-Type
-
application/x-www-form-urlencoded
会在url上拼接字符串,如:k=123&c=12241,同时对于中文还会转码。 -
application/json
直接会在请求体中 添加object对象 如: { a: 123, b: 456 } -
multipart/form-data
常用文件传输。
在network中可以看到添加带数据类型等各类标识的文件类型字符串请求体 告诉服务器端接收对象是一个文件数据流
如果采用JSON传输文件,得到的只是一个文件的描述对象,并不是文件本身:
FormData
FormData是Ajax 2.0对象用以将数据编译成键值对,以便于XMLHttpRequest来发送数据。XMLHttpRequest Level 2提供的一个接口对象,可以使用该对象来模拟和处理表单并方便的进行文件上传操作。
创建FormData对象并赋值
const data = newFormdData()
data.set("name", "小A")
data.set("name1", "小B")
data.append("sex", "男")
data.append("sex", "女")
set()
和 FormData.append
(FormData 接口的append() 方法 会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键。) 不同之处在于:如果某个 key 已经存在,set() 会直接覆盖所有该 key 对应的值,而 `FormData.append则是在该 key 的最后位置再追加一个值。
FormData取值
data.get("name") // 小A
data.has("name") // true
FormData对文件的处理
// antd的文件上传callback配置
beforeUpload: (file: Blob) => {
const data = new FormData()
data.append("file", file)
}
FormData与请求
使用axios发送post请求上传文件(multipart/form-data)到后端
Base64
base64只适合处理size小的文件。
base64是长得像字符串的byte类型字段。
base64可以通过application/json
直接传输。
blob转base64:
export const blobToBase64 = (blob) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.onload = (e) => {
resolve(e.target.result);
};
// readAsDataURL
fileReader.readAsDataURL(blob);
fileReader.onerror = () => {
reject(new Error('blobToBase64 error'));
};
});
}
base64转blob:
export function base64ToBlob(str) {
let bstr = window.atob(str);
let n = bstr.length;
let u8Arr = new Uint8Array(n);
while(n--){
u8Arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8Arr])
}
window.atob()和window.btoa()
window.atob()
是用来解码base64字符串。
window.btoa()
方法用于创建一个 base-64 编码的字符串。
FileReader
是一个用于读取前端本地文件或者Blob类型数据的对象。
方法
方法 | 作用 | 参数 | 返回值 |
---|---|---|---|
abort() | 中止读取操作 | none | none |
readAsArrayBuffer() | 读取file和Blob内容 | file/blob | result属性中返回ArrayBuffer数据对象的文件内容 |
eadAsBinaryString()[已被W3废弃] | 读取file和Blob内容 | file/blob | result属性中返回原始二进制数据的文件内容 |
readAsDataURL() | 读取file和Blob内容 | file/blob | result属性中返回data:URL格式的Base64字符串的文件内容 |
readAsText() | 读取file和Blob内容 | file/blob | result属性中返回一个字符串的文件内容 |
事件
FileReader.onabort
:该事件是中止读取的时候触发。
FileReader.onerror
:该事件是读取发生错误的时候触发。
FileReader.onload
:该事件是读取完成的时候触发。
FileReader.onloadstart
:该事件是读取操作刚开始的时候触发。
FileReader.onloadend
:该事件是读取结束的时候触发(失败和成功的时候都会触发)。
FileReader.onprogress
:该事件在读取的时候触发。
以FileReader.onload
为例,入参数为event,可以取到的数据如下:
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onload = (e) => {
console.log(e, "e")
}
如果是在FileReader.onprogress
事件中,可以通过event中的loaded/total
计算解析进度。
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onprogress = (e) => {
console.log(e.loaded/e.total, "progress")
}
只读属性
FileReader.error(只读)
:一个异常,表示在读取文件时发生的错误
FileReader.readyState(只读)
:表示FileReader状态的数字
值 | 状态名 | 描述 |
---|---|---|
0 | EMPTY | 未加载任何数据 |
1 | LOADING | 数据加载中 |
2 | DONE | 数据加载完毕 |
FileReader.result(只读)
:读取完文件的内容,该属性在数据读取完成之后才有效,文件内容的格式是由读取的方法所决定。
ArrayBuffer
附:存储单位换算
1 KB = 1024 bytes(字节)
1 Mb = 1024 Kb
1 MB = 1024 KB
1 GB = 1024 MB
一种二进制数组,通过数组(实际上不是数组)的形式直接操作内存。
ArrayBuffer
对象下,还包含两种视图:TypedArray
和DataView
。
ArrayBuffer
不可直接读取,需要通过他的两种视图进行读取。
TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。
数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
---|---|---|---|
Int8 | 1 | 8 位带符号整数 | signed char |
Uint8 | 1 | 8 位不带符号整数 | unsigned char |
Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
Int16 | 2 | 16 位带符号整数 | short |
Uint16 | 2 | 16 位不带符号整数 | unsigned short |
Int32 | 4 | 32 位带符号整数 | int |
Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
Float32 | 4 | 32 位浮点数 | float |
Float64 | 8 | 64 位浮点数 | double |
TypedArray
包含以下构造函数:
Int8Array:8 位有符号整数,长度 1 个字节。
Uint8Array:8 位无符号整数,长度 1 个字节。
Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不同。
Int16Array:16 位有符号整数,长度 2 个字节。
Uint16Array:16 位无符号整数,长度 2 个字节。
Int32Array:32 位有符号整数,长度 4 个字节。
Uint32Array:32 位无符号整数,长度 4 个字节。
Float32Array:32 位浮点数,长度 4 个字节。
Float64Array:64 位浮点数,长度 8 个字节。
DataView
实例提供 8 个方法读取内存:
getInt8:读取 1 个字节,返回一个 8 位整数。
getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
getInt16:读取 2 个字节,返回一个 16 位整数。
getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
getInt32:读取 4 个字节,返回一个 32 位整数。
getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
getFloat32:读取 4 个字节,返回一个 32 位浮点数。
getFloat64:读取 8 个字节,返回一个 64 位浮点数。
new Blob()
const aBlob = new Blob( array, options );
-
array 是一个由
ArrayBuffer
,ArrayBufferView
,Blob
,DOMString
等对象构成的Array
,或者其他类似对象的混合体,它将会被放进Blob
。DOMStrings 会被编码为 UTF-8。 -
options 是一个可选的
BlobPropertyBag
字典,它可能会指定如下两个属性:-
type
,默认值为""
,它代表了将会被放入到 blob 中的数组内容的 MIME 类型。 -
endings
,默认值为"transparent"
,用于指定包含行结束符\n
的字符串如何被写入。 它是以下两个值中的一个:"native"
,代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者"transparent"
,代表会保持 blob 中保存的结束符不变
-
使用Blob来存储二进制对象,虽然是二进制原始数据但是类似文件的对象,因此可以像操作文件对象一样操作Blob对象。
Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据,Blob和ArrayBuffer之间可以进行转换。
Blob.arrayBuffer()
与FileReader.readAsArrayBuffer()
类似,但是Blob.arrayBuffer()
返回的是一个promise实例,而不是需要通过onload
监听。
const bufferPromise = blob.arrayBuffer();
blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/);
var buffer = await blob.arrayBuffer();
Blob.slice()
Blob.slice()
方法用于创建一个包含源 Blob
的指定字节范围内的数据的新 Blob
对象。
Blob.slice(<start, end, contentType>)
start
和end
分别表示需要截取的下标(Blob.length),contentType
代表截取后想要赋予新的数据片段的类型。
Blob.stream()
读取Blob
对象,详见Blob.stream
Blob.text()
text()
方法返回一个 Promise
对象,包含 blob 中的内容,使用 UTF-8 格式编码。
const textPromise = blob.text();
blob.text().then(text => /* 执行的操作…… */);
var text = await blob.text();
分片上传
核心思想是借助Blob.slice()
对原始文件进行切片,然后通过http进行并发传输,当所有切片传输完毕后,通知后端进行合并,这里需要对切片进行编号处理,以保证在合并的时候有正确的顺序。
if(file){
// 设置分片大小
const sliceSize = 10 * 1024 * 1024; // 10m
const blobList = []
// 对文件分片
for(let i = 0; i <= Math.floor(file.size / sliceSize); i+=1){
blobList.push(file.slice(i * sliceSize, ( i + 1 ) * sliceSize < file.size ? ( i + 1 ) * sliceSize : file.size))
}
// 创建请求,并并发发送
const requestList = blobList.map((it, i) => {
return () => {const formData = new FormData()
formData.append("file", it)
formData.append("hash", i.toString())
testRequest(formData)
}
}
)
// 等到所有分片数据都发送完毕后,发送一个合并分片的请求
Promise.all(requestList.map(it => it())).then(() => {
console.log("发送合并文件请求")
})
}
这里更好的做法是把每一份分片生成一个hash值来做唯一标识。
使用spark-md5将文件转换为hash:
npm i spark-md5
// 获取apk的md5
var fileReader = new FileReader()
var spark = new SparkMD5() // 创建md5对象(基于SparkMD5)
fileReader.readAsBinaryString(myfile) // myfile 对应上传的文件
// 文件读取完毕之后的处理
fileReader.onload = (e) => {
console.log('获取文件的md5')
spark.appendBinary(e.target.result)
const md5 = spark.end()
console.log(md5)
由于读取文件、生成hash这一步骤比较耗时间,可能会造成页面卡死,推荐使用web-worker处理:
// 导入脚本
self.importScripts("/spark-md5.min.js");
// 生成文件 hash
self.onmessage = e => {
const { fileChunkList } = e.data;
const spark = new self.SparkMD5.ArrayBuffer();
let percentage = 0;
let count = 0;
const loadNext = index => {
const reader = new FileReader();
reader.readAsArrayBuffer(fileChunkList[index].file);
reader.onload = e => {
count++;
spark.append(e.target.result);
if (count === fileChunkList.length) {
self.postMessage({
percentage: 100,
hash: spark.end()
});
self.close();
} else {
percentage += 100 / fileChunkList.length;
self.postMessage({
percentage
});
// calculate recursively
loadNext(count);
}
};
};
loadNext(0);
};
最终可以得到该文件的最终hash: spark.end()
断点续传
核心思路:需要服务端/前端记住上次暂停时上传到哪个hash位置了。
改造request
如果要实现断点续传,就需要用到请求的一些特殊能力:
- 取消未完成请求。
- 获取请求成功失败情况,取得成功请求和失败请求的队列。
- 开始上传时,生成一个上传队列,将上传过程中的请求(或请求hash放入列表中)
let requestList = []
const importBackListFn = async (data) => {
requestList.push({
data: data.get("data"),
hash: data.get("hash"),
})
const result = await request()
return {
...result,
hash: data.get("hash"),
}
}
- 上传过程中,将已上传成功的文件分片从队列中删除
importBackListFn().then((res) => {
if(res){
requestList = requestList.filter(it => it.hash !== res.hash)
}
})
- 暂停时,将队列中的所有请求都取消,
xhr.abort()
或者new AbortController()
,并清空上传队列
const handleAbort = () => {
// 原生
xhr.abort()
// fetch
const controller = new AbortController()
controller.abort()
requestList = []
}
- 重新续传时,通过后端请求取到已上传的队列,与本地文件分片对比,重新生成待上传的队列
// 请求已上传文件列表
getFileListHash().then(res => {
blobList = blobList.filter(it => res.findIndex(ite => ite.hash === it.hash) !== -1)
})
// 继续上传blobList
参考上面的分片请求方法
分片上传/断点续传如何控制上传进度条
- 需要新增一个变量存储已上传成功的文件列表,包含hash和size即可。
const hasuploadFile;
importBackListFn().then((res) => {
if(res){
requestList = requestList.filter(it => it.hash !== res.hash)
hasuploadFile.push(res)
}
})
- 通过
hasuploadFile.map(it => it.size).reduce((pre, cur) => pre + cur)/file.size
计算百分比即可。
const process = hasuploadFile.map(it => it.size).reduce((pre, cur) => pre + cur)/file.size;
下载
Blob形式
这里的object参数是用于创建URL的File对象、Blob 对象或者 MediaSource 对象,生成的链接就是以blob:开头的一段地址,表示指向的是一个二进制数据。
其中localhost:1234是当前网页的主机名称和端口号,也就是location.host,而且这个Blob URL是可以直接访问的。需要注意的是,即使是同样的二进制数据,每调用一次URL.createObjectURL()
方法,就会得到一个不一样的Blob URL。这个URL的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个Blob URL就失效。
通过URL.revokeObjectURL(objectURL)
可以释放 URL 对象。当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了,允许平台在合适的时机进行垃圾收集。
const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl
需要在页面上创建a标签元素,并模拟点击
let arch = window.document.createElement("a")
if (arch.href) {
window.URL.revokeObjectURL(arch.href)
}
arch.href = objectURL
arch.download = filename || "juicy"
arch.click()
base64形式
转成Blob再下载。
或者
base64拼成的链接可以直接通过a标签下载: