UNPKG

hdjs

Version:
852 lines (688 loc) 27.4 kB
/** * @fileOverview 负责文件上传相关。 */ define([ '../base', '../uploader', '../file', '../lib/transport', './widget' ], function( Base, Uploader, WUFile, Transport ) { var $ = Base.$, isPromise = Base.isPromise, Status = WUFile.Status; // 添加默认配置项 $.extend( Uploader.options, { /** * @property {Boolean} [prepareNextFile=false] * @namespace options * @for Uploader * @description 是否允许在文件传输时提前把下一个文件准备好。 * 对于一个文件的准备工作比较耗时,比如图片压缩,md5序列化。 * 如果能提前在当前文件传输期处理,可以节省总体耗时。 */ prepareNextFile: false, /** * @property {Boolean} [chunked=false] * @namespace options * @for Uploader * @description 是否要分片处理大文件上传。 */ chunked: false, /** * @property {Boolean} [chunkSize=5242880] * @namespace options * @for Uploader * @description 如果要分片,分多大一片? 默认大小为5M. */ chunkSize: 5 * 1024 * 1024, /** * @property {Boolean} [chunkRetry=2] * @namespace options * @for Uploader * @description 如果某个分片由于网络问题出错,允许自动重传多少次? */ chunkRetry: 2, /** * @property {Boolean} [threads=3] * @namespace options * @for Uploader * @description 上传并发数。允许同时最大上传进程数。 */ threads: 3, /** * @property {Object} [formData={}] * @namespace options * @for Uploader * @description 文件上传请求的参数表,每次发送都会发送此对象中的参数。 */ formData: {} /** * @property {Object} [fileVal='file'] * @namespace options * @for Uploader * @description 设置文件上传域的name。 */ /** * @property {Object} [sendAsBinary=false] * @namespace options * @for Uploader * @description 是否已二进制的流的方式发送文件,这样整个上传内容`php://input`都为文件内容, * 其他参数在$_GET数组中。 */ }); // 负责将文件切片。 function CuteFile( file, chunkSize ) { var pending = [], blob = file.source, total = blob.size, chunks = chunkSize ? Math.ceil( total / chunkSize ) : 1, start = 0, index = 0, len, api; api = { file: file, has: function() { return !!pending.length; }, shift: function() { return pending.shift(); }, unshift: function( block ) { pending.unshift( block ); } }; while ( index < chunks ) { len = Math.min( chunkSize, total - start ); pending.push({ file: file, start: start, end: chunkSize ? (start + len) : total, total: total, chunks: chunks, chunk: index++, cuted: api }); start += len; } file.blocks = pending.concat(); file.remaning = pending.length; return api; } Uploader.register({ name: 'upload', init: function() { var owner = this.owner, me = this; this.runing = false; this.progress = false; owner .on( 'startUpload', function() { me.progress = true; }) .on( 'uploadFinished', function() { me.progress = false; }); // 记录当前正在传的数据,跟threads相关 this.pool = []; // 缓存分好片的文件。 this.stack = []; // 缓存即将上传的文件。 this.pending = []; // 跟踪还有多少分片在上传中但是没有完成上传。 this.remaning = 0; this.__tick = Base.bindFn( this._tick, this ); // 销毁上传相关的属性。 owner.on( 'uploadComplete', function( file ) { // 把其他块取消了。 file.blocks && $.each( file.blocks, function( _, v ) { v.transport && (v.transport.abort(), v.transport.destroy()); delete v.transport; }); delete file.blocks; delete file.remaning; }); }, reset: function() { this.request( 'stop-upload', true ); this.runing = false; this.pool = []; this.stack = []; this.pending = []; this.remaning = 0; this._trigged = false; this._promise = null; }, /** * @event startUpload * @description 当开始上传流程时触发。 * @for Uploader */ /** * 开始上传。此方法可以从初始状态调用开始上传流程,也可以从暂停状态调用,继续上传流程。 * * 可以指定开始某一个文件。 * @grammar upload() => undefined * @grammar upload( file | fileId) => undefined * @method upload * @for Uploader */ startUpload: function(file) { var me = this; // 移出invalid的文件 $.each( me.request( 'get-files', Status.INVALID ), function() { me.request( 'remove-file', this ); }); // 如果指定了开始某个文件,则只开始指定的文件。 if ( file ) { file = file.id ? file : me.request( 'get-file', file ); if (file.getStatus() === Status.INTERRUPT) { file.setStatus( Status.QUEUED ); $.each( me.pool, function( _, v ) { // 之前暂停过。 if (v.file !== file) { return; } v.transport && v.transport.send(); file.setStatus( Status.PROGRESS ); }); } else if (file.getStatus() !== Status.PROGRESS) { file.setStatus( Status.QUEUED ); } } else { $.each( me.request( 'get-files', [ Status.INITED ] ), function() { this.setStatus( Status.QUEUED ); }); } if ( me.runing ) { return Base.nextTick( me.__tick ); } me.runing = true; var files = []; // 如果有暂停的,则续传 file || $.each( me.pool, function( _, v ) { var file = v.file; if ( file.getStatus() === Status.INTERRUPT ) { me._trigged = false; files.push(file); v.transport && v.transport.send(); } }); $.each(files, function() { this.setStatus( Status.PROGRESS ); }); file || $.each( me.request( 'get-files', Status.INTERRUPT ), function() { this.setStatus( Status.PROGRESS ); }); me._trigged = false; Base.nextTick( me.__tick ); me.owner.trigger('startUpload'); }, /** * @event stopUpload * @description 当开始上传流程暂停时触发。 * @for Uploader */ /** * 暂停上传。第一个参数为是否中断上传当前正在上传的文件。 * * 如果第一个参数是文件,则只暂停指定文件。 * @grammar stop() => undefined * @grammar stop( true ) => undefined * @grammar stop( file ) => undefined * @method stop * @for Uploader */ stopUpload: function( file, interrupt ) { var me = this, block; if (file === true) { interrupt = file; file = null; } if ( me.runing === false ) { return; } // 如果只是暂停某个文件。 if ( file ) { file = file.id ? file : me.request( 'get-file', file ); if ( file.getStatus() !== Status.PROGRESS && file.getStatus() !== Status.QUEUED ) { return; } file.setStatus( Status.INTERRUPT ); $.each( me.pool, function( _, v ) { // 只 abort 指定的文件。 if (v.file === file) { block = v; return false; } }); block.transport && block.transport.abort(); if (interrupt) { me._putback(block); me._popBlock(block); } return Base.nextTick( me.__tick ); } me.runing = false; // 正在准备中的文件。 if (this._promise && this._promise.file) { this._promise.file.setStatus( Status.INTERRUPT ); } interrupt && $.each( me.pool, function( _, v ) { v.transport && v.transport.abort(); v.file.setStatus( Status.INTERRUPT ); }); me.owner.trigger('stopUpload'); }, /** * @method cancelFile * @grammar cancelFile( file ) => undefined * @grammar cancelFile( id ) => undefined * @param {File|id} file File对象或这File对象的id * @description 标记文件状态为已取消, 同时将中断文件传输。 * @for Uploader * @example * * $li.on('click', '.remove-this', function() { * uploader.cancelFile( file ); * }) */ cancelFile: function( file ) { file = file.id ? file : this.request( 'get-file', file ); // 如果正在上传。 file.blocks && $.each( file.blocks, function( _, v ) { var _tr = v.transport; if ( _tr ) { _tr.abort(); _tr.destroy(); delete v.transport; } }); file.setStatus( Status.CANCELLED ); this.owner.trigger( 'fileDequeued', file ); }, /** * 判断`Uplaode`r是否正在上传中。 * @grammar isInProgress() => Boolean * @method isInProgress * @for Uploader */ isInProgress: function() { return !!this.progress; }, _getStats: function() { return this.request('get-stats'); }, /** * 掉过一个文件上传,直接标记指定文件为已上传状态。 * @grammar skipFile( file ) => undefined * @method skipFile * @for Uploader */ skipFile: function( file, status ) { file = file.id ? file : this.request( 'get-file', file ); file.setStatus( status || Status.COMPLETE ); file.skipped = true; // 如果正在上传。 file.blocks && $.each( file.blocks, function( _, v ) { var _tr = v.transport; if ( _tr ) { _tr.abort(); _tr.destroy(); delete v.transport; } }); this.owner.trigger( 'uploadSkip', file ); }, /** * @event uploadFinished * @description 当所有文件上传结束时触发。 * @for Uploader */ _tick: function() { var me = this, opts = me.options, fn, val; // 上一个promise还没有结束,则等待完成后再执行。 if ( me._promise ) { return me._promise.always( me.__tick ); } // 还有位置,且还有文件要处理的话。 if ( me.pool.length < opts.threads && (val = me._nextBlock()) ) { me._trigged = false; fn = function( val ) { me._promise = null; // 有可能是reject过来的,所以要检测val的类型。 val && val.file && me._startSend( val ); Base.nextTick( me.__tick ); }; me._promise = isPromise( val ) ? val.always( fn ) : fn( val ); // 没有要上传的了,且没有正在传输的了。 } else if ( !me.remaning && !me._getStats().numOfQueue && !me._getStats().numofInterrupt ) { me.runing = false; me._trigged || Base.nextTick(function() { me.owner.trigger('uploadFinished'); }); me._trigged = true; } }, _putback: function(block) { var idx; block.cuted.unshift(block); idx = this.stack.indexOf(block.cuted); if (!~idx) { this.stack.unshift(block.cuted); } }, _getStack: function() { var i = 0, act; while ( (act = this.stack[ i++ ]) ) { if ( act.has() && act.file.getStatus() === Status.PROGRESS ) { return act; } else if (!act.has() || act.file.getStatus() !== Status.PROGRESS && act.file.getStatus() !== Status.INTERRUPT ) { // 把已经处理完了的,或者,状态为非 progress(上传中)、 // interupt(暂停中) 的移除。 this.stack.splice( --i, 1 ); } } return null; }, _nextBlock: function() { var me = this, opts = me.options, act, next, done, preparing; // 如果当前文件还有没有需要传输的,则直接返回剩下的。 if ( (act = this._getStack()) ) { // 是否提前准备下一个文件 if ( opts.prepareNextFile && !me.pending.length ) { me._prepareNextFile(); } return act.shift(); // 否则,如果正在运行,则准备下一个文件,并等待完成后返回下个分片。 } else if ( me.runing ) { // 如果缓存中有,则直接在缓存中取,没有则去queue中取。 if ( !me.pending.length && me._getStats().numOfQueue ) { me._prepareNextFile(); } next = me.pending.shift(); done = function( file ) { if ( !file ) { return null; } act = CuteFile( file, opts.chunked ? opts.chunkSize : 0 ); me.stack.push(act); return act.shift(); }; // 文件可能还在prepare中,也有可能已经完全准备好了。 if ( isPromise( next) ) { preparing = next.file; next = next[ next.pipe ? 'pipe' : 'then' ]( done ); next.file = preparing; return next; } return done( next ); } }, /** * @event uploadStart * @param {File} file File对象 * @description 某个文件开始上传前触发,一个文件只会触发一次。 * @for Uploader */ _prepareNextFile: function() { var me = this, file = me.request('fetch-file'), pending = me.pending, promise; if ( file ) { promise = me.request( 'before-send-file', file, function() { // 有可能文件被skip掉了。文件被skip掉后,状态坑定不是Queued. if ( file.getStatus() === Status.PROGRESS || file.getStatus() === Status.INTERRUPT ) { return file; } return me._finishFile( file ); }); me.owner.trigger( 'uploadStart', file ); file.setStatus( Status.PROGRESS ); promise.file = file; // 如果还在pending中,则替换成文件本身。 promise.done(function() { var idx = $.inArray( promise, pending ); ~idx && pending.splice( idx, 1, file ); }); // befeore-send-file的钩子就有错误发生。 promise.fail(function( reason ) { file.setStatus( Status.ERROR, reason ); me.owner.trigger( 'uploadError', file, reason ); me.owner.trigger( 'uploadComplete', file ); }); pending.push( promise ); } }, // 让出位置了,可以让其他分片开始上传 _popBlock: function( block ) { var idx = $.inArray( block, this.pool ); this.pool.splice( idx, 1 ); block.file.remaning--; this.remaning--; }, // 开始上传,可以被掉过。如果promise被reject了,则表示跳过此分片。 _startSend: function( block ) { var me = this, file = block.file, promise; // 有可能在 before-send-file 的 promise 期间改变了文件状态。 // 如:暂停,取消 // 我们不能中断 promise, 但是可以在 promise 完后,不做上传操作。 if ( file.getStatus() !== Status.PROGRESS ) { // 如果是中断,则还需要放回去。 if (file.getStatus() === Status.INTERRUPT) { me._putback(block); } return; } me.pool.push( block ); me.remaning++; // 如果没有分片,则直接使用原始的。 // 不会丢失content-type信息。 block.blob = block.chunks === 1 ? file.source : file.source.slice( block.start, block.end ); // hook, 每个分片发送之前可能要做些异步的事情。 promise = me.request( 'before-send', block, function() { // 有可能文件已经上传出错了,所以不需要再传输了。 if ( file.getStatus() === Status.PROGRESS ) { me._doSend( block ); } else { me._popBlock( block ); Base.nextTick( me.__tick ); } }); // 如果为fail了,则跳过此分片。 promise.fail(function() { if ( file.remaning === 1 ) { me._finishFile( file ).always(function() { block.percentage = 1; me._popBlock( block ); me.owner.trigger( 'uploadComplete', file ); Base.nextTick( me.__tick ); }); } else { block.percentage = 1; me.updateFileProgress( file ); me._popBlock( block ); Base.nextTick( me.__tick ); } }); }, /** * @event uploadBeforeSend * @param {Object} object * @param {Object} data 默认的上传参数,可以扩展此对象来控制上传参数。 * @param {Object} headers 可以扩展此对象来控制上传头部。 * @description 当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。 * @for Uploader */ /** * @event uploadAccept * @param {Object} object * @param {Object} ret 服务端的返回数据,json格式,如果服务端不是json格式,从ret._raw中取数据,自行解析。 * @description 当某个文件上传到服务端响应后,会派送此事件来询问服务端响应是否有效。如果此事件handler返回值为`false`, 则此文件将派送`server`类型的`uploadError`事件。 * @for Uploader */ /** * @event uploadProgress * @param {File} file File对象 * @param {Number} percentage 上传进度 * @description 上传过程中触发,携带上传进度。 * @for Uploader */ /** * @event uploadError * @param {File} file File对象 * @param {String} reason 出错的code * @description 当文件上传出错时触发。 * @for Uploader */ /** * @event uploadSuccess * @param {File} file File对象 * @param {Object} response 服务端返回的数据 * @description 当文件上传成功时触发。 * @for Uploader */ /** * @event uploadComplete * @param {File} [file] File对象 * @description 不管成功或者失败,文件上传完成时触发。 * @for Uploader */ // 做上传操作。 _doSend: function( block ) { var me = this, owner = me.owner, opts = me.options, file = block.file, tr = new Transport( opts ), data = $.extend({}, opts.formData ), headers = $.extend({}, opts.headers ), requestAccept, ret; block.transport = tr; tr.on( 'destroy', function() { delete block.transport; me._popBlock( block ); Base.nextTick( me.__tick ); }); // 广播上传进度。以文件为单位。 tr.on( 'progress', function( percentage ) { block.percentage = percentage; me.updateFileProgress( file ); }); // 用来询问,是否返回的结果是有错误的。 requestAccept = function( reject ) { var fn; ret = tr.getResponseAsJson() || {}; ret._raw = tr.getResponse(); fn = function( value ) { reject = value; }; // 服务端响应了,不代表成功了,询问是否响应正确。 if ( !owner.trigger( 'uploadAccept', block, ret, fn ) ) { reject = reject || 'server'; } return reject; }; // 尝试重试,然后广播文件上传出错。 tr.on( 'error', function( type, flag ) { block.retried = block.retried || 0; // 自动重试 if ( block.chunks > 1 && ~'http,abort'.indexOf( type ) && block.retried < opts.chunkRetry ) { block.retried++; tr.send(); } else { // http status 500 ~ 600 if ( !flag && type === 'server' ) { type = requestAccept( type ); } file.setStatus( Status.ERROR, type ); owner.trigger( 'uploadError', file, type ); owner.trigger( 'uploadComplete', file ); } }); // 上传成功 tr.on( 'load', function() { var reason; // 如果非预期,转向上传出错。 if ( (reason = requestAccept()) ) { tr.trigger( 'error', reason, true ); return; } // 全部上传完成。 if ( file.remaning === 1 ) { me._finishFile( file, ret ); } else { tr.destroy(); } }); // 配置默认的上传字段。 data = $.extend( data, { id: file.id, name: file.name, type: file.type, lastModifiedDate: file.lastModifiedDate, size: file.size }); block.chunks > 1 && $.extend( data, { chunks: block.chunks, chunk: block.chunk }); // 在发送之间可以添加字段什么的。。。 // 如果默认的字段不够使用,可以通过监听此事件来扩展 owner.trigger( 'uploadBeforeSend', block, data, headers ); // 开始发送。 tr.appendBlob( opts.fileVal, block.blob, file.name ); tr.append( data ); tr.setRequestHeader( headers ); tr.send(); }, // 完成上传。 _finishFile: function( file, ret, hds ) { var owner = this.owner; return owner .request( 'after-send-file', arguments, function() { file.setStatus( Status.COMPLETE ); owner.trigger( 'uploadSuccess', file, ret, hds ); }) .fail(function( reason ) { // 如果外部已经标记为invalid什么的,不再改状态。 if ( file.getStatus() === Status.PROGRESS ) { file.setStatus( Status.ERROR, reason ); } owner.trigger( 'uploadError', file, reason ); }) .always(function() { owner.trigger( 'uploadComplete', file ); }); }, updateFileProgress: function(file) { var totalPercent = 0, uploaded = 0; if (!file.blocks) { return; } $.each( file.blocks, function( _, v ) { uploaded += (v.percentage || 0) * (v.end - v.start); }); totalPercent = uploaded / file.size; this.owner.trigger( 'uploadProgress', file, totalPercent || 0 ); } }); });