前端-檔案上傳核心思想

久宇詩發表於2022-03-31

一、檔案上傳幾種方式

  1. form表單上傳
  2. iframe
  3. FormData非同步上傳

1、from 表單上傳

首先要知道我們上傳檔案時需要修改form表單的 enctype='multipart/form-data'
產生問題:
form表單提交之後會重新整理頁面
form表單上傳大檔案時,很容易遇見伺服器超時

1.1 普通上傳

<form action="http:localhost:8080/uploadFile" method="POST" enctype="multipart/form-data">
    <input type="file" name="myfile">
    <input type="submit">
</form>

1.2非同步上傳

方案1:base64上傳

通過canvas講圖片裝成base64,然後在服務端進行解碼。
base64會將原本的體積轉成4/3的體積,so會增大請求體加,浪費頻寬,上傳和解析的時間會明顯增加。

<input type="file" id='file'>
<canvas id='canvas'></canvas>
<img src="" id='target-img'>
<script>
    let canvas = document.getElementById("canvas"),
        targetImg = document.getElementById('target-img'),
        file = document.getElementById('file'),
        context = canvas.getContext('2d')

    file.onchange = function() {
        let URL = window.URL || window.webkitURL
        let dataURL = URL.createObjectURL(this.files[0]) // 建立URL物件
        let img = new Image()
        img.crossOrigin = "anonymous" // 只有伺服器模式開啟, 才有效
        img.src = dataURL
        img.onload = function() {
            URL.revokeObjectURL(this.src) //  img載入完成後,主動釋放URL物件
            canvas.width = img.width
            canvas.height = img.height
            context.drawImage(img, 0, 0, img.width, img.height)
            let dataBase64Url = canvas.toDataURL('img/png')
            targetImg.src = dataBase64Url
        }

    }
</script>
方案2:二進位制形式

除了進行base64編碼,還可以在前端直接讀取檔案內容後以二進位制格式上傳

關鍵api:
參考

  • FileReader:物件允許Web應用程式非同步讀取儲存在使用者計算機上的檔案(或原始資料緩衝區)的內容,使用 File 或 Blob 物件指定要讀取的檔案或資料。

    • File:物件可以是來自使用者在一個<input>元素上選擇檔案後返回的files物件

    • readAsBinaryString: 方法會讀取指定的 Blob 或 File 物件,當讀取完成的時候,readyState 會變成DONE(已完成),並觸發 loadend (en-US) 事件,同時result 屬性將包含所讀取檔案原始二進位制格式

  • Blob: 前端的一個專門用於支援檔案操作的二進位制物件

  • ArrayBuffer:前端的一個通用的二進位制緩衝區,類似陣列,但在API和特性上卻有諸多不同

  • Buffer:Node.js提供的一個二進位制緩衝區,常用來處理I/O操作

  • Uint8Array:型別陣列表示的8位無符號整數陣列

二進位制上傳

檔案路徑格式轉二進位制

var reader = new FileReader();//①

reader.readAsBinaryString(file);// 把從input裡讀取的檔案內容,放到fileReader的result欄位裡
reader.onload = function(){
	 readBinary(this.result) // 讀取result或直接上傳
}
// 讀取二進位制檔案
function readBinary(text){
    var data = new ArrayBuffer(text.length);//建立一個長度為text.length的二進位制快取區
    var ui8a = new Uint8Array(data, 0);
    for (var i = 0; i < text.length; i++){ 
        ui8a[i] = (text.charCodeAt(i) & 0xff);
    }
    console.log(ui8a)
}

二進位制下載
在向後端發起請求時,需要在請求頭中加上

responseType: 'blob'

這樣在返回data中可以得到一個瀏覽器可以解析的blob資料

	const downURL = window.URL.createObjectURL(new Blob([data]));
	 // data 為獲取到的二進位制資料
	const listNode = document.createElement("a");
	// 這裡注意 : 非同源a標籤的download去命名沒有用
	listNode.download = '合同公允價錯誤檔案下載.xlsx';
	listNode.style.display = "none";
	listNode.href = downURL;

2、frame上傳

低版本瀏覽器上,xhr請求不支援formdata上傳,只能form表單上傳。
form表單上傳,出現的問題上文已經提到,會本身進行頁面跳轉,產生原因為target屬性導致
target我們或多或少有些瞭解,a標籤也有改屬性:
_self:預設值,在相同的視窗中開啟響應頁面
_blank:在新視窗開啟
_parent:在父視窗開啟
_top:在最頂層的視窗開啟

實現方案
實現非同步上傳的感覺,自理我們就要用到framename去置頂名字的iframe中開啟,也就是<iframe name='formtarget'></iframe><form target='formtarget'>,這樣一來返回的資料會被iframe接收,就不會出現重新整理問題,而返回的內容可以通過iframe文字拿到。
問題:預覽圖片只有先傳給後臺,後臺再返回一個線上的地址

