qc.ifish7.com/Public/webuploader/widgets/upload.js

851 lines
27 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 ) {
me.owner.trigger('startUpload', file);// 开始上传或暂停恢复的trigger event
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;
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) {
v.transport && v.transport.abort();
if (interrupt) {
me._putback(v);
me._popBlock(v);
}
}
});
me.owner.trigger('stopUpload', file);// 暂停trigger event
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 );
}
});
});