天天看點

前端實作檔案的斷點續傳

前端實作檔案的斷點續傳

早就聽說過斷點續傳這種東西,前端也可以實作一下

斷點續傳在前端的實作主要依賴着html5的新特性,是以一般來說在老舊浏覽器上支援度是不高的

本文通過斷點續傳的簡單例子(前端檔案送出+後端php檔案接收),了解其大緻的實作過程

還是先以圖檔為例,看看最後的樣子

前端實作檔案的斷點續傳

一、一些知識準備

斷點續傳,既然有斷,那就應該有檔案分割的過程,一段一段的傳。

以前檔案無法分割,但随着html5新特性的引入,類似普通字元串、數組的分割,我們可以可以使用slice方法來分割檔案。

是以斷點續傳的最基本實作也就是:前端通過filelist對象擷取到相應的檔案,按照指定的分割方式将大檔案分段,然後一段一段地傳給後端,後端再按順序一段段将檔案進行拼接。

而我們需要對filelist對象進行修改再送出,在之前的文章中知曉了這種送出的一些注意點,因為filelist對象不能直接更改,是以不能直接通過表單的.submit()方法上傳送出,需要結合formdata對象生成一個新的資料,通過ajax進行上傳操作。

二、實作過程

這個例子實作了檔案斷點續傳的基本功能,不過手動的“暫停上傳”操作還未實作成功,可以在上傳過程中重新整理頁面來模拟上傳的中斷,體驗“斷點續傳”、

有可能還有其他一些小bug,但基本邏輯大緻如此。

1. 前端實作

首先選擇檔案,列出選中的檔案清單資訊,然後可以自定義的做上傳操作

(1)是以先設定好頁面dom結構

<!-- 上傳的表單 --> 

<form method="post" id="myform" action="/filetest.php" enctype="multipart/form-data"> 

    <input type="file" id="myfile" multiple=""> 

    <!-- 上傳的檔案清單 --> 

    <table id="upload-list"> 

        <thead> 

            <tr> 

                <th width="35%">檔案名</th> 

                <th width="15%">檔案類型</th> 

                <th width="15%">檔案大小</th> 

                <th width="20%">上傳進度</th> 

                <th width="15%"> 

                    <input type="button" id="upload-all-btn" value="全部上傳"> 

                </th> 

            </tr> 

        </thead> 

        <tbody></tbody> 

    </table> 

</form> 

<!-- 上傳檔案清單中每個檔案的資訊模版 --> 

這裡一并将css樣式扔出來