<iframe id="iframe1" name="formtarget" style="display: none"></iframe>
<form id="fm1" action="/app04/ajax1/" method="POST" target="formtarget" enctype="multipart/form-data">
    <input type="file" name="k3"/>
    <input type="submit">
</form>
<script>
file.onchange = function() {
    let iframe = document.getElementById('iframe1')
    iframe.addEventListener("load", function() {
        var content = this.contents().
        var data = JSON.parse(content)

    })
}
</script>

3、FormData非同步上傳

利用FormData模擬表單資料,通過ajax進行提交,可以更加靈活地傳送Ajax請求。可以使用FormData來模擬表單提交。

let files = e.target.files // 獲取input的file物件
let formData = new FormData();
formData.append('file', file);
axios.post(url, formData);

二、大檔案上傳

在同一個請求中,要上傳大量的資料,導致整個過程會比較漫長,且失敗後需要重頭開始上傳

大檔案上傳我們需要考慮三個方面:

  • 切片:拆分上傳請求
  • 斷點續傳
  • 顯示上傳進度和暫停上傳

1、切片

識別切片來源
保證切片拼接順序

  • 我們一般採用編碼的方式進行上傳,獲取檔案對應的二進位制內容。
  • 計算出內容的總大小,根據檔案大小切成對應的分片。
  • 上傳時標識出當前檔案,告訴後端上傳到了第幾個(可以用時間戳形式)。
    • 不加表示的話後端在追加切片時,無法識別切片順序
    • 介面異常的情況下無法正確拼接

實現
根據檔名、檔案長度等基本資訊進行拼接,為了避免多個使用者上傳相同的檔案,可以再額外拼接使用者資訊如uid等保證唯一性
根據檔案的二進位制內容計算檔案的hash,這樣只要檔案內容不一樣,則標識也會不一樣,缺點在於計算量比較大.
將檔案拆分成piece大小的分塊,然後每次請求只需要上傳這一個部分的分塊即可

 let file = document.querySelector("[name=file]").files[0];
const LENGTH = 1024 * 1024 * 0.1;
let chunks = sliceFile(file, LENGTH); // 首先拆分切片
chunks.forEach((chunk,index) => {

    let fd = new FormData();
    fd.append("file", chunk);
    // 傳遞context
    fd.append("context", file.name + file.length);
    // 傳遞切片索引值
    fd.append("chunk", index + 1);

    upload(fd)    
})
  function sliceFile(file, piece = 1024 * 1024 * 5) {
        let totalSize = file.size; // 檔案總大小

        let start = 0; // 每次上傳的開始位元組
        let end = start + piece; // 每次上傳的結尾位元組
        let chunks = []

        while (start < totalSize) {
            // 根據長度擷取每次需要上傳的資料
            // File物件繼承自Blob物件,因此包含slice方法
            let blob = file.slice(start, end);
            chunks.push(blob)
            start = end;

            end = start + piece;
        }
        return chunks
    }

請求

/**
 * 檔案上傳  
 * @param {} params
 */
export function upload (params) {
  const data = new FormData();
  data.append('file', params.file);
  data.append('type', params.type);
  return $axios({
    method: 'post',
    url: "/api/Files/upload",
    data: data,
    headers: {
      'Content-Type': 'multipart/form-data',
    }
  })
}

2、斷點續傳

我們在上傳或者下載檔案的時候,如果已經進行了一部分,這時候網路故障、頁面關閉的情況下,不需要從頭開始操作,而是從指定位置繼續進行操作,這種處理方式就是所說的“斷點續傳”

斷點:的由來是在下載過程中,將一個下載檔案分成了多個部分,同時進行多個部分一起的下載,當某個時間點,任務被暫停了,此時下載暫停的位置就是斷點了。
續傳:一個任務從暫停到開始時,會從上一次任務暫停處開始(可以每次傳輸成功後加一個表示為告訴前端傳輸進度)。

實現思路:

  • 儲存已上傳的切片資訊
  • 選擇未上傳的切片進行上傳
  • 全部上傳成功後後端進行檔案合併

實現方案:

  1. 本地儲存:我們可以利用localstorage,cookie等方式儲存在瀏覽器內,這種情況下我們不依賴於後端,直接本地讀取就行。清理了本地檔案,會導致上傳記錄丟失。
  2. 其實伺服器知道我們已經傳輸到了哪些切片,那些進度,我們通過介面去傳輸為上傳的切片即可。

3、上傳進度和暫停

進度:我們可以利用xhr.upload.onprogress = Function方法做進度的監聽

xhr.upload.onprogress = function(e) {
    if (e.lengthComputable) {
        var percent = Math.floor( e.loaded / e.total * 100);//進度計算
        if(percent == 100){
           
        }else{
           
        }
    }
};

暫停:如果該請求已被髮出,XMLHttpRequest.abort() 方法將終止該請求,實現上傳暫停的效果。
繼續:和斷點繼傳類似,先獲取傳輸的列表,然後重新傳送未上傳的切片。

相關文章