js大文件分片上傳

大文件上傳

大文件使用一次請求發(fā)送的話,那么這個請求的時間就會非常的長。一旦請求的過程中出現(xiàn)一些問題,比如網(wǎng)絡(luò)斷開了,那么你就不得不把整個文件重新上傳一遍。這樣的代價是用戶接受不了的,也非常的浪費(fèi)資源。所以我們在做大文件上傳的時候,我們往往會對文件進(jìn)行分片。

分片原理

在客戶端,首先把整個的大文件數(shù)據(jù),分成一個一個的數(shù)據(jù)小塊,你可以把每一塊想象成為單獨的一個小文件,然后利用單文件上傳把這些小文件依次傳到服務(wù)器,當(dāng)最后把文件全部傳輸完成之后在服務(wù)器端使用程序把整個文件的小數(shù)據(jù)組裝起來,形成一個完成的文件。這個組裝由后端完成!

在這個過程中前端要做到最核心的就是要把文件進(jìn)行分片,分成一個一個的小塊。所以大文件的上傳的第一個核心技術(shù)點就是文件如何來分片。

舉例分析

// splitFile.html
<html>
    <head> </head>
    <body>
        <input type="file"/>
        <script>
            const inp = document.querySelector('input');
            inp.onchange = (e) => {
                const file = inp.files[0];
                if(!file) return;
                console.log(file);
            }
        </script>
    </body>
</html>

在瀏覽器中打開這個例子,這里有一個input元素,然后監(jiān)聽它的onchange事件,拿到它選擇的文件。選擇一個文件,我們得到的是一個File對象

file-objc.jpg

接下來我們對這個File對象進(jìn)行切片,這個對象里面有一個函數(shù)叫做slice,它跟數(shù)組的slice一個用法,它的起始和結(jié)束就表示說從這個文件第多少個字節(jié)開始取,取到第多少個字節(jié)。file.slice(0,100)表示取該文件的0-99個字節(jié),得到一個文件的切片數(shù)據(jù)。修改onchange方法如下:

const file = inp.files[0];
if(!file) return;
// 0-99個字節(jié)
const piece = file.slice(0,100);
console.log(piece);

打印結(jié)果我們發(fā)現(xiàn)分片后的數(shù)據(jù)是一個Blob類型,我們知道Blob類型它也是表示文件數(shù)據(jù)的,也就是說用ajax請求的時候可以直接把它發(fā)送到服務(wù)器。它的用法和File對象的用法是一樣的。

有了slice函數(shù)我們就可以非常輕松的完成切片,我們寫一個工具函數(shù),并在拿到File對象后使用該函數(shù)進(jìn)行分割如下:

<html>
    <head></head>
    <body>
        <input type="file"/>
        <script>
            const inp = document.querySelector('input');
            inp.onchange = (e) => {
                const file = inp.files[0];
                if(!file) return;
                //這里一次分割50k,獲取到分片結(jié)果
               const chunks = createChunks(file, 50 *1024);
               console.log(chunks);
            }

            /*
                file:File對象,
                chunkSize: 切片的大小
            */
            function createChunks(file, chunkSize) {
                const result = [];
                for (let i = 0; i < file.size; i+=chunkSize) {
                  result.push(file.slice(i, i + chunkSize));  
                }
                return result;
            }
        </script>
    </body>
</html>

打印一下,我們已經(jīng)獲取到了分片的結(jié)果是一個Blob數(shù)組。分片是很快的,原因是FileBlob對象其實只是保存了文件的基本信息,比如說File對象文件有多大、文件是什么類型、文件的名字和位置等這些基本信息。它并沒有保存文件的數(shù)據(jù)。Blob也是一樣的,它保存了數(shù)據(jù)的大小和類型。所以分片其實就是一個簡單的數(shù)學(xué)運(yùn)算。

到這里分片就結(jié)束了,我們真正讀取數(shù)據(jù)的時候需要利用FileReader才能真正的把它們的數(shù)據(jù)讀出來。

斷點續(xù)傳

有這樣一個場景,我們在分片上傳的時候網(wǎng)絡(luò)不好或斷網(wǎng)了,我們下一次要接著上傳之前上傳過的分片,之前上傳過的分片就不用再上傳了。這種文件秒傳是怎么操作的呢?看一下它的基本原理:

斷點續(xù)傳.jpg

它其實就是跟服務(wù)器的一次對話,重新上傳同一個文件時會先訪問一下服務(wù)器,告訴服務(wù)器我要給你上傳一個文件,你告訴我一下這個文件我上傳過了沒有,還有哪些分片還沒有上傳。服務(wù)器會返回對應(yīng)的結(jié)果,告訴客戶端之前已經(jīng)上傳過了哪些分片了,還有哪些編號的分片還需要傳遞。

