Node.js 解決Gzip下獲取真實的下載進度問題

Tony_Stark發表於2018-05-23

當在專案中需要獲取介面response返回資料進度的時候通常是通過以下方式操作的

    // AJAX
    
    var xhr = newXMLHttpRequest();
    xhr.open('GET','your-http-path');
    xhr.onprogress = function(event){
        if(event.lengthComputable){
            // 獲取返回報文總位元組長度
            let total = max=event.total;
            // 當前已返回位元組長度
            let loaded =event.loaded;
            // 計算下載進度
            return 100 * (loaded / total);
        }
    };
    
    // Axios
    axios.create({
        withCredentials: true,
        headers:{
            'X-Requested-With': 'XMLHttpRequest'
        },
        onDownloadProgress: p => { 
            return 100 * ( p.loaded / p.total ) 
        }
})
複製程式碼

實際上total的數值是從response headers的Content-Length獲取的,但是當後端服務或者nginx開啟gzip之後,Content-Length的長度是gzip壓縮之後的,然而loaded得到的又是解壓完實際的字串長度,所以 total 和 loaded並不能等效相除等到正確的百分比。

alt

google了很多 solution 並不能很好解決以上的問題,忽然靈機一動想到以下的solution


我使用的是Axios作為前端http請求庫
ajax 監聽Progress事件控制程式碼,回撥引數中物件event能獲得請求返回頭,
但是這個headers中只攜帶了Content-type 和 Content-Length, 我嘗試著將未壓縮的資料的大小通過設定Content-type的值攜帶過去
Content-Type: application/json;charset=utf-8;real-length=2018
結果成功的得到了以下的方案

補充1:有人會問為什麼不自定義一個headers,通過自定義的頭進行傳遞,很遺憾我試過在服務端設定自定義headers頭,但是就像上面所說的可能是從安全形度出發在progress事件中只能拿到Content-type 和 Content-Length。
補充2: 有人又會說為什麼不直接在服務端response headers中直接修改Content-Length的大小,要繞怎麼大一圈把前後端都驚動了呢,因為就算你設定Content-length為未壓縮之前的大小,
前端發起請求時,需要Content-length 和 請求實際返回的二進位制流的資料包大小是一樣的,不然請求會假死。
補充3: 在計算機編碼中一個位元組佔用8 bit(1 byte = 8 bit),而一個字元可能是一個單位元組字元,也可能是雙位元組字元。
另外,Buffer.byteLength()方法在寫http響應頭時經常要用到,如果想改寫http響應頭Cotent-Length時,千萬記得一定要用Buffer.byteLength()方法,而不要使用String.prototype.length屬性
複製程式碼

Front End Client

  • /util/axios.js
    function getDownloadProgress(event){
    	let header = e.currentTarget.getResponseHeader('Content-Type'),
    	arr = header.split(';'),
    	realLenghtArr = arr[2] && arr[2].split('=') || [],
    	realLenght = realLenghtArr[1] || 0,
    	progress = ( + e.loaded ) / (+ realLenght) * 100;
    	return progress
    }
    axios.create({
        withCredentials: true,
        headers:{
            'X-Requested-With': 'XMLHttpRequest'
        },
        onDownloadProgress: e => { 
           let progress = getDownloadProgress(e);
           return progress;
        }
    })
複製程式碼

Node.js Servers

  • /middleware/gzip.js
    // 關鍵程式碼
    let realLength = Buffer.byteLength(ctx.body, 'utf8');
    ctx.set({
        'Content-Type': `application/json;charset=utf-8;real-length=${ realLength }`
    });
    // gzip
    let buf = await zlib.gzipSync(ctx.body);
    ctx.body = buf;
複製程式碼

相關文章