重新整理檔案上傳

Aaron發表於2021-10-01

背景

檔案上傳對於前端來說應該是既陌生又熟悉,每次在做檔案上傳的時候無論是檔案上傳圖片還是上傳其他型別檔案,如果檔案相對來說比較小的情況可以把檔案轉換成檔案流傳輸到伺服器,為了能夠更好的完善上傳檔案功能,做了一些調研並整理一了下。

瞭解File物件

目前前端暫不支援操作本地檔案,所以只能使用者主動觸發才能獲取到使用者所選擇的File物件。使用者可以通過三種方法操作觸發:

  1. 通過input type="file"選擇本地檔案
  2. 通過拖拽的方式把檔案拖過來
  3. 在編輯框裡面複製貼上
通過input獲取

當然了第一種方法是目前前端使用最為普遍的,通過input的型別,可以快速拿到使用者所選擇的File物件。

HTML程式碼如下:

<form>
  <input type="file" id="fileInput" name="fileContent">
</form>

然後通過FormData(文末會稍加解釋)物件獲取到整個表單的內容:

document.getElementById("fileInput").onchange = function(){
  let formData = new FormData(this.form);
  formData.append("fileName", this.value);
  console.log(this.value);
  console.log(formData);
}

程式碼中分別列印了input.valueformDatainput.value所列印出來的是一個虛擬的路徑,是無法通過或者路徑訪問到使用者所選擇的檔案的。然而formData列印出來的則是一個空物件,我們所看到的是空物件,並不代表整個物件就是空的,只是瀏覽器對該物件進行了出了,無法對檔案進行操作,只能通過append新增一些欄位。

//  FormData
{
    __proto__: FormData
}

說了這麼多還是沒有說到File物件,其實當使用者選擇完檔案之後,File物件的例項就已經建立了,存放到了對應input DOMfiles中。

在使用input type="file"的時候,可以在瀏覽器上看到一個瀏覽器預設的按鈕,貌似看起來不是那麼特別的友好。筆者對於這個問題處理如下:

<button id="btn"></button>
document.getElementById("btn").onclick = function(){
    const oInput = document.createElement("input");
    oInput.setAttribute("type","file");
    oInput.click();
    oInput.onchange = function(){
        console.log(this.files[0])
    }
}
//  File輸出結果
{
    lastModified: 1600000000000,
    lastModifiedDate: Thu Sep 30 2021 15:11:10 GMT+0800 (中國標準時間),
    name: "logo.jpg",
    size: "20000",
    type: "image/jpg",
    webkitRelativePath: "",
    __proto__: File
}

當然File只是存放於input DOM中,使用哪種方式獲取都是可以的。我們所看到的File物件,其實是File的例項,包含了修改時間,檔名、檔案大小等資訊。

由於我們所獲取到的File物件,所以沒有辦法直接展示在頁面中,但是像圖片這種檔案又需要預覽,我們就需要用到FileReader(文末介紹)物件對File物件來進一步處理。

通過例項化FileReader調它的readAsDataURL並把File物件傳給它,監聽它的onload事件,load完讀取的結果就在它的result屬性裡了。它是一個base64格式的,可直接賦值給一個imgsrc

document.getElementById("btn").onclick = function(){
    const oInput = document.createElement("input");
    oInput.setAttribute("type","file");
    oInput.click();
    oInput.onchange = function(){
        let fileReader = new FileReader();
        let { type:fileType } = this.files[0];
        fileReader.onload = function(){
            if(/^image/.test(fileType)){
                const img = document.createElement("img");
                console.log(this.result);
                img.setAttribute("src",this.result);
                document.body.appendChild(img);
            }
        }
        fileReader.readAsDataURL(this.files[0]);
    }
}

使用FileReader除了可讀取為base64之外,還能讀取為以下格式:

// 按base64的方式讀取,結果是base64,任何檔案都可轉成base64的形式
fileReader.readAsDataURL(this.files[0]);

// 以二進位制字串方式讀取,結果是二進位制內容的utf-8形式,已被廢棄了
fileReader.readAsBinaryString(this.files[0]);

// 以原始二進位制方式讀取,讀取結果可直接轉成整數陣列
fileReader.readAsArrayBuffer(this.files[0]);

其它的主要是能讀取為ArrayBuffer,它是一個原始二進位制格式的結果。它對前端開發人員也是透明的,不能夠直接讀取裡面的內容,但可以通過ArrayBuffer.length得到長度,還能轉成整型陣列,就能知道檔案的原始二進位制內容。

Drop讀取檔案

通過Drop如何才能讀取到檔案內容呢?如果說通過input是傳統的話,那麼通過Drop獲取檔案就只能說是流行了。

HTML:

<div class="drop-container">
    drop your image here
</div>

javascript:

const onImageDrop = document.getElementById("img-drop");
onImageDrop.addEventListener("dragover",function(event){
  event.preventDefault();
})
onImageDrop.addEventListener("drop", function(event){
  event.preventDefault();
  console.log(event);
  let file = event.dataTransfer.files[0];
  let fileReader = new FileReader();
  let { type:fileType } = file;
  fileReader.onload = function(){
    if(/^image/.test(fileType)){
      const img = document.createElement("img");
      img.setAttribute("src",this.result);
      document.body.appendChild(img);
    }
  }
  fileReader.readAsDataURL(file);
  let formData = new FormData();
  formData.append("fileContent", file);
});

資料在drop事件的event.dataTransfer.files裡面,拿到這個File物件之後就可以和輸入框進行一樣的操作了,即使用FileReader讀取,或者是新建一個空的formData,然後把它appendformData裡面。

貼上讀取檔案