body { 

    font-family: arial; 

form { 

    margin: 50px auto; 

    width: 600px; 

input[type="button"] { 

    cursor: pointer; 

table { 

    display: none; 

    margin-top: 15px; 

    border: 1px solid #ddd; 

    border-collapse: collapse; 

table th { 

    color: #666; 

table td, table th { 

    padding: 5px; 

    text-align: center; 

    font-size: 14px; 

(2)接下來是js的實作解析

通過filelist對象我們能擷取到檔案的一些資訊

前端實作檔案的斷點續傳

其中的size就是檔案的大小,檔案的分分割分片需要依賴這個

這裡的size是位元組數,是以在界面顯示檔案大小時,可以這樣轉化

// 計算檔案大小 

size = file.size > 1024 

    ? file.size / 1024  > 1024 

    ? file.size / (1024 * 1024) > 1024 

    ? (file.size / (1024 * 1024 * 1024)).tofixed(2) + 'gb' 

    : (file.size / (1024 * 1024)).tofixed(2) + 'mb' 

    : (file.size / 1024).tofixed(2) + 'kb' 

    : (file.size).tofixed(2) + 'b'; 

選擇檔案後顯示檔案的資訊,在模版中替換一下資料

// 更新檔案資訊清單 

uploaditem.push(uploaditemtpl 

    .replace(/{{filename}}/g, file.name) 

    .replace('{{filetype}}', file.type || file.name.match(/\.\w+$/) + '檔案') 

    .replace('{{filesize}}', size) 

    .replace('{{progress}}', progress) 

    .replace('{{totalsize}}', file.size) 

    .replace('{{uploadval}}', uploadval) 

); 

不過,在顯示檔案資訊的時候,可能這個檔案之前之前已經上傳過了,為了斷點續傳,需要判斷并在界面上做出提示

通過查詢本地看是否有相應的資料(這裡的做法是當本地記錄的是已經上傳100%時,就直接是重新上傳而不是繼續上傳了)

// 初始通過本地記錄,判斷該檔案是否曾經上傳過 

percent = window.localstorage.getitem(file.name + '_p'); 

if (percent && percent !== '100.0') { 

    progress = '已上傳 ' + percent + '%'; 

    uploadval = '繼續上傳'; 

顯示了檔案資訊清單

前端實作檔案的斷點續傳

點選開始上傳,可以上傳相應的檔案

前端實作檔案的斷點續傳

上傳檔案的時候需要就将檔案進行分片分段

比如這裡配置的每段1024b,總共chunks段(用來判斷是否為末段),第chunk段,目前已上傳的百分比percent等

需要提一下的是這個暫停上傳的操作,其實我還沒實作出來,暫停不了無奈ing…

前端實作檔案的斷點續傳
前端實作檔案的斷點續傳

接下來是分段過程

// 設定分片的開始結尾 

var blobfrom = chunk * eachsize, // 分段開始 

    blobto = (chunk + 1) * eachsize > totalsize ? totalsize : (chunk + 1) * eachsize, // 分段結尾 

    percent = (100 * blobto / totalsize).tofixed(1), // 已上傳的百分比 

    timeout = 5000, // 逾時時間 

    fd = new formdata($('#myform')[0]); 

fd.append('thefile', findthefile(filename).slice(blobfrom, blobto)); // 分好段的檔案 

fd.append('filename', filename); // 檔案名 

fd.append('totalsize', totalsize); // 檔案總大小 

fd.append('islastchunk', islastchunk); // 是否為末段 

fd.append('isfirstupload', times === 'first' ? 1 : 0); // 是否是第一段(第一次上傳) 

// 上傳之前查詢是否以及上傳過分片 

chunk = window.localstorage.getitem(filename + '_chunk') || 0; 

chunk = parseint(chunk, 10); 

檔案應該支援覆寫上傳,是以如果檔案以及上傳完了,現在再上傳,應該重置資料以支援覆寫(不然後端就直接追加blob資料了)

// 如果第一次上傳就為末分片,即檔案已經上傳完成,則重新覆寫上傳 

if (times === 'first' && islastchunk === 1) { 

    window.localstorage.setitem(filename + '_chunk', 0); 

    chunk = 0; 

    islastchunk = 0; 

這個times其實就是個參數,因為要在上一分段傳完之後再傳下一分段,是以這裡的做法是在回調中繼續調用這個上傳操作

前端實作檔案的斷點續傳

接下來就是真正的檔案上傳操作了,用ajax上傳,因為用到了formdata對象,是以不要忘了在$.ajax({}加上這個配置processdata: false

上傳了一個分段,通過傳回的結果判斷是否上傳完畢,是否繼續上傳

success: function (rs) { 

    rs = json.parse(rs); 

    // 上傳成功 

    if (rs.status === 200) { 

        // 記錄已經上傳的百分比 

        window.localstorage.setitem(filename + '_p', percent); 

        // 已經上傳完畢 

        if (chunk === (chunks - 1)) { 

            $progress.text(msg['done']); 

            $this.val('已經上傳').prop('disabled', true).css('cursor', 'not-allowed'); 

            if (!$('#upload-list').find('.upload-item-btn:not(:disabled)').length) { 

                $('#upload-all-btn').val('已經上傳').prop('disabled', true).css('cursor', 'not-allowed'); 

            } 

        } else { 

            // 記錄已經上傳的分片 

            window.localstorage.setitem(filename + '_chunk', ++chunk); 

            $progress.text(msg['in'] + percent + '%'); 

            // 這樣設定可以暫停,但點選後動态的設定就暫停不了.. 

            // if (chunk == 10) { 

            //     ispaused = 1; 

            // } 

            console.log(ispaused); 

            if (!ispaused) { 

                startupload(); 

        } 

    } 

    // 上傳失敗,上傳失敗分很多種情況,具體按實際來設定 

    else if (rs.status === 500) { 

        $progress.text(msg['failed']); 

}, 

error: function () { 

    $progress.text(msg['failed']); 

繼續下一分段的上傳時,就進行了遞歸操作,按順序地上傳下一分段

截個圖..

前端實作檔案的斷點續傳

這是完整的js邏輯,代碼有點兒注釋了應該不難看懂吧哈哈

// 全部上傳操作 

$(document).on('click', '#upload-all-btn', function () { 

    // 未選擇檔案 

    if (!$('#myfile').val()) { 

        $('#myfile').focus(); 

    // 模拟點選其他可上傳的檔案 

    else { 

        $('#upload-list .upload-item-btn').each(function () { 

            $(this).click(); 

        }); 

}); 

// 選擇檔案-顯示檔案資訊 

$('#myfile').change(function (e) { 

    var file, 

        uploaditem = [], 

        uploaditemtpl = $('#file-upload-tpl').html(), 

        size, 

        percent, 

        progress = '未上傳', 

        uploadval = '開始上傳'; 

    for (var i = 0, j = this.files.length; i < j; ++i) { 

        file = this.files[i]; 

        percent = undefined; 

        progress = '未上傳'; 

        // 計算檔案大小 

        size = file.size > 1024 ? file.size / 1024 > 1024 ? file.size / (1024 * 1024) > 1024 ? (file.size / 

            (1024 * 1024 * 1024)).tofixed(2) + 'gb' : (file.size / (1024 * 1024)).tofixed(2) + 'mb' : (file 

            .size / 1024).tofixed(2) + 'kb' : (file.size).tofixed(2) + 'b'; 

        // 初始通過本地記錄,判斷該檔案是否曾經上傳過 

        percent = window.localstorage.getitem(file.name + '_p'); 

        if (percent && percent !== '100.0') { 

            progress = '已上傳 ' + percent + '%'; 

            uploadval = '繼續上傳'; 

        // 更新檔案資訊清單 

        uploaditem.push(uploaditemtpl 

            .replace(/{{filename}}/g, file.name) 

            .replace('{{filetype}}', file.type || file.name.match(/\.\w+$/) + '檔案') 

            .replace('{{filesize}}', size) 

            .replace('{{progress}}', progress) 

            .replace('{{totalsize}}', file.size) 

            .replace('{{uploadval}}', uploadval)); 

    $('#upload-list').children('tbody').html(uploaditem.join('')) 

        .end().show(); 

/** 

* 上傳檔案時,提取相應比對的檔案項 

* @param  {string} filename   需要比對的檔案名 

* @return {filelist}          比對的檔案項目 

*/ 

function findthefile(filename) { 

    var files = $('#myfile')[0].files, 

        thefile; 

    for (var i = 0, j = files.length; i < j; ++i) { 

        if (files[i].name === filename) { 

            thefile = files[i]; 

            break; 

    return thefile ? thefile : []; 

// 上傳檔案 

$(document).on('click', '.upload-item-btn', function () { 

    var $this = $(this), 

        state = $this.attr('data-state'), 

        msg = { 

            done: '上傳成功', 

            failed: '上傳失敗', 

            in : '上傳中...', 

            paused: '暫停中...' 

        }, 

        filename = $this.attr('data-name'), 

        $progress = $this.closest('tr').find('.upload-progress'), 

        eachsize = 1024, 

        totalsize = $this.attr('data-size'), 

        chunks = math.ceil(totalsize / eachsize), 

        chunk, 

        // 暫停上傳操作 

        ispaused = 0; 

    // 進行暫停上傳操作 

    // 未實作,這裡通過動态的設定ispaused值并不能阻止下方ajax請求的調用 

    if (state === 'uploading') { 

        $this.val('繼續上傳').attr('data-state', 'paused'); 

        $progress.text(msg['paused'] + percent + '%'); 

        ispaused = 1; 

        console.log('暫停:', ispaused); 

    // 進行開始/繼續上傳操作 

    else if (state === 'paused' || state === 'default') { 

        $this.val('暫停上傳').attr('data-state', 'uploading'); 

    // 第一次點選上傳 

    startupload('first'); 

    // 上傳操作 times: 第幾次 

    function startupload(times) { 

        // 上傳之前查詢是否以及上傳過分片 

        chunk = window.localstorage.getitem(filename + '_chunk') || 0; 

        chunk = parseint(chunk, 10); 

        // 判斷是否為末分片 

        var islastchunk = (chunk == (chunks - 1) ? 1 : 0); 

        // 如果第一次上傳就為末分片,即檔案已經上傳完成,則重新覆寫上傳 

        if (times === 'first' && islastchunk === 1) { 

            window.localstorage.setitem(filename + '_chunk', 0); 

            chunk = 0; 

            islastchunk = 0; 

        // 設定分片的開始結尾 

        var blobfrom = chunk * eachsize, // 分段開始 

            blobto = (chunk + 1) * eachsize > totalsize ? totalsize : (chunk + 1) * eachsize, // 分段結尾 

            percent = (100 * blobto / totalsize).tofixed(1), // 已上傳的百分比 

            timeout = 5000, // 逾時時間 

            fd = new formdata($('#myform')[0]); 

        fd.append('thefile', findthefile(filename).slice(blobfrom, blobto)); // 分好段的檔案 

        fd.append('filename', filename); // 檔案名 

        fd.append('totalsize', totalsize); // 檔案總大小 

        fd.append('islastchunk', islastchunk); // 是否為末段 

        fd.append('isfirstupload', times === 'first' ? 1 : 0); // 是否是第一段(第一次上傳) 

        // 上傳 

        $.ajax({ 

            type: 'post', 

            url: '/filetest.php', 

            data: fd, 

            processdata: false, 

            contenttype: false, 

            timeout: timeout, 

            success: function (rs) { 

                rs = json.parse(rs); 

                // 上傳成功 

                if (rs.status === 200) { 

                    // 記錄已經上傳的百分比 

                    window.localstorage.setitem(filename + '_p', percent); 

                    // 已經上傳完畢 

                    if (chunk === (chunks - 1)) { 

                        $progress.text(msg['done']); 

                        $this.val('已經上傳').prop('disabled', true).css('cursor', 'not-allowed'); 

                        if (!$('#upload-list').find('.upload-item-btn:not(:disabled)').length) { 

                            $('#upload-all-btn').val('已經上傳').prop('disabled', true).css('cursor', 

                                'not-allowed'); 

                        } 

                    } else { 

                        // 記錄已經上傳的分片 

                        window.localstorage.setitem(filename + '_chunk', ++chunk); 

                        $progress.text(msg['in'] + percent + '%'); 

                        // 這樣設定可以暫停,但點選後動态的設定就暫停不了.. 

                        // if (chunk == 10) { 

                        //     ispaused = 1; 

                        // } 

                        console.log(ispaused); 

                        if (!ispaused) { 

                            startupload(); 

                    } 

                } 

                // 上傳失敗,上傳失敗分很多種情況,具體按實際來設定 

                else if (rs.status === 500) { 

                    $progress.text(msg['failed']); 

            }, 

            error: function () { 

                $progress.text(msg['failed']); 

2. 後端實作

這裡的後端實作還是比較簡單的,主要用依賴了 file_put_contents、file_get_contents 這兩個方法

前端實作檔案的斷點續傳

要注意一下,通過formdata對象上傳的檔案對象,在php中也是通過$_files全局對象擷取的,還有為了避免上傳後檔案中文的亂碼,用一下iconv

斷點續傳支援檔案的覆寫,是以如果已經存在完整的檔案,就将其删除

// 如果第一次上傳的時候,該檔案已經存在,則删除檔案重新上傳 

if ($isfirstupload == '1' && file_exists('upload/'.$filename) && filesize('upload/'.$filename) == $totalsize) { 

    unlink('upload/'.$filename); 

使用上述的兩個方法,進行檔案資訊的追加,别忘了加上 file_append 這個參數~

// 繼續追加檔案資料 

if (!file_put_contents('upload/'.$filename, file_get_contents($_files['thefile']['tmp_name']), file_append)) { 

    $status = 501; 

} else { 

    // 在上傳的最後片段時,檢測檔案是否完整(大小是否一緻) 

    if ($islastchunk === '1') { 

        if (filesize('upload/'.$filename) == $totalsize) { 

            $status = 200; 

            $status = 502; 

    } else { 

        $status = 200; 

一般在傳完後都需要進行檔案的校驗吧,是以這裡簡單校驗了檔案大小是否一緻

根據實際需求的不同有不同的錯誤處理方法,這裡就先不多處理了

完整的php部分

<?php 

    header('content-type: text/plain; charset=utf-8'); 

    $files = $_files['thefile']; 

    $filename = iconv('utf-8', 'gbk', $_request['filename']); 

    $totalsize = $_request['totalsize']; 

    $islastchunk = $_request['islastchunk']; 

    $isfirstupload = $_request['isfirstupload']; 

    if ($_files['thefile']['error'] > 0) { 

        $status = 500; 

        // 此處為一般的檔案上傳操作 

        // if (!move_uploaded_file($_files['thefile']['tmp_name'], 'upload/'. $_files['thefile']['name'])) { 

        //     $status = 501; 

        // } else { 

        //     $status = 200; 

        // } 

        // 以下部分為檔案斷點續傳操作 

        // 如果第一次上傳的時候,該檔案已經存在,則删除檔案重新上傳 

        if ($isfirstupload == '1' && file_exists('upload/'. $filename) && filesize('upload/'. $filename) == $totalsize) { 

            unlink('upload/'. $filename); 

        // 否則繼續追加檔案資料 

        if (!file_put_contents('upload/'. $filename, file_get_contents($_files['thefile']['tmp_name']), file_append)) { 

            $status = 501; 

            // 在上傳的最後片段時,檢測檔案是否完整(大小是否一緻) 

            if ($islastchunk === '1') { 

                if (filesize('upload/'. $filename) == $totalsize) { 

                    $status = 200; 

                } else { 

                    $status = 502; 

            } else { 

                $status = 200; 

    echo json_encode(array( 

        'status' => $status, 

        'totalsize' => filesize('upload/'. $filename), 

        'islastchunk' => $islastchunk 

    )); 

?> 

先到這兒~

作者:佚名

來源:51cto