早就聽說過斷點續傳這種東西,前端也可以實作一下
斷點續傳在前端的實作主要依賴着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