還有一種方式則是通過貼上的形式獲取到檔案內容,這種讀取檔案的方式,通常實在一個編輯框裡面操作,把divcontenteditable設定為true:

<div contenteditable="true">
  hello, paste your image here
</div>

貼上的資料是在event.originalEvent.files裡面:

document.getElementById("editor").addEventListener("paste",function(event){
  let file = event.clipboardData.files[0];
  console.log(file)
});

檔案上傳

通過三種方法都可以獲取到File物件,目前對於前端來說有兩種常用的上傳檔案方法方法。

  1. 整檔案上傳
  2. 切片上傳
整檔案上傳

其實對於上傳整檔案相對來說是比較簡單的,因為不需要太多的操作,通過FormData物件,把相應的檔案傳輸給對應的地址即可。

document.getElementById("btn").onclick = function(){
    const oInput = document.createElement("input");
    oInput.setAttribute("type","file");
    oInput.click();
    oInput.onchange = function(){
        const formdata = new FormData();
        formdata.append("file",file);
        const xhr = new XMLHttpRequest();
        xhr.open("post","上傳檔案地址");
        //獲取上傳的進度
        xhr.upload.onprogress = function (event) {
            if(event.lengthComputable){
                //  進度
                const percent = event.loaded/event.total *100;
            }
        }
        //將formdata上傳
        xhr.send(formdata);
    }
}
切片上傳

檔案太大的時候使用普通方式上傳就不太靠譜了,長時間的等待回讓使用者失去耐心,甚至導致使用者的流失。這個時候就需要用到切片上傳,把檔案切割成幾個小的檔案,分別上傳到服務端。切片上傳相對普通檔案上傳來說難度要大一些,因為涉及到檔案分割,和後端的配合,這裡只講述前端內容,對後端如何實現不做贅述(後續會使用node實現)。

document.getElementById("btn").onclick = function(){
  const oInput = document.createElement("input");
  oInput.setAttribute("type","file");
  oInput.click();
  oInput.onchange = function(){
    const file = oInput.files[0];
    const perFileSize = 2097152;
    const blobParts = Math.ceil(file.size / perFileSize);
    let progress = 0;
    let blobSize = 0;
    for (let i = 0; i < blobParts; i++) {
      const formData = new FormData();
      const _blob = file.slice(i * perFileSize, (i + 1) * perFileSize);
      formData.append('_blob', _blob);
      formData.append('filename', file.name);
      formData.append('index', i + 1);
      formData.append('total', blobParts);
      const xhr = new XMLHttpRequest();
      xhr.open("post","上傳檔案地址");
      xhr.onload = function onload() {
        blobSize += _blob.size;
        //  進度
        progress = parseInt((blobSize / file.size) * 100);
      };
      //    將formdata上傳
      xhr.send(formdata);
    };
  }
}

上述內容中主要是通過file.slice對檔案進行切割拆分,獲取到切割後的Blod(文末介紹)物件,然後把Blod物件傳輸給後端即可,接下來就是後端對傳輸的內容進行處理了,這裡暫時不做贅述。

結束語

對於這次檔案上傳的學習學到了很多東西,雖然都是基礎性的東西,但是還是很有用的,對於元件的封裝以及檔案上傳工具的封裝,都是有很大的幫助的。

上面也只是舉了一些簡單的例子,具體的業務邏輯還是需要具體的分析的。很多東西並不是一概而論的。


注:關於FormData

FormData型別其實是在XMLHttpRequest2級定義的,它是為序列化表以及建立與表單格式相同的資料(當然是用於XHR傳輸)提供便利。FormData裡面儲存的資料形式,一對key/value組成一條資料,key是唯一的,一個key可能對應多個value。如果是使用表單初始化,每一個表單欄位對應一條資料,它們的HTML name屬性即為key值,它們value屬性對應value值。

  1. 通過append(key, value)新增資料;
  2. 通過get(key)/getAll(key)獲取對應的value;
  3. 通過set(key, value)設定修改資料;
  4. 通過has(key)判斷是否對應的key值;
  5. 通過delete(key)刪除資料;

注:關於FileReader

FileReader是前端進行檔案處理的一個重要的web api,特別是在對圖片的處理上。FileReader物件允許Web應用程式非同步讀取儲存在使用者計算機上的檔案(或原始資料緩衝區)的內容,使用FileBlob物件指定要讀取的檔案或資料。

FileReader讀取檔案方法如下:

  1. readAsText(file, encoding):以純文字形式讀取檔案,讀取到的文字儲存在result屬性中。第二個引數代表編碼格式;
  2. readAsDataUrl(file):讀取檔案並且將檔案以資料URI的形式儲存在result屬性中;
  3. readAsBinaryString(file):讀取檔案並且把檔案以字串儲存在result屬性中;
  4. readAsArrayBuffer(file):讀取檔案並且將一個包含檔案內容的ArrayBuffer儲存咋result屬性中;

FileReader事件監控

  1. progress:每隔50ms左右,會觸發一次progress事件;
  2. error:在無法讀取到檔案資訊的條件下觸發;
  3. load:在成功載入後就會觸發;

注:關於Blod

Blob物件表示一個不可變的,原始資料的類似檔案物件。Blob表示的資料不一定是一個JavaScript原生格式blob物件本質上是js中的一個物件,裡面可以儲存大量的二進位制編碼格式的資料。

Blob屬性:

  1. isClosed 是否在該物件上呼叫過
  2. size 物件中所包含資料的大小
  3. type 物件所包含資料的MIME型別

Blob方法:

  1. close 關閉 Blob 物件,以便能釋放底層資源
  2. slice 返回一個新的 Blob 物件,包含了源 Blob 物件中指定範圍內的資料