likes
comments
collection
share

聊聊web中关于文件的使用,及大文件分片上传的实践

作者站长头像
站长
· 阅读数 17

绝大多数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异步读取文件,这个通常用在大文件的处理上。

聊聊web中关于文件的使用,及大文件分片上传的实践

三、上传文件

文件处理之后,用户点击按钮,上传文件。这时需要和服务端进行交互,也就是常说的调用后端提供接口操作,由于接口通常是异步操作,同时文件上传到服务器的过程中,是一个漫长的过程,为了友好的给用户呈现上传状态,页面通常需要提供进度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算法来验证。

那么大文件打的该如何上传呢,百度的Webuploaderfex.baidu.com/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
    me.uploader.md5File(file)
        .progress(function(percentage) {
            this.getImportWin().setLoading("正在读取文件,请稍后")
        })
        .then(function (fileMd5){
            file.fileMd5 = fileMd5 //设置文件md5值,后端接口检测是否已上传属性
            this.getImportWin().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
    }
})

me.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 上传进度、结果处理

监听uploadProgressuploadSuccessuploadError事件,处理文件的上传进度、结果等。这几个事件,主要针对不同项目会做不同的响应处理,因此不再赘述。

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
                    this.saveMd5(block, blockMd5) // 记录所有分片的md5

                    if ( Object.keys(this.chunkWrappers).length ) {
                        // 如果分片数和成功数相同,则表示合并失败了
                        // 或者分片没成功
                        if ( block.chunks > this.chunkWrappers.sliceDtos.length ) {
                            var successMd5List = this.chunkWrappers.sliceDtos.length ?
                                    this.chunkWrappers.sliceDtos.map(function(slice) {
                                        return slice.sliceMd5
                                    }) : []
                            successMd5List.indexOf(blockMd5) == -1 ?
                                deferred.resolve() :
                                deferred.reject()
                        } else {
                            deferred.reject()
                            if (block.chunk == (this.chunkWrappers.sliceDtos.length - 1))
                                this.mergeFile(block)
                        }
                    } else {
                        deferred.resolve()
                    }
                })
            return deferred.promise()
        }
    }
})

至此,整个大文件的上传过程结束。

本文由浅到深的梳理了文件上传所涉及到的知识,包括文件规范定义、文件API的使用方式,XMLHTTPREQUEST的应用形式等,同时结合实践描述了大文件分片上传与重传过程,旨在提供一个清晰的文件上传思路给大家,希望对大家有用。

转载自:https://juejin.cn/post/7234795667478904890
评论
请登录