通過一次Ajax請求,客戶端就能夠知道這個文件我該如何處理,那么在這個交互過程中客戶端必須要告訴服務(wù)器一個關(guān)鍵的信息,一個能夠唯一代表這個文件的東西就是文件的hash值。hash是一種算法,它可以把任何數(shù)據(jù)換算成一個固定長度的字符串,這個字符串是不可逆的。

所以在上傳文件的時候需要生成一個hash值,并在服務(wù)器記錄一下,在下一次重傳的時候我再告訴你這個hash值之前有沒有傳過,還有哪些分片需要上傳,通過這個hash值就能夠代表整個的文件內(nèi)容。這里我們使用md5作為它的hash算法,接下來的關(guān)鍵點就是我們?nèi)绾卧诳蛻舳擞嬎愠鲞@個文件的hash值,我們可以使用一個第三方庫spark-md5來做這個事。

<html>
    <head> </head>
    <body>
        <input type="file"/>
        <script src="./spark-md5.js"></script>
        <script>
            const inp = document.querySelector('input');
            inp.onchange = (e) => {
                const file = inp.files[0];
                if(!file) return;
                //這里一次分割50k,獲取到分片結(jié)果
                const chunks = createChunks(file, 50 *1024);
                hash(chunks);
            }

            /*
                生成文件hsah值,
                為了防止文件過大,采用分塊增量算法
            */
            function hash(chunks) {
                const spark = new SparkMD5();
                // 遞歸函數(shù)
                function _read(i) {
                    if(i >= chunks.length) {
                        console.log(spark.end());
                        return; //讀取完成
                    }
                    const blob = chunks[i];
                    const reader = new FileReader();
                    reader.onload = e => {
                        // 讀取到的字節(jié)數(shù)組
                        const bytes = e.target.result;
                        spark.append(bytes);
                        _read(i+1);
                    }
                    reader.readAsArrayBuffer(blob);
                }

                _read(0);
            }

            /*
                file:File對象,
                chunkSize: 切片的大小
            */
            function createChunks(file, chunkSize) {
                const result = [];
                for (let i = 0; i < file.size; i+=chunkSize) {
                  result.push(file.slice(i, i + chunkSize));  
                }
                return result;
            }
        </script>
    </body>
</html>

上面代碼中,為了防止文件過大,我們采用分塊增量算法來獲取hash值。修改代碼選擇文件,我們就可以打印這個文件的hash值了。如果文件過大計算量也將變大,就沒切片時那么的快了。

接下來我們把hash函數(shù)封裝成一個異步函數(shù),讓它返回一個Promise:

<html>

<head></head>

<body>
    <input type="file" />
    <script src="./spark-md5.js"></script>
    <script>
        const inp = document.querySelector('input');
        inp.onchange = async (e) => {
            const file = inp.files[0];
            if (!file) return;
            //這里一次分割50k,獲取到分片結(jié)果
            const chunks = createChunks(file, 50 * 1024);
            const result = await hash(chunks);
            console.log(result);
        }

        /*
            生成文件hsah值,
            為了防止文件過大,采用分塊增量算法
        */
        function hash(chunks) {
            return new Promise((resolve) => {
                const spark = new SparkMD5();
                // 遞歸函數(shù)
                function _read(i) {
                    if (i >= chunks.length) {
                        resolve(spark.end());
                        return; //讀取完成
                    }
                    const blob = chunks[i];
                    const reader = new FileReader();
                    reader.onload = e => {
                        // 讀取到的字節(jié)數(shù)組
                        const bytes = e.target.result;
                        spark.append(bytes);
                        _read(i + 1);
                    }
                    reader.readAsArrayBuffer(blob);
                }

                _read(0);
            })

        }

        /*
            file:File對象,
            chunkSize: 切片的大小
        */
        function createChunks(file, chunkSize) {
            const result = [];
            for (let i = 0; i < file.size; i += chunkSize) {
                 // slice越界則取數(shù)據(jù)最大長度
                result.push(file.slice(i, i + chunkSize));
            }
            return result;
        }
    </script>
</body>

</html>

為了防止主線程卡死,我們一般不會放到主線程里的,我們可以利用web worker單獨去開一個線程。因為這個操作時CPU密集型任務(wù),如果放到單獨線程還是卡頓,可能是特別大的文件造成的,這個時候可以先粗略的對這個文件分成一些大塊,單獨去計算每個大塊,比如每個大塊有300M的數(shù)據(jù)量,將這個大塊再分小塊,這樣計算它的hash是非常快的。當(dāng)有空閑的時候再去慢慢計算后邊的hash值,因為后邊的hash值還不著急用。

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

相關(guān)閱讀更多精彩內(nèi)容

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