上傳
為什么常用FormData對象來上傳file
還可以用base64, 見下文。
Content-Type

-
application/x-www-form-urlencoded
會在url上拼接字符串,如:k=123&c=12241,同時(shí)對于中文還會轉(zhuǎn)碼。 -
application/json
直接會在請求體中 添加object對象 如: { a: 123, b: 456 } -
multipart/form-data
常用文件傳輸。
在network中可以看到添加帶數(shù)據(jù)類型等各類標(biāo)識的文件類型字符串請求體 告訴服務(wù)器端接收對象是一個(gè)文件數(shù)據(jù)流
image.png
如果采用JSON傳輸文件,得到的只是一個(gè)文件的描述對象,并不是文件本身:

FormData
FormData是Ajax 2.0對象用以將數(shù)據(jù)編譯成鍵值對,以便于XMLHttpRequest來發(fā)送數(shù)據(jù)。XMLHttpRequest Level 2提供的一個(gè)接口對象,可以使用該對象來模擬和處理表單并方便的進(jìn)行文件上傳操作。

創(chuàng)建FormData對象并賦值
const data = newFormdData()
data.set("name", "小A")
data.set("name1", "小B")
data.append("sex", "男")
data.append("sex", "女")
set() 和 FormData.append(FormData 接口的append() 方法 會添加一個(gè)新值到 FormData 對象內(nèi)的一個(gè)已存在的鍵中,如果鍵不存在則會添加該鍵。) 不同之處在于:如果某個(gè) key 已經(jīng)存在,set() 會直接覆蓋所有該 key 對應(yīng)的值,而 `FormData.append則是在該 key 的最后位置再追加一個(gè)值。
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發(fā)送post請求上傳文件(multipart/form-data)到后端
Base64
base64只適合處理size小的文件。
base64是長得像字符串的byte類型字段。
base64可以通過application/json直接傳輸。
blob轉(zhuǎn)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轉(zhuǎn)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()方法用于創(chuàng)建一個(gè) base-64 編碼的字符串。
FileReader
是一個(gè)用于讀取前端本地文件或者Blob類型數(shù)據(jù)的對象。
方法
| 方法 | 作用 | 參數(shù) | 返回值 |
|---|---|---|---|
| abort() | 中止讀取操作 | none | none |
| readAsArrayBuffer() | 讀取file和Blob內(nèi)容 | file/blob | result屬性中返回ArrayBuffer數(shù)據(jù)對象的文件內(nèi)容 |
| eadAsBinaryString()[已被W3廢棄] | 讀取file和Blob內(nèi)容 | file/blob | result屬性中返回原始二進(jìn)制數(shù)據(jù)的文件內(nèi)容 |
| readAsDataURL() | 讀取file和Blob內(nèi)容 | file/blob | result屬性中返回data:URL格式的Base64字符串的文件內(nèi)容 |
| readAsText() | 讀取file和Blob內(nèi)容 | file/blob | result屬性中返回一個(gè)字符串的文件內(nèi)容 |
事件
FileReader.onabort:該事件是中止讀取的時(shí)候觸發(fā)。
FileReader.onerror:該事件是讀取發(fā)生錯(cuò)誤的時(shí)候觸發(fā)。
FileReader.onload:該事件是讀取完成的時(shí)候觸發(fā)。
FileReader.onloadstart:該事件是讀取操作剛開始的時(shí)候觸發(fā)。
FileReader.onloadend:該事件是讀取結(jié)束的時(shí)候觸發(fā)(失敗和成功的時(shí)候都會觸發(fā))。
FileReader.onprogress:該事件在讀取的時(shí)候觸發(fā)。
以FileReader.onload為例,入?yún)?shù)為event,可以取到的數(shù)據(jù)如下:
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onload = (e) => {
console.log(e, "e")
}

如果是在FileReader.onprogress事件中,可以通過event中的loaded/total計(jì)算解析進(jìn)度。
const fileReader = new FileReader();
fileReader.readAsDataURL(file)
fileReader.onprogress = (e) => {
console.log(e.loaded/e.total, "progress")
}

只讀屬性
FileReader.error(只讀):一個(gè)異常,表示在讀取文件時(shí)發(fā)生的錯(cuò)誤
FileReader.readyState(只讀):表示FileReader狀態(tài)的數(shù)字
| 值 | 狀態(tài)名 | 描述 |
|---|---|---|
| 0 | EMPTY | 未加載任何數(shù)據(jù) |
| 1 | LOADING | 數(shù)據(jù)加載中 |
| 2 | DONE | 數(shù)據(jù)加載完畢 |
FileReader.result(只讀):讀取完文件的內(nèi)容,該屬性在數(shù)據(jù)讀取完成之后才有效,文件內(nèi)容的格式是由讀取的方法所決定。
ArrayBuffer
附:存儲單位換算
1 KB = 1024 bytes(字節(jié))
1 Mb = 1024 Kb
1 MB = 1024 KB
1 GB = 1024 MB
一種二進(jìn)制數(shù)組,通過數(shù)組(實(shí)際上不是數(shù)組)的形式直接操作內(nèi)存。
ArrayBuffer對象下,還包含兩種視圖:TypedArray和DataView。
ArrayBuffer不可直接讀取,需要通過他的兩種視圖進(jìn)行讀取。
TypedArray視圖支持的數(shù)據(jù)類型一共有 9 種(DataView視圖支持除Uint8C以外的其他 8 種)。
| 數(shù)據(jù)類型 | 字節(jié)長度 | 含義 | 對應(yīng)的 C 語言類型 |
|---|---|---|---|
| Int8 | 1 | 8 位帶符號整數(shù) | signed char |
| Uint8 | 1 | 8 位不帶符號整數(shù) | unsigned char |
| Uint8C | 1 | 8 位不帶符號整數(shù)(自動過濾溢出) | unsigned char |
| Int16 | 2 | 16 位帶符號整數(shù) | short |
| Uint16 | 2 | 16 位不帶符號整數(shù) | unsigned short |
| Int32 | 4 | 32 位帶符號整數(shù) | int |
| Uint32 | 4 | 32 位不帶符號的整數(shù) | unsigned int |
| Float32 | 4 | 32 位浮點(diǎn)數(shù) | float |
| Float64 | 8 | 64 位浮點(diǎn)數(shù) | double |
TypedArray包含以下構(gòu)造函數(shù):
Int8Array:8 位有符號整數(shù),長度 1 個(gè)字節(jié)。
Uint8Array:8 位無符號整數(shù),長度 1 個(gè)字節(jié)。
Uint8ClampedArray:8 位無符號整數(shù),長度 1 個(gè)字節(jié),溢出處理不同。
Int16Array:16 位有符號整數(shù),長度 2 個(gè)字節(jié)。
Uint16Array:16 位無符號整數(shù),長度 2 個(gè)字節(jié)。
Int32Array:32 位有符號整數(shù),長度 4 個(gè)字節(jié)。
Uint32Array:32 位無符號整數(shù),長度 4 個(gè)字節(jié)。
Float32Array:32 位浮點(diǎn)數(shù),長度 4 個(gè)字節(jié)。
Float64Array:64 位浮點(diǎn)數(shù),長度 8 個(gè)字節(jié)。
DataView實(shí)例提供 8 個(gè)方法讀取內(nèi)存:
getInt8:讀取 1 個(gè)字節(jié),返回一個(gè) 8 位整數(shù)。
getUint8:讀取 1 個(gè)字節(jié),返回一個(gè)無符號的 8 位整數(shù)。
getInt16:讀取 2 個(gè)字節(jié),返回一個(gè) 16 位整數(shù)。
getUint16:讀取 2 個(gè)字節(jié),返回一個(gè)無符號的 16 位整數(shù)。
getInt32:讀取 4 個(gè)字節(jié),返回一個(gè) 32 位整數(shù)。
getUint32:讀取 4 個(gè)字節(jié),返回一個(gè)無符號的 32 位整數(shù)。
getFloat32:讀取 4 個(gè)字節(jié),返回一個(gè) 32 位浮點(diǎn)數(shù)。
getFloat64:讀取 8 個(gè)字節(jié),返回一個(gè) 64 位浮點(diǎn)數(shù)。
new Blob()
const aBlob = new Blob( array, options );
-
array 是一個(gè)由
ArrayBuffer,ArrayBufferView,Blob,DOMString等對象構(gòu)成的Array,或者其他類似對象的混合體,它將會被放進(jìn)Blob。DOMStrings 會被編碼為 UTF-8。 -
options 是一個(gè)可選的
BlobPropertyBag字典,它可能會指定如下兩個(gè)屬性:-
type,默認(rèn)值為"",它代表了將會被放入到 blob 中的數(shù)組內(nèi)容的 MIME 類型。 -
endings,默認(rèn)值為"transparent",用于指定包含行結(jié)束符\n的字符串如何被寫入。 它是以下兩個(gè)值中的一個(gè):"native",代表行結(jié)束符會被更改為適合宿主操作系統(tǒng)文件系統(tǒng)的換行符,或者"transparent",代表會保持 blob 中保存的結(jié)束符不變
-
使用Blob來存儲二進(jìn)制對象,雖然是二進(jìn)制原始數(shù)據(jù)但是類似文件的對象,因此可以像操作文件對象一樣操作Blob對象。
Blob與ArrayBuffer的區(qū)別是,除了原始字節(jié)以外它還提供了mime type作為元數(shù)據(jù),Blob和ArrayBuffer之間可以進(jìn)行轉(zhuǎn)換。
Blob.arrayBuffer()
與FileReader.readAsArrayBuffer()類似,但是Blob.arrayBuffer()返回的是一個(gè)promise實(shí)例,而不是需要通過onload監(jiān)聽。
const bufferPromise = blob.arrayBuffer();
blob.arrayBuffer().then(buffer => /* 處理 ArrayBuffer 數(shù)據(jù)的代碼……*/);
var buffer = await blob.arrayBuffer();
Blob.slice()
Blob.slice() 方法用于創(chuàng)建一個(gè)包含源 Blob的指定字節(jié)范圍內(nèi)的數(shù)據(jù)的新 Blob 對象。
Blob.slice(<start, end, contentType>)
start和end分別表示需要截取的下標(biāo)(Blob.length),contentType代表截取后想要賦予新的數(shù)據(jù)片段的類型。
Blob.stream()
讀取Blob對象,詳見Blob.stream
Blob.text()
text() 方法返回一個(gè) Promise 對象,包含 blob 中的內(nèi)容,使用 UTF-8 格式編碼。
const textPromise = blob.text();
blob.text().then(text => /* 執(zhí)行的操作…… */);
var text = await blob.text();
分片上傳
核心思想是借助Blob.slice()對原始文件進(jìn)行切片,然后通過http進(jìn)行并發(fā)傳輸,當(dāng)所有切片傳輸完畢后,通知后端進(jìn)行合并,這里需要對切片進(jìn)行編號處理,以保證在合并的時(shí)候有正確的順序。
if(file){
// 設(shè)置分片大小
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))
}
// 創(chuàng)建請求,并并發(fā)發(fā)送
const requestList = blobList.map((it, i) => {
return () => {const formData = new FormData()
formData.append("file", it)
formData.append("hash", i.toString())
testRequest(formData)
}
}
)
// 等到所有分片數(shù)據(jù)都發(fā)送完畢后,發(fā)送一個(gè)合并分片的請求
Promise.all(requestList.map(it => it())).then(() => {
console.log("發(fā)送合并文件請求")
})
}
這里更好的做法是把每一份分片生成一個(gè)hash值來做唯一標(biāo)識。
使用spark-md5將文件轉(zhuǎn)換為hash:
npm i spark-md5
// 獲取apk的md5
var fileReader = new FileReader()
var spark = new SparkMD5() // 創(chuàng)建md5對象(基于SparkMD5)
fileReader.readAsBinaryString(myfile) // myfile 對應(yīng)上傳的文件
// 文件讀取完畢之后的處理
fileReader.onload = (e) => {
console.log('獲取文件的md5')
spark.appendBinary(e.target.result)
const md5 = spark.end()
console.log(md5)
由于讀取文件、生成hash這一步驟比較耗時(shí)間,可能會造成頁面卡死,推薦使用web-worker處理:
// 導(dǎo)入腳本
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()
斷點(diǎn)續(xù)傳
核心思路:需要服務(wù)端/前端記住上次暫停時(shí)上傳到哪個(gè)hash位置了。
改造request
如果要實(shí)現(xiàn)斷點(diǎn)續(xù)傳,就需要用到請求的一些特殊能力:
- 取消未完成請求。
- 獲取請求成功失敗情況,取得成功請求和失敗請求的隊(duì)列。
- 開始上傳時(shí),生成一個(gè)上傳隊(duì)列,將上傳過程中的請求(或請求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"),
}
}
- 上傳過程中,將已上傳成功的文件分片從隊(duì)列中刪除
importBackListFn().then((res) => {
if(res){
requestList = requestList.filter(it => it.hash !== res.hash)
}
})
- 暫停時(shí),將隊(duì)列中的所有請求都取消,
xhr.abort()或者new AbortController(),并清空上傳隊(duì)列
const handleAbort = () => {
// 原生
xhr.abort()
// fetch
const controller = new AbortController()
controller.abort()
requestList = []
}
- 重新續(xù)傳時(shí),通過后端請求取到已上傳的隊(duì)列,與本地文件分片對比,重新生成待上傳的隊(duì)列
// 請求已上傳文件列表
getFileListHash().then(res => {
blobList = blobList.filter(it => res.findIndex(ite => ite.hash === it.hash) !== -1)
})
// 繼續(xù)上傳blobList
參考上面的分片請求方法
分片上傳/斷點(diǎn)續(xù)傳如何控制上傳進(jìn)度條
- 需要新增一個(gè)變量存儲已上傳成功的文件列表,包含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計(jì)算百分比即可。
const process = hasuploadFile.map(it => it.size).reduce((pre, cur) => pre + cur)/file.size;
下載
Blob形式
這里的object參數(shù)是用于創(chuàng)建URL的File對象、Blob 對象或者 MediaSource 對象,生成的鏈接就是以blob:開頭的一段地址,表示指向的是一個(gè)二進(jìn)制數(shù)據(jù)。
其中l(wèi)ocalhost:1234是當(dāng)前網(wǎng)頁的主機(jī)名稱和端口號,也就是location.host,而且這個(gè)Blob URL是可以直接訪問的。需要注意的是,即使是同樣的二進(jìn)制數(shù)據(jù),每調(diào)用一次URL.createObjectURL()方法,就會得到一個(gè)不一樣的Blob URL。這個(gè)URL的存在時(shí)間,等同于網(wǎng)頁的存在時(shí)間,一旦網(wǎng)頁刷新或卸載,這個(gè)Blob URL就失效。
通過URL.revokeObjectURL(objectURL)可以釋放 URL 對象。當(dāng)你結(jié)束使用某個(gè) URL 對象之后,應(yīng)該通過調(diào)用這個(gè)方法來讓瀏覽器知道不用在內(nèi)存中繼續(xù)保留對這個(gè)文件的引用了,允許平臺在合適的時(shí)機(jī)進(jìn)行垃圾收集。
const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl
需要在頁面上創(chuàng)建a標(biāo)簽元素,并模擬點(diǎn)擊
let arch = window.document.createElement("a")
if (arch.href) {
window.URL.revokeObjectURL(arch.href)
}
arch.href = objectURL
arch.download = filename || "juicy"
arch.click()
base64形式
轉(zhuǎn)成Blob再下載。
或者
base64拼成的鏈接可以直接通過a標(biāo)簽下載:

