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博大精深,本文只是一个粗浅的分析,抱着学习人家对象编程技术的态度,文中错误不妥之处欢迎指正交流,呵呵。

 

posted @ 2018-05-31 11:56  pzhu1  阅读(453)  评论(0编辑  收藏  举报