绝大多数B端项目都会涉及到文件管理,文件管理的流程,通常是用户从自己的设备中选择一个或多个文件,点击上传按钮,以类似表单的形式进行提交;服务端收到请求后,按照一定规则将用户的文件,存储在文件服务器中,同时提供应用API,给用户展示上传文件信息。文件管理的难点,通常在大文件的上传上;但本文并不想直接介绍如何实现大文件上传,相反的,想从web文件认识出发,由浅到深的梳理下这块的知识。
一、type="file"的input元素
HTML Standard的Upload state规范中,详细描述了web端进行文件选择的规则,即如果在html页面中,对input元素设置了type="file"的属性,那么就意味着用户可点击输入框区域对当前设备中的文件进行选择,选择的文件,可以用过文件API进行访问。同时该规则中,详细描述了type="file"属性的input标签中的其他属性的定义,如multiple属性指定了用户是否可多选文件;accpet定义了可选择的文档类型。因此,无论是自己实现上传功能,或引用第三方的上传插件时,总能发现这个神奇的input元素,它作为文件上传的基础,起着至关重要的作用。
// webuploader插件中,对input初始化定义
input = this.input = $( document.createElement('input') ),
input.attr( 'type', 'file' );
input.attr( 'name', opts.name );
if ( opts.multiple ) {
input.attr( 'multiple', 'multiple' );
}
if ( opts.accept && opts.accept.length > 0 ) {
...
input.attr( 'accept', arr.join(',') );
}
二、读取选择文件
针对所有设置type="file"的input元素,都会开放一个files属性,通过该属性可读取已上传的文件信息。files是一个fileList的集合,fileList中则存放着file对象。该对象中提供了文件的重要属性,如name、size、type等,开发时可对文件进行合理处理。除此之外,文件API中还提供FileReader异步读取文件,这个通常用在大文件的处理上。
三、上传文件
文件处理之后,用户点击按钮,上传文件。这时需要和服务端进行交互,也就是常说的调用后端提供接口操作,由于接口通常是异步操作,同时文件上传到服务器的过程中,是一个漫长的过程,为了友好的给用户呈现上传状态,页面通常需要提供进度progress信息,上传成功或失败之后,需要进行相应处理。因此,在这个过程中需要用到XMLHttpRequest技术,即实例化XMLHttpRequest,依据其和服务器进行交互,同时在交互过程中,调用load、progress等事件,在当前事件中封装给用户展示的信息。
// webuploader中,根据xhr的处理
var xhr = new XMLHttpRequest()
xhr.on( 'uploadprogress progress', function( e ) {
var percent = e.loaded / e.total;
percent = Math.min( 1, Math.max( 0, percent ) );
return me.trigger( 'progress', percent );
});
xhr.on( 'load', function() {
...
})
xhr.on( 'error', function() {
...
});
四、大文件上传实践
在实际应用中,文件通常在上传过程中,会出现文件过大而导致上传失败、或网络因素导致文件上传中断等情况。原始处理方法,往往会对整个文件进行重传,但重传过程还是会碰到上述问题。如果能将文件切片,按照小容量去分片传,将大大提高上传效率。同时,分片上传的方式,可以记录文件的失败片,重传过程中,只需要上传失败片,最后合并分片文件,即可完成大文件的上传。
这里如何记录文件的唯一性、分片的唯一性,则通过MD5算法来验证。
那么大文件到底该如何上传呢,百度的Webuploader提供了完整的思路。这里将应用过程中的处理逻辑进行总结。
4.1 初始化文件容器
初始化之前,需要下载webuploader的css、js文件,并在项目中引用。
通俗来说,是初始化一个input标签元素,在应用中,不需要再自定义标签,我们只需提供一个其它dom的id属性,在webuploader初始化中进行指定即可。指定之后,webuploader会自动生成标签,同时给该input标签设置css属性。
// 该指定的容器,可以是任意标签,如div、table等
<div id="uploadForm"></div>
4.2 初始化webuploader
我们要使用webuploader,因此必须初始化一个uploader实例,通过该实例去调用插件封装的属性和方法。在该初始化过程中,除了指定input标签的生成容器id之外,还可以定义支持选择的文档类型、分片大小、是否分片等信息。具体可参照官网API进行设置。
this.uploader = WebUploader.create({
auto: false,
swf: BASE_URL+'webuploader/Uploader.swf',
pick: '#uploadForm', // div标签容器
server: '/fm/file/upload', // 上传接口
method: 'post', // 上传接口对应method
chunked: true, // 是否分区
chunkSize: 10485760, // 默认5M,5242880B
chunkRetry: 0,
threads: 5,
resize: false,
accept: {
title: '文件格式',
extensions: 'txt,csv,xls,xlsx,pdf',
mimeTypes: 'text/plain, ' +
'text/csv, ' +
'application/vnd.ms-excel, ' +
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, ' +
'application/pdf'
},
withCredentials: true,
compress: false,
resize: false
})
4.3 上传过程
上传过程,涉及到以下几个过程。
4.3.1 文件分片前,文件MD5的计算
webuploader提供上传事件的一些列接口,当文件选择后,首先会触发fileQueued事件,该事件,即前面介绍的读取文件信息步骤,我们可以在该事件中,通过调用通用接口md5File计算该文件的Md5值。该Md5值,是后续文件验证唯一性的标志。
this.uploader.on('fileQueued', function (file) {
// 重传的逻辑
if ( (Object.keys(me.chunkWrappers).length) && (file.name != me.chunkWrappers.fileName) ) {
RMsg.show("选择文件与需重传文件名称或格式不同,请重新选择!")
return
}
// 设置选择的值
document.getElementById("uploadForm-inputEl").value = files.join(";")
// 计算上传文件的md5
this.uploader.md5File(file)
.progress(function(percentage) {
this.setLoading("正在读取文件,请稍后")
})
.then(function (fileMd5){
file.fileMd5 = fileMd5 //设置文件md5值,后端接口检测是否已上传属性
this.setLoading(false);
})
})
4.3.2 检测与上传
在上传文件之前,通常我们会检测文件是否已经上传过,该上传的定义由项目来确定,可能是上传失败的,可能是中断上传的,只有符合条件的文件,调用upload方法。 调用upload方法后,首先会监听uploadBeforeSend事件,该事件可定义上传接口的参数,但这个事件由于是异步的,如果是多文件的上传可能会造成参数问题,如需要上传check接口中返回的某个参数,在当前事件中,可能会设置无效。webuploader提供了额外hook,用于对这种情况的处理。即在调用uploaderBeforeSend事件前,会首先调用hook,在hook中进行处理。hook定义可自行查官网
checkUploadFile: function(file) {
this.upload()
}
WebUploader.Uploader.register({
'before-send': 'beforeSend'
}, {
beforeSend: function (block) {
// 在该hook中,主要是重传的逻辑,下面篇幅介绍。
// 获取分片文件的分片md5
}
})
this.uploader.on('uploadBeforeSend', function( block, data, headers ) {
data.chunkFile = block.blob // 分片文件
data.sliceNo = parseInt(block.chunk)+1 // 分片序号
data.fileSlice = block.chunks // 分片总数
data.fileName = block.file.fileName // 文件名称
data.fileId = block.file.fileId // 文件id
data.sliceMd5 = block.blockMd5 // 分片文件的md5
data.filePath = block.file.filePath // 文件路径
headers.Authorization = getToken() // 头部认证信息
})
4.3.3 上传进度、结果处理
监听uploadProgress,uploadSuccess,uploadError事件,处理文件的上传进度、结果等。这几个事件,主要针对不同项目会做不同的响应处理,因此不再赘述。
4.3.4 重传
重传作为大文件上传的重要内容之一,需要涉及到重传片的识别、文件合并的过程。在重传之前,通过接口获取成功片数,这里的成功有一种特殊情况,即分片都已上传成功,只是最后合并出错,这里需要单独走合并接口;其他分片上传失败的情况,则重新走上传流程,因此在hook中,需要判断失败片,即失败片走upload,成功片不再调用上传接口。
// 接口获取成功片数
checkSuccessSlice: function() {
...
this.chunkWrappers = response // 保存用于判断
}
// 分片前的hook处理重传逻辑
WebUploader.Uploader.register({
'before-send': 'beforeSend'
}, {
beforeSend: function (block) {
var _this = this // 当前uploader对象
if ( _this.options.chunked ) { // 是否分片
var deferred = WebUploader.Deferred(),
filename = block.file.name; // 文件名称
(new WebUploader.Uploader()).md5File( block.file, block.start, block.end )
.progress(function(percentage) {
})
.then(function( blockMd5 ) {
block.blockMd5 = blockMd5
if ( Object.keys(this.chunkWrappers).length ) {
// 如果分片数和成功数相同,则表示合并失败了
// 或者分片没成功
if ( block.chunks > this.apiReturnObj.length ) {
var successMd5List = this.apiReturnObj.length ?
this.apiReturnObj.map(function(slice) {
return slice.sliceMd5
}) : []
successMd5List.indexOf(blockMd5) == -1 ?
deferred.resolve() :
deferred.reject()
} else {
deferred.reject()
if (block.chunk == (this.apiReturnObj.length - 1))
this.mergeFile(block)
}
} else {
deferred.resolve()
}
})
return deferred.promise()
}
}
})
至此,整个大文件的上传过程结束。
本文由浅到深的梳理了文件上传所涉及到的知识,包括文件规范定义、文件API的使用方式,XMLHTTPREQUEST的应用形式等,同时结合实践描述了大文件分片上传与重传过程,旨在提供一个清晰的文件上传思路给大家,希望对大家有用。