基于JavaScript实现前端文件的断点续传


Posted in Javascript onOctober 17, 2016

还是先以图片为例,看看最后的样子

基于JavaScript实现前端文件的断点续传

一、一些知识准备

断点续传,既然有断,那就应该有文件分割的过程,一段一段的传。

以前文件无法分割,但随着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>
<!-- 上传文件列表中每个文件的信息模版 -->
<script type="text/template" id="file-upload-tpl">
<tr>
<td>{{fileName}}</td>
<td>{{fileType}}</td>
<td>{{fileSize}}</td>
<td class="upload-progress">{{progress}}</td>
<td>
<input type="button" class="upload-item-btn" data-name="{{fileName}}" data-size="{{totalSize}}" data-state="default" value="{{uploadVal}}">
</td>
</tr>
</script>

这里一并将CSS样式扔出来

<style type="text/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;
border: 1px solid #ddd;
text-align: center;
font-size: 14px;
}
</style>

(2)接下来是JS的实现解析

通过FileList对象我们能获取到文件的一些信息

基于JavaScript实现前端文件的断点续传

其中的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 = '继续上传';
}

显示了文件信息列表

基于JavaScript实现前端文件的断点续传

点击开始上传,可以上传相应的文件

基于JavaScript实现前端文件的断点续传

上传文件的时候需要就将文件进行分片分段

比如这里配置的每段1024B,总共chunks段(用来判断是否为末段),第chunk段,当前已上传的百分比percent等

需要提一下的是这个暂停上传的操作,其实我还没实现出来,暂停不了无奈ing...

基于JavaScript实现前端文件的断点续传

基于JavaScript实现前端文件的断点续传

接下来是分段过程

 

 // 设置分片的开始结尾
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其实就是个参数,因为要在上一分段传完之后再传下一分段,所以这里的做法是在回调中继续调用这个上传操作

基于JavaScript实现前端文件的断点续传

接下来就是真正的文件上传操作了,用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']);
}

继续下一分段的上传时,就进行了递归操作,按顺序地上传下一分段

截个图..

基于JavaScript实现前端文件的断点续传

这是完整的JS逻辑,代码有点儿注释了应该不难看懂吧哈哈

<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript">
// 全部上传操作
$(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 = '未上传';
uploadVal = '开始上传';
// 计算文件大小
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),
percent,
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');
isPaused = 0;
}
// 第一次点击上传
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']);
}
});
}
});
</script>

2. 后端实现

这里的后端实现还是比较简单的,主要用依赖了 file_put_contents、file_get_contents 这两个方法

基于JavaScript实现前端文件的断点续传

要注意一下,通过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;
} else {
$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;
} else {
// 此处为一般的文件上传操作
// 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;
} else {
// 在上传的最后片段时,检测文件是否完整(大小是否一致)
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
));
?>

以上所述是小编给大家介绍的基于JavaScript实现前端文件的断点续传,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
setTimeout与setInterval在不同浏览器下的差异
Jan 24 Javascript
JavaScript 字符串处理函数使用小结
Dec 02 Javascript
js获取鼠标点击的位置实现思路及代码
May 09 Javascript
jQuery控制网页打印指定区域的方法
Apr 07 Javascript
被遗忘的javascript的slice() 方法
Apr 20 Javascript
JavaScript判断数组重复内容的两种方法(推荐)
Jun 06 Javascript
ES6使用Set数据结构实现数组的交集、并集、差集功能示例
Oct 31 Javascript
JavaScript实现带有子菜单和控件的slider轮播图效果
Nov 01 Javascript
Angular使用过滤器uppercase/lowercase实现字母大小写转换功能示例
Mar 27 Javascript
图解javascript作用域链
May 27 Javascript
Layui点击图片弹框预览的实现方法
Sep 16 Javascript
JS图片懒加载技术实现过程解析
Jul 27 Javascript
js html5 css俄罗斯方块游戏再现
Oct 17 #Javascript
Node.js包管理器Yarn的入门介绍与安装
Oct 17 #Javascript
深入理解JS实现快速排序和去重
Oct 17 #Javascript
JavaScript中关键字 in 的使用方法详解
Oct 17 #Javascript
Angular 2应用的8个主要构造块有哪些
Oct 17 #Javascript
jQuery表单验证简单示例
Oct 17 #Javascript
jQuery右下角悬浮广告实例
Oct 17 #Javascript
You might like
PHP 和 XML: 使用expat函数(二)
2006/10/09 PHP
编写PHP脚本过滤用户上传的图片
2015/07/03 PHP
基于php实现的验证码小程序
2016/12/13 PHP
PHP实现基于图的深度优先遍历输出1,2,3...n的全排列功能
2017/11/10 PHP
javascript 面向对象编程 万物皆对象
2009/09/17 Javascript
JavaScript DOM学习第八章 表单错误提示
2010/02/19 Javascript
JQuery文本框高亮显示插件代码
2011/04/02 Javascript
js实时获取系统当前时间实例代码
2013/06/28 Javascript
Extjs4实现两个GridPanel之间数据拖拽功能具体方法
2013/11/21 Javascript
jquery复选框全选/取消示例
2013/12/30 Javascript
javascript实现获取cookie过期时间的变通方法
2014/08/14 Javascript
javascript框架设计之浏览器的嗅探和特征侦测
2015/06/23 Javascript
javascript html5移动端轻松实现文件上传
2020/03/27 Javascript
jQuery Ajax 上传文件处理方式介绍(推荐)
2016/06/30 Javascript
BootStrap下拉菜单和滚动监听插件实现代码
2016/09/26 Javascript
详解React开发中使用require.ensure()按需加载ES6组件
2017/05/12 Javascript
vue非父子组件通信问题及解决方法
2018/06/11 Javascript
微信小程序实现日历功能
2018/11/27 Javascript
json数据格式常见操作示例
2019/06/13 Javascript
jQuery 筛选器简单操作示例
2019/10/02 jQuery
python转换字符串为摩尔斯电码的方法
2015/07/06 Python
Python实现计算圆周率π的值到任意位的方法示例
2018/05/08 Python
Pandas:DataFrame对象的基础操作方法
2018/06/07 Python
Python MongoDB 插入数据时已存在则不执行,不存在则插入的解决方法
2019/09/24 Python
django 利用Q对象与F对象进行查询的实现
2020/05/15 Python
matlab、python中矩阵的互相导入导出方式
2020/06/01 Python
Python实现GIF图倒放
2020/07/16 Python
Python根据字典的值查询出对应的键的方法
2020/09/30 Python
html5文本内容_动力节点Java学院整理
2017/07/11 HTML / CSS
网页美工求职信
2014/02/15 职场文书
《草虫的村落》教学反思
2014/02/16 职场文书
广告业务员岗位职责
2015/02/13 职场文书
go原生库的中bytes.Buffer用法
2021/04/25 Golang
Springboot如何使用logback实现多环境配置?
2021/06/16 Java/Android
MySQL系列之九 mysql查询缓存及索引
2021/07/02 MySQL
了解Kubernetes中的Service和Endpoint
2022/04/01 Servers