webuploader是百度fex团队开发的一个十分便捷的上传插件,可是咱们在实际生产中,会发现使用它与咱们的需求有各类各样的出入。最近作上传功能,踩了很多坑,如今来记录一下。若是个人文章中有任何不妥或者不对的地方,欢迎指正。javascript
上图是翻自网宿云的文档的分片上传流程。html
经过该图,咱们可知网宿云组织上传文件形式是vue
{文件[块1(分片1,分片2,分片3,…),块2,块3,…]}html5
而webuploader对文件分片的形式以下java
{文件[块1(分片1),块2(分片1),块3(分片1),…]}jquery
即一块便是一片。鉴于网宿云的上传一片一块在逻辑上没毛病,咱们一样能一块一块完成上传git
这里注意,请仔细看网宿云或七牛云分片上传的文档,了解如何分片上传。其中一个很重要的概念是块,片上下文,即ctx,请前往查看github
咱们先来看webuploader一个文件上传流程中,触发的钩子和事件web
一个文件的上传只触发三个实际使用的钩子api
1. before-send-file 上传文件前 2. before-send 上传块前 3. after-send-file 上传文件结束
触发多个事件
1. uploadStart 开始上传前 2. uploadAccept 验证上传是否合法的事件,取ctx只能在这一步进行,比较悲惨 3. uploadBeforeSend 上传文件前,对应before-send-file 4. uploadProgress 文件上传进度事件 5. uploadSkip 跳过当前文件上传事件,当出现该事件,uploader内部标记该文件已经上传成功 6. stopUpload 暂停当前文件上传时触发 7. startUpload 恢复上传当前文件触发,或开始上传也会触发 8. uploadSuccess 文件上传成功触发 9. uploadError 文件上传失败触发
经过比对网宿云的分片上传流程,咱们会发现他远远不知足咱们当下需求,缺乏上传分片前的钩子,缺乏上传分片后的钩子,这是不一样的分片姿式决定的,目前来讲除非咱们本身修改widgets/upload模块,要不没什么好的方式解决他
因此下面是修改该模块的内容
// 负责将文件切片。 function CuteFile( file, chunkSize ) { ... // 七牛云,网宿云规定的最大的块的大小,chunkSize不能大于它 var blockSize = 4 * 1024 * 1024 while ( index < chunks ) { len = Math.min( chunkSize, total - start ); let block = { file: file, start: start, end: chunkSize ? (start + len) : total, total: total, chunks: chunks, chunk: index, cuted: api } // 增长块id block.blockIndex = Math.floor(block.start / blockSize); // 增长块内片偏移量标识 block.offset = block.start % blockSize; // 增长块内最后一片标识(网宿云要求在组合文件的时候,须要用每块最后一片上传成功的ctx做为参数来组合文件) block.lastChunk = block.end % blockSize === 0 || block.end === total; if (block.start % blockSize === 0) { // 增长块头标识 block.mkblk = true; // 计算总块数 let blocks = Math.ceil( total / opts.blockSize ); // 增长块大小标识 block.size = (block.blockIndex + 1) === blocks ? (total - block.start) : blockSize; } pending.push(block); index++; start += len; } file.blocks = pending.concat(); file.remaning = pending.length; return api; }
这样改事后有一个毛病,那就是因为片上传是顺序上传,片上传是没法并发的~这样改的结果就是,一个文件只能顺序上传全部片了。。~本修改只是一个示例,若是真的要彻底支持块并发,片顺序上传,必需要修改block的结构,让block存储该块中全部片内容。其结构应该是
block: { ... file: 父节点的引用 cutes: [ 片1, 片2, 片3 ], percents: x, remaning: cutes.length }
除此以外,把实施上传的主体变动为片,并实现或触发一些支持分片上传的自定义事件,这样就能够以块为单位,并发上传,块中片顺序上传了。
经过网上大量的例子,以下:
uploader.register({ 'before-send-file': 'bsf', 'before-send': 'bbs', 'after-send-file': 'afs' }, { 'bsf': function () { ... }, 'bbs': function (block) { var server = ''; var D = webUploader.Deferred() if (block.chunk === 1) { uploader.options.server = 'xxxx' } else { uploader.options.server = 'xxxxx' } setTimeout(function () { D.resolve() }, 200) return D.promise() }, 'afs': function () { ... } })
从例子看,彷佛webuploader只有一个通用的options来配置服务器地址,formData, headers信息等,因为before-send-file, before-send, after-send-file三个钩子是异步执行的,因此在并发上传时,修改分片上传或mkblk操做所需的服务配置可能会给咱们带来困扰。按照这个思路,一个解决方案是实现一个uploadTaskManager,使用worker来进行多实例并发上传操做。
然而近期,经过读webuploader/widgets/upload.js的源代码,咱们发现如下内容:
_doSend: function( block ) { var me = this, owner = me.owner, // 可喜可贺 opts = $.extend({}, me.options, block.options), file = block.file, tr = new Transport( opts ), data = $.extend({}, opts.formData ), headers = $.extend({}, opts.headers ), requestAccept, ret; ...
可喜可贺,咱们彻底能够经过直接给block增长options来保证before-send钩子执行时不扰乱总体options配置
// appendWidget不用管,是我添加用于追加注册一个挂件的方法。 // 因为register方法是在webuploader实例化的时候才将注册的挂件挂载上,因此才有了这个方法 this.$uploader.appendWidget({ 'before-send-file': 'bsf', 'before-send': 'bbs', 'after-send-file': 'afs', 'name': 'progress' }, { bsf: (file) => { // 这个也不用管,是我为vue增长的插件,每次响应get操做都返回一个webuploader.Deferred() let deferred = this.$deferred // 为webuploader增长的sha1hash计算方法 this.$uploader.sha1File(file) .progress((e) => { // console.log(file.name, e) }) .then((sha1Hash) => { file.sha1Hash = sha1Hash api.path.upload({ name: file.name, pid: file.pid, hash: file.sha1Hash }) .then((res) => { let data = res.body if (data.msg === 'file already exists') { this.$uploader.skipFile(file) } else { file.token = data.token file.server = data.url } deferred.resolve() }) }) return deferred.promise() }, bbs: (block) => { let deferred = this.$deferred if (!block.options) { let file = block.file // 直接设置options来达到修改server,headers配置的目的 block.options = { headers: { 'Content-Type': 'application/octet-stream', 'Authorization': file.token, 'UploadBatch': file.source.uid } } // webuploader切出的block上没有mkblk, blockIndex, size, offset属性等,这是我为了支持分片上传作的修改,请注意 if (block.mkblk) { block.options.server = file.server + '/mkblk/' + block.size + '/' + block.blockIndex } else { // 寻找当前片在整个块中的偏移 block.options.server = file.server + '/bput/' + file.ctxs[block.chunk - 1] + '/' + block.offset } } deferred.resolve() return deferred.promise() }, afs: (file) => { let deferred = this.$deferred if (file.skipped) { deferred.resolve() } else { let server = file.server + '/mkfile/' + file.size this.$http.post(server, file.mkblkctxs.join(','), { headers: { Authorization: file.token, 'Content-Type': 'text/plain', UploadBatch: file.source.uid } }) .then(res => { if (res.body.code) { deferred.reject(res.body.message) } else { deferred.resolve() } }) } return deferred.promise() }, 'name': 'progress' })
这里用html5无依赖版本进行说明
1.html5版本没有提供md5File的具体实现,而是以钩子的形式给你了,若是真的须要聚合md5计算方法,能够按照全量版本里的模块注册形式,依次引入md5计算辅助库,引入全量包里的lib/md5, runtime/html5/md5, widgets/md5三个模块,并在preset模块中引入widgets/md5, runtime/html5/md5两个模块,完成模块组合。若是不须要在内部聚合,能够直接使用register注册一个匿名挂件,并把md5-file这个命令钩子所对应的函数实现便可。 2.无依赖版本的内建jquery还不彻底,这致使了无依赖版本没法运行,请自行为dollar-builtin模块增长$.param, $.inArray两个方法,并将weuploader中用到了$.map方法的地方改成$.each(内建的jquery不支持$.map) 3.删除全部与dom相关的依赖,只保留无dom操做相关的纯逻辑模块(其实不删除也能够,只要不配置dom相关挂件便可) 4.将webuploader实现为vue的插件,能够直接为Vue.prototype添加一个uploader的实例
如下是一个内聚实现七牛云qeTag hash的代码,因为是临时测试修改,没有在乎语法和模块引入,见谅。
修改uploader模块,为webuploader添加sha1File方法的命令
// 批量添加纯命令式方法。 $.each({ upload: 'start-upload', stop: 'stop-upload', getFile: 'get-file', getFiles: 'get-files', addFile: 'add-file', addFiles: 'add-file', sort: 'sort-files', removeFile: 'remove-file', cancelFile: 'cancel-file', skipFile: 'skip-file', retry: 'retry', isInProgress: 'is-in-progress', makeThumb: 'make-thumb', md5File: 'md5-file', sha1File: 'sha1-file', // 这里添加~ getDimension: 'get-dimension', addButton: 'add-btn', predictRuntimeType: 'predict-runtime-type', refresh: 'refresh', disable: 'disable', enable: 'enable', reset: 'reset' }, function( fn, command ) { Uploader.prototype[ fn ] = function() { return this.request( command, arguments ); }; });
加入一个sha1的依赖,这里我使用的是js-sha1
实现/widgets/sha1,实现sha1File接口
/** * @fileOverview sha1计算 */ import Base from '../base' import Uploader from '../uploader' import Sha1 from '../lib/sha1' import Blob from '../lib/blob' export default Uploader.register({ name: 'sha1', /** * 计算文件 sha1_hash 值,返回一个 promise 对象,能够监听 progress 进度。 * * * @method sha1File * @grammar sha1File( file[, start[, end]] ) => promise * @for Uploader * @example * * uploader.on( 'fileQueued', function( file ) { * var $li = ...; * * uploader.sha1File( file ) * * // 及时显示进度 * .progress(function(percentage) { * console.log('Percentage:', percentage); * }) * * // 完成 * .then(function(val) { * console.log('sha1 result:', val); * }); * * }); */ sha1File: function( file, start, end ) { var sha1 = new Sha1(), deferred = Base.Deferred(), blob = (file instanceof Blob) ? file : this.request( 'get-file', file ).source; sha1.on( 'progress load', function( e ) { e = e || {}; deferred.notify( e.total ? e.loaded / e.total : 1 ); }); sha1.on( 'complete', function() { deferred.resolve( sha1.getResult() ); }); sha1.on( 'error', function( reason ) { deferred.reject( reason ); }); if ( arguments.length > 1 ) { start = start || 0; end = end || 0; start < 0 && (start = blob.size + start); end < 0 && (end = blob.size + end); end = Math.min( end, blob.size ); blob = blob.slice( start, end ); } sha1.loadFromBlob( blob ); return deferred.promise(); } });
实现/lib/sha1,链接运行时sha1库的封装
/** * @fileOverview sha1 */ import RuntimeClient from '../runtime/client' import Mediator from '../mediator' function Sha1() { RuntimeClient.call( this, 'Sha1' ); } // 让 Sha1 具有事件功能。 Mediator.installTo( Sha1.prototype ); Sha1.prototype.loadFromBlob = function( blob ) { var me = this; if ( me.getRuid() ) { me.disconnectRuntime(); } // 链接到blob归属的同一个runtime. me.connectRuntime( blob.ruid, function() { me.exec('init'); me.exec( 'loadFromBlob', blob ); }); }; Sha1.prototype.getResult = function() { return this.exec('getResult'); }; export default Sha1;
建立一个运行时库/runtime/html5/sha1,这里使用了Crypto-JS v2.5.1进行辅助计算
/** * @fileOverview Transport flash实现 */ import Html5Runtime from './runtime' import Sha1 from '@/plugins/sha1' import Uploader from '../../uploader' import Crypto from '@/libs/Crypto' export default Html5Runtime.register( 'Sha1', { init: function() { // do nothing. }, loadFromBlob: function( file ) { var blob = file.getSource(), chunkSize = 4 * 1024 * 1024, chunks = Math.ceil( blob.size / chunkSize ), chunk = 0, owner = this.owner, me = this, blobSlice = blob.mozSlice || blob.webkitSlice || blob.slice, loadNext, fr; var hashs = [], ret = ''; fr = new FileReader(); loadNext = function() { var start, end; start = chunk * chunkSize; end = Math.min( start + chunkSize, blob.size ); fr.onload = function( e ) { // var block = Tool.Crypto.util.bytesToWords( new Uint8Array(e.target.result)); var sha1 = Sha1.create(); var hash = sha1.update(e.target.result).digest(); hashs = hashs.concat(hash); if (end === file.size) { var perfex = 0x16; if (chunks > 1) { perfex = 0x96 sha1 = Sha1.create(); hash = sha1.update(hashs).digest() hashs = hash } hashs.unshift(perfex) ret = Crypto.util.bytesToBase64(hashs); } owner.trigger( 'progress', { total: file.size, loaded: end }); }; fr.onloadend = function() { fr.onloadend = fr.onload = null; if ( ++chunk < chunks ) { setTimeout( loadNext, 1 ); } else { setTimeout(function(){ owner.trigger('load'); // 导出的是urlsafe的base64 me.result = ret.replace(/\//g,'_').replace(/\+/g,'-'); loadNext = file = blob = hashs = null; owner.trigger('complete'); }, 50 ); } }; fr.readAsArrayBuffer( blobSlice.call( blob, start, end ) ); }; loadNext(); }, getResult: function() { return this.result; } });
为preset/html5only挂载依赖
/** * @fileOverview 只有html5实现的文件版本。 */ import Base from '../base' import '../widgets/widget' import '../widgets/queue' import '../widgets/runtime' import '../widgets/upload' import '../widgets/validator' import '../widgets/md5' import '../widgets/sha1' import '../runtime/html5/blob' import '../runtime/html5/transport' import '../runtime/html5/md5' import '../runtime/html5/sha1' export default Base;
如何使用?和md5File使用姿式如出一辙