更改状态时延迟加载角度组件脚本

时间:2014-02-12 23:51:36

标签: javascript angularjs angular-ui-router

这个问题在最后一天消失了。

我一直试图让我的AngularJS应用程序懒洋洋地为每个州的组件加载脚本文件。我正在开发一个在Angular上工作的大型项目,index.html文件已经变成了100多个<script>标签,包括各种控制器,服务和库的JS。它们中的大多数都很小,所以加载时间并不是一个巨大的问题(虽然它可能是这样),但它对我来说从来都不是很干净。

也许是因为我已经习惯了PHP的自动加载器,或者刚被编译时可以加载自己的依赖项的所有语言所破坏。如果必须在应用程序的根文档中为某些次要的边缘状态指令加载脚本,或者对于指令实际所属的模块,如果它被移动到没有{{1的另一个应用程序荣耀之列。

无论哪种方式,我都在开始一个新项目,并希望保持清洁,但以这种方式将组件加载到Angular中会带来许多挑战。其中很多已经在文档或某些博客文章,SO问题或其他问题上得到了解决,但我还没有看到与其他Angular组件完全集成的端到端解决方案。

  1. 如果在呈现页面时已经加载了Angular和模块,则仅引导<script>指令。即使使用延迟加载启动应用程序也需要一种解决方法。
  2. 模块API的方法仅在引导应用程序之前有效。在引导应用程序之后注册新的控制器,指令,过滤器或服务,但在实际加载了定义它们的脚本之后(以及实际需要它们时)需要一种解决方法。
  3. 延迟加载脚本和调用基于AJAX的服务都需要调用回调,并且将服务调用的结果注入状态控制器要求在状态转换开始时调用实际存在的服务。实际上 INVOKING 一个延迟加载的服务并在状态发生变化之前解决它...需要一个解决方法。
  4. 所有这些都需要以一种看起来不那么笨拙的方式组合在一起,并且可以轻松地在多个应用程序中重复使用,而不必每次都重新发明轮子。
  5. 我见过#1和#2的答案。显然,angular.bootstrap可用于在整个页面加载后没有ng-app指令时启动模块。引导后添加组件不太明显,但saving references to the various $provider services in the config blocks does the trickoverwriting the module API more seamlessly so。解决#3并以满足#4的方式完成所有这些工作更加难以捉摸。

    上述解决#2的例子是针对控制器和指令的。添加服务变得有点复杂,异步,延迟加载,并且意味着将数据提供给延迟加载的控制器,尤其如此。关于Isitor先生,他的代码当然适用于将控制器注册为概念证明,但是代码的编写方式不容易扩展到那些对脚本有意义的延迟加载的应用程序,更大的应用程序,包含数十到数百个包含,依赖项和异步服务。

    我将发布我提出的解决方案,但如果有人提出改进建议或者已经找到了一种极其不同的,更好的方法,请随意添加。

1 个答案:

答案 0 :(得分:10)

以下是Angular模块lazy的代码,具体取决于ui.router模块。当它包含在模块的依赖项中时,将启用状态脚本的延迟加载功能。我已经包含了主要应用程序模块的示例,一些惰性组件以及我的index.html,为了演示目的而进行了清理。我正在使用Script.js库来实际处理脚本加载。

<强>角-UI-路由器lazy.js

/**
 * Defines an AngularJS module 'lazy' which depends on and extends the ui-router
 * module to lazy-load scripts specified in the 'scripts' attribute of a state
 * definition object.  This is accomplished by registering a $stateChangeStart
 * event listener with the $rootScope, interrupting the associated state change
 * to invoke the included $scriptService which returns a promise that restarts the
 * previous state transition upon resolution.  The promise resolves when the
 * extended Script.js script loader finishes loading and inserting a new <script>
 * tag into the DOM.
 *
 * Modules using 'lazy' to lazy-load controllers and services should call lazy.makeLazy
 * on themselves to update the module API to inject references for the various $providers 
 * as the original methods are only useful before bootstrapping, during configuration,
 * when references to the $providers are in scope.  lazy.makeLazy will overwrite the
 * module.config functions to save these references so they are available at runtime,
 * after module bootstrapping.
 * See http://ify.io/lazy-loading-in-angularjs/ for additional details on this concept
 *
 * Calls to $stateProvider.state should include a 'scripts' property in the object
 * parameter containing an object with properties 'controllers', 'directives', 'services',
 * 'factories', and 'js', each containing an array of URLs to JS files defining these
 * component types, with other miscelleneous scripts described in the 'js' array.
 * These scripts will all be loaded in parallel and executed in an undefined order
 * when a state transition to the specified state is started.  All scripts will have
 * been loaded and executed before the 'resolve' property's promises are deferred,
 * meaning services described in 'scripts' can be injected into functions in 'resolve'.
 */

 (function() {
    // Instantiate the module, include the ui.router module for state functionality
    var lazy = angular.module('lazy',['ui.router']);

    /**
     * Hacking Angular to save references to $providers during module configuration.
     * 
     * The $providers are necessary to register components, but they use a private injector
     * only available during bootstrap when running config blocks.  The methods attached to the
     * Vanilla AngularJS modules rely on the same config queue, they don't actually run after the
     * module is bootstrapped or save any references to the providers in this injector.
     * In makeLazy, these methods are overwritten with methods referencing the dependencies
     * injected at configuration through their run context.  This allows them to access the
     * $providers and run the appropriate methods on demand even after the module has been
     * bootstrapped and the $providers injector and its references are no longer available.
     *
     * @param module      An AngularJS module resulting from an angular.module call.
     * @returns module    The same module with the provider convenience methods updated
     * to include the DI $provider references in their run context and to execute the $provider
     * call immediately rather than adding calls to a queue that will never again be invoked.
     */
    lazy.makeLazy = function(module) {
      // The providers can be injected into 'config' function blocks, so define a new one
      module.config(function($compileProvider,$filterProvider,$controllerProvider,$provide) {
        /**
         * Factory method for generating functions to call the appropriate $provider's
         * registration function, registering a provider under a given name.
         * 
         * @param registrationMethod    $provider registration method to call
         * @returns function            A function(name,constructor) calling
         * registationMethod(name,constructor) with those parameters and returning the module.
         */
        var register = function(registrationMethod) {
          /**
           * Function calls registrationMethod against its parameters and returns the module.
           * Analogous to the original module.config methods but with the DI references already saved.
           *
           * @param name          Name of the provider to register
           * @param constructor   Constructor for the provider
           * @returns module      The AngularJS module owning the providers
           */
          return function(name,constructor) {
            // Register the provider
            registrationMethod(name,constructor);
            // Return the module
            return module;
          };
        };

        // Overwrite the old methods with DI referencing methods from the factory
        // @TODO: Should probably derive a LazyModule from a module prototype and return
        // that for the sake of not overwriting native AngularJS code, but the old methods
        // don't work after `bootstrap` so they're not necessary anymore anyway.
        module.directive = register($compileProvider.directive);
        module.filter = register($filterProvider.register);
        module.controller = register($controllerProvider.register);
        module.provider = register($provide.provider);
        module.service = register($provide.service);
        module.factory = register($provide.factory);
        module.value = register($provide.value);
        module.constant = register($provide.constant);
      });
      // Return the module
      return module;
    };

    /**
     * Define the lazy module's star $scriptService with methods for invoking
     * the extended Script.js script loader to load scripts by URL and return
     * promises to do so.  Promises require the $q service to be injected, and
     * promise resolutions will take place in the Script.js rather than Angular
     * scope, so $rootScope must be injected to $apply the promise resolution
     * to Angular's $digest cycles.
     */
    lazy.service('$scriptService',function($q,$rootScope) {
      /**
       * Loads a batch of scripts and returns a promise which will be resolved
       * when Script.js has finished loading them.
       *
       * @param url   A string URL to a single script or an array of string URLs
       * @returns promise   A promise which will be resolved by Script.js
       */
      this.load = function(url) {
        // Instantiate the promise
        var deferred = $q.defer();
        // Resolve and bail immediately if url === null
        if (url === null) { deferred.resolve(); return deferred.promise; }
        // Load the scripts
        $script(url,function() {
          // Resolve the promise on callback
          $rootScope.$apply(function() { deferred.resolve(); });
        });
        // Promise that the URLs will be loaded
        return deferred.promise;
      };

      /**
       * Convenience method for loading the scripts specified by a 'lazy'
       * ui-router state's 'scripts' property object.  Promises that all
       * scripts will be loaded.
       *
       * @param scripts   Object containing properties 'controllers', 'directives',
       * 'services', 'factories', and 'js', each containing an array of URLs to JS
       * files defining those components, with miscelleneous scripts in the 'js' array.
       * any of these properties can be left off of the object safely, but scripts
       * specified in any other object property will not be loaded.
       * @returns promise   A promise that all scripts will be loaded
       */
      this.loadState = function(scripts) {
        // If no scripts are given, instantiate, resolve, and return an easy promise
        if (scripts === null) { var d = $q.defer; d.resolve(); return d; }
        // Promise that all these promises will resolve
        return $q.all([
          this.load(scripts['directives'] || null),
          this.load(scripts['controllers'] || null),
          this.load(scripts['services'] || null),
          this.load(scripts['factories'] || null),

          this.load(scripts['js'] || null)
        ]);
      };
    });

    // Declare a run block for the module accessing $rootScope, $scriptService, and $state
    lazy.run(function($rootScope,$scriptService,$state) {
      // Register a $stateChangeStart event listener on $rootScope, get a script loader
      // for the $rootScope, $scriptService, and $state service.
      $rootScope.$on('$stateChangeStart',scriptLoaderFactory($scriptService,$state));
    });

    /**
     * Returns a two-state function for handing $stateChangeStart events.
     * In the first state, the handler will interrupt the event, preventing
     * the state transition, and invoke $scriptService.loadState on the object
     * stored in the state definition's 'script' property.  Upon the resolution
     * of the loadState call, the handler restarts a $stateChangeStart event
     * by invoking the same transition.  When the handler is called to handle
     * this second event for the original state transition, the handler is in its
     * second state which allows the event to continue and the state transition
     * to happen using the ui-router module's default functionality.
     *
     * @param $scriptService    Injected $scriptService instance for lazy-loading.
     * @param $state            Injected $state service instance for state transitions.
     */
    var scriptLoaderFactory = function($scriptService,$state) {
      // Initialize handler state
      var pending = false;
      // Return the defined handler
      return function(event,toState,toParams,fromState,fromParams) {
        // Check handler state, and change state
        if (pending = !pending) {   // If pending === false state
          // Interrupt state transition
          event.preventDefault();
          // Invoke $scriptService to load state's scripts
          $scriptService.loadState(toState.scripts)
            // When scripts are loaded, restart the same state transition
            .then(function() { $state.go(toState,toParams); });
        } else {  // If pending === true state
          // NOOP, 'ui-router' default event handlers take over
        }
      };
    };
  })();

/** End 'lazy' module */

<强>的index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Lazy App</title>
    <script type='text/javascript' src='libs/script.js'></script>
    <script type='text/javascript'>
      $script.queue(null,'libs/angular/angular.min.js','angular')
             .queue('angular','libs/angular/angular-ui-router.min.js','ui-router')
             .queue('ui-router','libs/angular/angular-ui-router-lazy.js','lazy')
             .queue('lazy',null,'libs-angular')

             .queue('libs-angular','lazyapp/lazyapp.module.js','lazyapp-module');

      $script.ready('lazyapp-module',function() { console.log('All Scripts Loaded.'); });
    </script>
  </head>

  <body>
    <div ui-view='mainView'></div>
  </body>
</html>

功能被黑客入侵Script.js因为我更喜欢语法

$script.queue = function(aQueueBehind,aUrl,aLabel) {
  if (aQueueBehind === null) { return $script((aUrl === null?[null]:aUrl),aLabel); }
  $script.ready(aQueueBehind,function() {
    if (aUrl !== null)
      $script(aUrl,aLabel);
    else
      $script.done(aLabel);
  });
  return $script;
}

<强> lazyapp.module.js

(function() {
  var lazyApp = angular && angular.module('lazyApp ',['lazy']);
  lazyApp = angular.module('lazy').makeLazy(lazyApp);

  lazyApp.config(function($stateProvider) {

    $stateProvider.state({
      name: 'root',
      url: '',
      views: {
        'mainView': { templateUrl: '/lazyapp/views/mainview.html', controller: 'lazyAppController' }
      },
      scripts: {
        'directives': [ 'lazyapp/directives/lazyheader/src/lazyheader.js' ],
        'controllers': [ 'lazyapp/controllers/lazyappcontroller.js' ],
        'services': [ 'lazyapp/services/sectionservice.js' ]
      },
      resolve: {
        sections: function(sectionService) {
          return sectionService.getSections();
        }
      }
    });
  });

  angular.bootstrap(document,['lazyApp']);
})();

<强> sectionservice.js

(function() {
  var lazyApp = angular.module('lazyApp');

  lazyApp.service('sectionService',function($q) {
    this.getSections = function() {
      var deferred = $q.defer();
      deferred.resolve({
        'home': {},
        'news': {},
        'events': {},
        'involved': {},
        'contacts': {},
        'links': {}
      });
      return deferred.promise;
    };
  });
})();

<强> lazyheader.js

(function() {
  var lazyApp = angular.module('lazyApp ');

  lazyApp.directive('lazyHeader',function() {
    return {
      templateUrl: 'lazyapp/directives/lazyheader/templates/lazyheader-main.html',
      restrict: 'E'
    };
  });
})();

<强> lazyappcontroller.js

(function() {
  var lazyApp = angular.module('lazyApp ');

  lazyApp.controller('lazyAppController',function(sections) {
    // @TODO: Control things.
    console.log(sections);
  });
})();