更改状态时延迟加载 Angular 组件脚本 [英] Lazy Loading of Angular Component Scripts when Changing States
问题描述
这个问题一直困扰着最后一天左右.
This problem has been taking up the last day or so.
我一直试图让我的 AngularJS 应用程序懒惰地加载每个状态组件的脚本文件.我正在 Angular 中处理一个大型项目,index.html
文件已经变成了 100 多个 标签,包括用于各种控制器的 JS、服务和图书馆.它们中的大多数都很小,所以加载时间并不是一个大问题(尽管它可能是),但在我看来它从来都不是干净的.
I've been trying to get my AngularJS application to load the script files for each state's components lazily. I'm working on a large project at work in Angular, and the index.html
file has morphed into over 100 <script>
tags including the JS for the various controllers, services, and libraries. Most of them are small, so it isn't so much that loading time is a HUGE problem (though it could be), but it just never looked clean to me.
也许是因为我已经习惯了 PHP 的自动加载器,或者只是被所有可以在编译时加载自己的依赖项的语言宠坏了.必须在应用程序的根文档中为一些次要的边缘状态指令加载脚本,或者对于该指令实际所属的模块,如果它被移动到另一个没有 的应用程序中,则不加载脚本本身并不是模块化的
荣耀列表.
Maybe it's because I've gotten used to PHP's autoloader or have just been spoiled by all of the languages that can load their own dependencies at compile time. It's just not modular to have to load scripts for some minor, fringe state's directive in the root document of the application, or for the module that directive actually belongs to not to load the script itself if it's moved into another application without the <script>
list of glory.
无论哪种方式,我都在开始一个新项目并想让它保持干净,但是以这种方式将组件加载到 Angular 会带来许多挑战.其中很多问题已经在文档或博客文章、SO 问题或其他问题中一次或多次得到解决,但我还没有看到与其他 Angular 组件完美集成的端到端解决方案.>
Either way, I'm starting a new project and want to keep it cleaner, but loading components into Angular in this way presents a number of challenges. A lot of them have been addressed at one time or another in the documentation or some blog post, SO question, or another, but I've yet to see an end-to-end solution that integrates cleanly with other Angular components.
- 如果页面呈现时 Angular 和模块已经加载,Angular 只会引导
ng-app
指令.即使使用延迟加载启动应用程序也需要一种解决方法. - 模块 API 的方法仅在应用程序启动之前起作用.在应用程序启动后注册新的控制器、指令、过滤器或服务,但在实际加载定义它们的脚本之后(以及实际需要它们时)需要一种解决方法.
- 延迟加载脚本和调用基于 AJAX 的服务都需要调用回调,将服务调用的结果注入状态控制器需要这些服务实际存在才能在状态转换开始时被调用.实际上调用一个延迟加载的服务并在状态改变之前解决它......需要一种解决方法.
- 所有这些都需要以一种看起来不笨拙的方式组合在一起,并且可以轻松地在多个应用程序中重复使用,而无需每次都重新发明轮子.
- Angular only bootstraps the
ng-app
directive if Angular and the modules are already loaded when the page is rendered. Even starting the application with lazy-loading requires a workaround. - The module API's methods only work before an application is bootstrapped. Registering new controllers, directives, filters, or services after the application has been bootstrapped, but after the scripts defining them have actually been loaded (and when they're actually needed) requires a workaround.
- Both lazy loading scripts and invoking AJAX-based services require the invocation of callbacks, and injecting the result of service calls into state controllers requires those services to actually exist to be called when the state transition starts. Actually INVOKING a lazily loaded service and resolving it before the state changes...requires a workaround.
- All of this needs to fit together in a way that doesn't look kludgy and can easily be reused in multiple applications without reinventing the wheel each time.
我已经看到了 #1 和 #2 的答案.显然,angular.bootstrap
可用于启动一个在没有 ng-app
指令的情况下加载整个页面后的模块.在引导后添加组件不太明显,但是 保存对各种 $provider 的引用
配置块中的服务可以解决问题, 更无缝地覆盖module
API.解决 #3 并以满足 #4 的方式完成所有工作有点难以捉摸.
I've seen answers to #1 and #2. Obviously, angular.bootstrap
can be used to start up a module after the whole page has loaded without an ng-app
directive. Adding components after bootstrapping is a little less obvious, but saving references to the various $provider
services in the config blocks does the trick, overwriting the module
API more seamlessly so. Resolving #3 and doing it all in a way that satisfies #4 has been a bit more elusive.
上面解决 #2 的例子是针对控制器和指令的.添加服务变得有点复杂,异步的,延迟加载的,并且意味着将它们的数据提供给延迟加载的控制器,尤其是这样.对于 Isitor 先生,他的代码当然适用于将控制器注册为概念证明,但代码的编写方式并不能轻松扩展到延迟加载脚本有意义的应用程序类型,包含数十到数百个包含、依赖项和异步服务的更大的应用程序.
The above examples solving #2 were for controllers and directives. Adding in services turns out to be a little bit more complicated, asynchronous ones, lazily loaded, and meant to provide their data to a lazily loaded controller especially so. With respect to Mr. Isitor, his code certainly works for registering a controller as a proof of concept, but the code is not written in a way that easily scales up to the kind of application for which lazy-loading the scripts makes sense, a much larger application with tens to hundreds of includes, dependencies, and asynchronous services.
我将发布我想出的解决方案,但如果有人有改进它的建议,或者已经找到了一种截然不同的更好的方法,请随时添加.
I'm going to post the solution I came up with, but if anyone has suggestions to improve it or has already found a dramatically and radically different, better way, please feel free to add it on.
推荐答案
这是 Angular 模块 lazy
的代码,取决于 ui.router
模块.当它包含在模块的依赖项中时,状态脚本的延迟加载功能将被启用.我已经包含了主要应用程序模块、一些惰性组件和我的 index.html
的示例,为了演示目的而进行了清理.我正在使用 Script.js
库来实际处理脚本加载.
Here's the code for an Angular module lazy
, depending on the ui.router
module. When it's included in your module's dependencies, the lazy loading functionality of the state's scripts will be enabled. I've included examples of the primary app module, a few lazy components, and my index.html
, sanitized for demonstration purposes. I'm using the Script.js
library to actually handle the script loading.
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 中,因为我更喜欢语法
Function Hacked into Script.js because I Prefer the Syntax
$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);
});
})();
这篇关于更改状态时延迟加载 Angular 组件脚本的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!