js-上傳/下載文件

上傳

為什么常用FormData對象來上傳file

還可以用base64, 見下文。

Content-Type

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è)文件的描述對象,并不是文件本身:


image.png

FormData

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

image.png

創(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")
          }
image.png

如果是在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")
          }
image.png

只讀屬性

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

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對象下,還包含兩種視圖:TypedArrayDataView

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()

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>)

startend分別表示需要截取的下標(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ì)列。
  1. 開始上傳時(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"), 
        }
    }
  1. 上傳過程中,將已上傳成功的文件分片從隊(duì)列中刪除
importBackListFn().then((res) => {
  if(res){
      requestList = requestList.filter(it => it.hash !== res.hash)
  }
})
  1. 暫停時(shí),將隊(duì)列中的所有請求都取消,xhr.abort()或者new AbortController(),并清空上傳隊(duì)列
const handleAbort = () => {
  // 原生
    xhr.abort()
  // fetch
    const controller = new AbortController()
    controller.abort()

    requestList = []
}
  1. 重新續(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)度條

  1. 需要新增一個(gè)變量存儲已上傳成功的文件列表,包含hash和size即可。
const hasuploadFile;
importBackListFn().then((res) => {
  if(res){
      requestList = requestList.filter(it => it.hash !== res.hash)
      hasuploadFile.push(res)
  }
})
  1. 通過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)簽下載:


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容