webuploader上传插件源代码重点难点分析
webuploader源代码设计博大精深,具有工匠精神,本文分析webuploader源代码总体流程和一些重点难点,webuploader采用模块机制,比较复杂,模块编程和异步加载现在几乎已经成为历史,本文忽略wiget组件机制和flash部分的源码分析。
先看webuploader的总体程序结构:
(function( root, factory ) { // 直接执行匿名函数,传参,root就是window,factory就是模块构造函数
var modules = {},
_require = function( deps, callback ) { //获取依赖模块,调用callback,传递依赖模块,callback调setModule( id, factory, arguments )
var args, len, i;
// 如果deps不是数组,则直接返回指定module
if ( typeof deps === 'string' ) {
return getModule( deps );
} else {
args = [];
for( len = deps.length, i = 0; i < len; i++ ) {
args.push( getModule( deps[ i ] ) );
}
return callback.apply( null, args );
}
}
_define = function( id, deps, factory ) { // 调require
_require( deps || [], function() {
setModule( id, factory, arguments );
});
}
setModule = function( id, factory, args ) { // 执行模块构造函数factory,返回对象保存到modules[]
var module = {
exports: factory
},
returned;
if ( typeof factory === 'function' ) {
args.length || (args = [ _require, module.exports, module ]);
returned = factory.apply( null, args );
returned !== undefined && (module.exports = returned);
}
modules[ id ] = module.exports;
}
getModule = function( id ) {}
exportsTo = function( obj ) { // obj是引用所有模块集合,此函数是修改引用对象之后再返回引用对象,修改什么呢?就是把模块名中的xxx/路径转换为.xxx namespace
var key, host, parts, part, last, ucFirst;
// make the first character upper case.
ucFirst = function( str ) { //把首字母变为大写
return str && (str.charAt( 0 ).toUpperCase() + str.substr( 1 ));
};
for ( key in modules ) { // 循环处理modules[]中每一个模块,key就是模块名,以runtime/runtime为例
host = obj; // 每次循环开始时host重新引用obj
if ( !modules.hasOwnProperty( key ) ) {
continue;
}
parts = key.split('/'); //如果模块名有/xxx这样的路径,就取路径名到数组 parts=['runtime','runtime']
last = ucFirst( parts.pop() ); // 从parts取出(去掉)最后一部分,第一个字母变大写,数组变为['Runtime'],下面只循环一次,只有一个路径
while( (part = ucFirst( parts.shift() )) ) { // 如果模块名有路径,循环处理每一个部分,第一个字母大写
host[ part ] = host[ part ] || {}; // 修改属性是修改引用的obj的属性,第一次循环obj.Runtime={}
host = host[ part ]; // host重新赋值,不再引用之前的对象,而是引用host[part]对象也就是obj的一个属性。第一次循环host重新引用obj.Runtime
}
host[ last ] = modules[ key ]; //就是设置obj.Runtime.Runtime=modules['runtime/runtime'],这样就把moudles[]中的路径名变成了obj[]中的namespace,比如在modules[]中有一个模块runtime/runtime,那么在obj[]中就建立一个Runtime.Runtime模块,也就是建立obj.Runtime.Runtime模块,第一个Runtime是namespace,没有其它意义,第二个Runtime是属性名/模块名,其值是模块对象或函数。
}
return obj; //返回修改之后的模块集合,如果以为函数是修改host但却返回obj那就理解错了。
}
makeExport = function( dollar ) {
root.__dollar = dollar;
return exportsTo( factory( root, _define, _require ) ); //在这里执行factory函数产生各个模块
}
root.WebUploader = makeExport(); //把模块集合存储到全局对象window.WebUploader做为应用程序可以访问的api
})( window, function( window, define, require ) { //匿名函数即为factory函数,构造各个模块保存到modules[],最终返回webuploader全局对象含所有模块。
。。。
return require('webuploader');
});
webuploader调用方式:
uploader = WebUploader.create(opts) // 调用WebUploader.create这个api创建new Uploader实例
Base.create = Uploader.create = function( opts ) {
return new Uploader( opts );
};
Uploader构造函数:
function Uploader( opts ) {
this.options = $.extend( true, {}, Uploader.options, opts );
this._init( this.options );
}
之后写应用代码调用uploader实例的方法,比如:
uploader.addButton({
id: '#filePicker2',
label: '继续添加'
});
再比如:
stats = uploader.getStats(); //获取上传统计数据
点击按钮开始上传这个事件绑定是在应用程序upload.js中设置的:
$upload.on('click', function() { // $upload就是上传按钮元素对象。
if ( state === 'ready' ) {
uploader.upload(); // upload()方法就是执行request('start-upload',args),就是执行start-upload命令函数
Uploader.prototype[ upload ] = function() {
return this.request( 'start-upload', arguments );
// start-upload命令函数
startUpload: function(file) { // 点击"开始上传"按钮执行startupload,开始上传第一步是getfile,并不是真正开始上传
if ( file ) {
}else { // 执行这儿,获取文件,改变文件在queue里面的状态从inited->queued
$.each( me.request( 'get-files', [ Status.INITED ] ), function() { //request返回获取的文件,再循环处理每一个文件
this.setStatus( Status.QUEUED ); // this代表循环项(获取的文件对象)
setStatus: function( status, text ) {
this.trigger( 'statuschange', status, prevStatus ); //处理文件/queue/stats
}
});
Base.nextTick( me.__tick ); // 延迟执行__tick,uploader实例初始化时定义了__tick,就是执行_tick绑定uploader实例
this.__tick = Base.bindFn( this._tick, this ); //bindfn返回function(){_tick.apply(this,args)},__tick就是执行_tick绑定作用域
me.owner.trigger('startUpload'); //开始上传时要进行一些相关的状态数据处理,应用代码也可以绑定这些事件进行一些状态处理
_startSend: function( block ) {
promise = me.request( 'before-send', block, function() { // 先执行before-send命令,再执行回调,before-send命令不存在,因此就是延迟一秒执行callback(传入的匿名函数)
if ( file.getStatus() === Status.PROGRESS ) {
me._doSend( block ); // dosend是真正的上传代码,之前都是预处理,预处理还涉及到统计和进度数据处理,非常复杂。
}
});
_tick: function() { // 异步调度执行_startsend
me._startSend( val );
// 做上传操作。
_doSend: function( block ) { // 从startupload开始经过多次延迟异步调度才执行到这儿真正开始上传,封装层次非常多非常细致复杂
// 开始发送。
tr.appendBlob( opts.fileVal, block.blob, file.name );
tr.append( data );
tr.setRequestHeader( headers );
tr.send(); // 底层用xhr = new XMLHttpRequest()上传文件
命令的定义:
$.each({
upload: 'start-upload',
stop: 'stop-upload',
。。。
}, function( fn, command ) {
Uploader.prototype[ fn ] = function() {
return this.request( command, arguments );
};
});
调用命令函数的方式,比如:
promise = me.request( 'before-send', block, function() { //先执行before-send命令函数,再执行匿名函数(回调),并且返回promise,根据命令函数执行结果resolve返回的promise对象
some code 。。。
});
promise.fail(function() {} //注册失败回调, 如果失败则执行回调
执行命令函数的request代码是webuploader源代码的精华之一,非常复杂高深,下面就分析一下:
request: function( apiName, args, callback ) { //apiName比如是"start-upload"
for ( ; i < len; i++ ) { // 相当于假定每个组件都有apiName函数,要把每个组件的apiName函数都执行一遍
widget = widgets[ i ];
rlt = widget.invoke( apiName, args ); // invoke就是调用执行函数,这里就是执行apiName对应的函数并传递widget组件为作用域
}
// 如果有callback,则用异步方式。
if ( callback || dfds.length ) { // callback就是调用request时传入的匿名函数
return promise[ key ](function() { // 这个匿名函数就是为了延迟执行下一个then(callback),相当于setTimeout()的作用
var deferred = Base.Deferred(),
args = arguments;
if ( args.length === 1 ) {
args = args[ 0 ];
}
setTimeout(function() {
deferred.resolve( args );
}, 1 );
return deferred.promise();
})[ callback ? key : 'done' ]( callback || Base.noop );
//这个return语句超级复杂,它其实就是promise.then(fn).then(callback),由于promise是when返回的,因此是已经resolved的,会立即执行第一个then(),fn代码resolve返回的promise才执行下一个then(callback)里面的callback,也就是延迟执行callback。
//这个return语句的意思就是,在执行完apiName函数之后,再延迟执行callback,并返回promise对象,这样在调用request之后还可以写比如promise.fail(fn),继续下一个异步处理。
} else {
return rlts[ 0 ]; //最简单的情况就是返回执行apiName函数的结果数据,比如get-file执行完就是返回获取到的文件对象WUfile。
}
},
webuploader的功能模块有一部分用widget组件方式,还有一部分用new实例方式,html5runtime用new实例方式实现。
至于trigger/on那是webuploader自己设计的一套逻辑事件机制,用于异步调度执行相关过程,因为在操作过程中,会有几十个上百个异步过程需要调度执行,需要同步,比如过程2要保证在过程1之后执行。
tick也是延迟/异步调度方法。
webuploader博大精深,本文只是一个粗浅的分析,抱着学习人家对象编程技术的态度,文中错误不妥之处欢迎指正交流,呵呵。