【问题标题】:Lazy Loading of Angular Component Scripts when Changing States更改状态时延迟加载 Angular 组件脚本
【发布时间】:2014-02-12 23:51:35
【问题描述】:

这个问题已经占用了最后一天左右。

我一直试图让我的 AngularJS 应用程序懒惰地加载每个状态组件的脚本文件。我正在使用 Angular 开发一个大型项目,index.html 文件已演变为 100 多个 <script> 标记,包括用于各种控制器、服务和库的 JS。它们中的大多数都很小,所以加载时间并不是一个大问题(尽管它可能是),但它对我来说从来都不是干净的。

也许是因为我已经习惯了 PHP 的自动加载器,或者只是被所有可以在编译时加载它们自己的依赖项的语言宠坏了。必须为应用程序的根文档中的一些次要、边缘状态的指令加载脚本,或者对于该指令实际所属的模块,如果它被移动到另一个没有<script> 的应用程序中,则不加载脚本本身,这不是模块化的。荣耀列表。

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

  1. 如果 Angular 和模块在页面渲染时已经加载,Angular 只会引导 ng-app 指令。即使使用延迟加载启动应用程序也需要解决方法。
  2. 模块 API 的方法仅在应用程序启动之前有效。在应用程序启动之后注册新的控制器、指令、过滤器或服务,但在实际加载定义它们的脚本之后(以及在实际需要它们时)需要一种解决方法。
  3. 延迟加载脚本和调用基于 AJAX 的服务都需要调用回调,并且将服务调用的结果注入状态控制器需要这些服务实际存在才能在状态转换开始时被调用。实际上 INVOKING 延迟加载的服务并在状态更改之前解决它...需要一种解决方法。
  4. 所有这些都需要以一种看起来不笨拙的方式组合在一起,并且可以轻松地在多个应用程序中重复使用,而无需每次都重新发明轮子。

我已经看到了#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 先生,他的代码当然可以将控制器注册为概念证明,但代码的编写方式并不能轻松扩展到延迟加载脚本有意义的那种应用程序,a具有数十到数百个包含、依赖项和异步服务的更大应用程序。

我将发布我想出的解决方案,但如果有人有改进它的建议或已经找到了一种截然不同的更好的方法,请随时添加它。

【问题讨论】:

    标签: javascript angularjs angular-ui-router


    【解决方案1】:

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

    angular-ui-router-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);
      });
    })();
    

    【讨论】:

    • 我在这段代码中发现了一个错误。 $scriptService 不能正确处理子状态。它不会从父状态加载脚本。我已经在我的项目中修复了这个问题,有机会我会在这里更新代码。
    猜你喜欢
    • 2014-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-03-25
    • 1970-01-01
    • 2020-11-22
    • 2019-09-09
    • 2017-11-02
    相关资源
    最近更新 更多