寫在前面
本文主要是講了菜雞作者(我)在面對文件上傳的需求的時候的一些解決方法,如果有問題或者有更改的方法歡迎大家指出。
文件上傳
事情的起因是項目中有個需求是上傳文件,然后文件上傳大家都知道直接丟formData里面給后端就好了
cosnt formData = new FormData(),
formData.append('file', file);
// 發送文件
this.sendFile(formData)
二進制文件流上傳
本以為這樣就結束了,但是后來開始聯調的時候才發現后端需要的是二進制文件。完蛋沒有碰到過這種情況呀,二進制?是要把文件轉換成二進制文件么,馬上就去查了一下,了解到了上傳文件時input會返回上傳的文件的FileList對象,每一個文件是其中的一個File對象,然后有一個FileReader對象可以讀取文件,然后他有readAsArrayBuffer()的方法可以把讀到的內容轉換成ArrayBuffer對象,然后FileReader讀取文件是異步的,在讀取結束的時候回觸發onload事件通過這些就可以得到下面的代碼
// 新建一個FileReader對象
const reader = new FileReader()
// 調用readAsArrayBuffer方法把File轉換成ArrayBuffer
reader.readAsArrayBuffer(file)
// 監聽轉換完成
reader.onload = function () {
// binary為得到的二進制文件
const binary = this.result
// 發送文件
this.sendBinary(binary)
}
其實在過程中有個小插曲,發送binary給后端的時候一直給我返回格式不對的錯誤,然后我就試了各種方法,懷疑自己文件類型轉換錯了,最后才查出是后端的bug...
大文件上傳
再次以為結束了,后來后端說在他那上傳瀏覽器崩潰了。然后給了我文件,然后我上傳了一下發現
Chrome上傳一個200M的文件就直接崩潰了,后來確認了一下需求需要支持大文件的上傳,又開了一個新坑,開始研究大文件一般怎么上傳,和后端溝通后,后端那可以提供一個分割文件上傳的接口,我可以把文件拆分成2M大小的一個個小片段分開上傳。然后通過研究得到前端這邊的需要做的是,直接把之前的文件通過slice進行拆分,通過循環一次一次的發送請求,告訴后端一個開始和結束的請求,和當前文件的編號,后端對拆分的文件進行拼接
// 新建一個FileReader對象
const reader = new FileReader()
// 調用readAsArrayBuffer方法把File轉換成ArrayBuffer
reader.readAsArrayBuffer(file)
// 監聽轉換完成
reader.onload = function () {
// binary為得到的二進制文件
const binary = this.result
// 這里定義拆分后的文件為2M一個
const blockSize = 2 * 1024 * 1024
// 如果文件小於2M就直接上傳
if (file.size <= blockSize) {
// 上傳文件
this.sendBinary(binary)
} else {
// 記錄分割后的文件的順序,后端按照順序進行拼接
let num = 1
// 文件的標識,可以隨機生產一個隨機數,帶有這個標識的是同一個文件
const id = new Date().getTime()
// 當前分割的文件的位置,初始為單個拆分文件的大小
let nextSize = blockSize
// 循環到分割文件的位置是文件的大小截止
while (file.size > nextSize) {
// 當前分割的文件的位置,因為在最后一個的時候不一定是2M大小的文件,最后一個位置是文件的大小
nextSize = Math.min(num * blockSize, file.size)
// 分割文件,上一次的位置到當前文件的位置
const slice = binary.slice((num - 1) * blockSize, nextSize)
// 開始上傳
const param = new FormData()
// 傳一個文件的序號
param.append('chunk', num)
// 傳分割后的文件
param.append('slice', slice)
// 傳文件的唯一標識
param.append('id', id)
// 如果是最后一次,傳給后端一個結束的標志
if (num * blockSize >= file.size) {
param.append('status', 'end')
}
num++
// 發送請求
this.sendFileSlice(param)
}
}
}
后來發現還是有問題,看接口文檔才知道,這回后端接受的分割后的類型是File類型,emmm沒辦法,我又找到了一個File的構造器,可以把通過ArrayBuffer創建一個File對象。然后去MDN找到了文檔。
var myFile = new File(bits, name[, options]);
bits:ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 對象的 Array — 或者任何這些對象的組合。這是 UTF-8 編碼的文件內容。
name:USVString,表示文件名稱,或者文件路徑。
這不就把我得到的slice傳過去就好了,然后我就
const data = new File(slice, 'test.txt')
然后報錯
Uncaught TypeError: Failed to construct 'File': Iterator getter is not callable.
難道是MDN出錯了,然后我仔細觀察了一下他的示例,發現他傳的參數是一個數組
var file = new File(["foo"], "foo.txt", {
type: "text/plain",
});
我就試了一下改成數組,沒先到就可以了
const data = new File([slice], 'test.txt')
原來他對bits的描述非常容易讓人誤解,看英文版更容易理解,然后我把他的翻譯更改了一下改成了
一個包含ArrayBuffer,ArrayBufferView,Blob,或者 DOMString 對象的 Array — 或者任何這些對象的組合。這是 UTF-8 編碼的文件內容。
現在大家在MDN看到的應該是我更改后的版本了。
到此問題就算是解決了。直到寫這篇文章的時候我才發現File對象從Blob接口繼承了Blob.slice的方法,其實可以直接拿File來切割,我把他轉成了ArrayBuffer然后切割然后轉回成File,這么看來非常沒必要。如果有同樣需求的同學可以直接拿File切割。
寫在最后
這次上傳文件的需求折磨了我幾天的時間,無論是去了解二進制文件的轉換還是切割上傳文件還是掉在后端的bug里都花費了很長的時間,其實現在寫文章的時候看來,這個需求也不過如此,無非就是用一下關於文件的一些API,其實最開始主要就是沒有研究過這方面的API,導致之前除了很多問題,所以感覺自己的知識儲備還是需要加強的。
其次還有一個大問題是沒有及時的和后端進行溝通,首先是沒有和后端確認接口的格式,沒有確定准確的需求,還有是出現問題只是自己埋頭找答案沒有去問問是不是后端那的問題,應該和后端溝通一下,這樣會更快地找到問題的原因。