@softeq/angular-lazy
v1.0.0-beta.1
Published
Support of "non-route lazy loading" in Angular
Downloads
1
Readme
@softeq/angular-lazy
@softeq/angular-lazy
allows to load modules lazily without opening of a route.
Note!!! There is no any magic in this library and it does rely on @angular/router
to load modules. This library has strong limitations described here below. READ THEM BEFORE USING THIS LIBRARY.
Description
Let's consider an example to better understand when this library can be helpful.
Example
We have a huge enterprise application.
Its substantial part presents typical web application having a large number of pages (MAIN
part of our application).
Also it has complex component displayed in overlay (or dialog) (SECONDARY
part of our application).
Say, this component can display PDF
and should work with three.js
.
This way we have two following Angular modules
// ====================== app-main.module.ts =========================
@NgModule({
...
})
export class AppMainModule {
}
// ====================== app-secondary.module.ts =========================
@NgModule({
...
})
export class AppSecondaryModule {
}
Lazy loading chunks of MAIN
application part
It is a good solution to split our application on chunks to avoid loading whole application.
Angular has great and ease to use tools to fulfill this task:
we can split our application on lazy loaded modules and tell @angular/router
how and when to load them,
everything else Angular does for us.
// ====================== app-main.module.ts =========================
@NgModule({
imports: [
RouterModule.forRoot([{
path: 'main-part1',
loadChildren: 'app-main-part1.module#AppPart1Module',
}, {
path: 'main-part2',
loadChildren: 'app-main-part2.module#AppPart2Module',
}]),
]
})
export class AppMainModule {}
// =================== app-main-part1.module.ts ======================
@NgModule({
imports: [
RouterModule.forChild([
...
]),
]
})
export class AppMainPart1Module {}
// =================== app-main-part2.module.ts ======================
@NgModule({
imports: [
RouterModule.forChild([
...
]),
]
})
export class AppMainPart2Module {}
According to this configuration module AppMainPart1Module
will be loaded when user comes under /main-part1
route and module AppMainPart2Module
will be loaded when user comes under /main-part2
route.
This solution optimizes well MAIN
part of the application: now we split it on separate chunks loaded only when they are really necessary.
Lazy loading of SECONDARY
application part
The SECONDARY
part of our application is also very large, because it has pdf support (pdf.js
), three.js
and may be some other libraries. Moreover SECONDARY
part may be necessary only for small part of users. There is no sense to blow up size of application in behalf of functionality which may be necessary only for some users. It would be a good choice to load SECONDARY
part only when it will be really necessary. The same way as we do for chunks of MAIN
application part.
So, we need to load SECONDARY
part of our application lazily. In turn, Angular offers the only standard way to do this: load module lazily when user goes by some route. Lazy loading of modules is deeply integrated into Angular framework and Angular build toolchain.
Unfortunately SECONDARY
part of application is opened in overlay or dialog rather than by route. Angular does not provide ease to use way to load module without associated route.
Solution this library proposes
This library proposes a solution to solve problem described here above. So, you can load modules lazily without opening a route.
- this solution is fully integrated into Angular build toolchain
- the irony is that this library still relies on
@angular/router
and it does require at least one route to be defined in loaded module - this library has strong limitations described here below
Angular module becomes helpful when it does something. Typical Angular module exports services, directives, pipes, and components, There is no exception for lazy loaded module. Typical lazy loaded module embeds (exports) ROUTES
into parent route configuration.
@NgModule({
RouterModule.forChild([
{ path: 'route1', component: Route1Component }
]),
})
export class TypicalLazyLoadedModule {
}
In the terms of @softeq/angular-lazy
library, lazy loaded module exports set of named data, called lazy symbols. Each lazy symbol has text name and some value. For example, AppSecondaryModule
can export symbol TestComponent
(which is an Angular component) under the name test
.
The following steps describe how to setup this library.
- Init library in the root application module
@NgModule({ imports: [ SofteqLazyModule.forRoot(), ], }) export class AppMainModule {}
- Init library in the lazy loaded module
@NgModule({ imports: [ SofteqLazyModule.forChild(), ], }) export class AppSecondaryModule {}
- Add lazy activator route to the lazy loaded module
@NgModule({ imports: [ SofteqLazyModule.forChild(), RouterModule.forChild([ lazyActivatorRoute, ]), ], }) export class AppSecondaryModule {}
- Add link to the lazy loaded module from the root module
@NgModule({ imports: [ SofteqLazyModule.forRoot(), RouterModule.forRoot([{ path: 'lazy-loaded', loadChildren: 'app-secondary.module#AppSecondaryModule' }]), ], }) export class AppMainModule {}
- Export lazy symbols from the lazy loaded module using
LAZY_SYMBOLS
token@NgModule({ imports: [ SofteqLazyModule.forChild(), RouterModule.forChild([ lazyActivatorRoute, ]), ], declarations: [ TestComponent, ], providers: [ { provide: LAZY_SYMBOLS, useValue: { test: TestComponent } }, // you can use useFactory instead ], }) export class AppSecondaryModule {}
How to load lazy symbol
To load lazy symbol anywhere in the application, inject LazyRegistry
service.
constructor(private lazyRegistry: LazyRegistry) {}
and use it to load lazy symbol
this.lazyRegistry.load('lazy-loaded', 'test').subscribe((loaded) => {
loaded.injector; // root Injector of lazy loaded module
loaded.symbol; // loaded symbol
})
parameters of load
method
- the first parameter is lazy route path (defined in
AppMainModule
) - the second parameter is a name of exported symbol (all lazy symbols were exported under
LAZY_SYMBOLS
token)
load
method returns pair of symbol
(TextComponent
in our case) and injector
. injector
is a root injector in lazy loaded module. Injector
is important to resolve returned symbol
.
Why do you need injector
returned by load
method?
For example, we want to load component from lazy loaded module and create it somewhere in the application. We can write something like
@Component({ ... })
export class SomeAppComponent implements OnInit {
constructor(private viewContainerRef: ViewContainerRef,
private lazyRegistry: LazyRegistry,
// we need componentFactoryResolver to create component dynamically
private componentFactoryResolver: ComponentFactoryResolver) {
}
ngOnInit(): void {
// load TestComponent from lazy-loaded module
this.lazyRegistry.load('lazy-loaded', 'test').subscribe((loaded) => {
// find ComponentFactory for TestComponent using exported component class
const componentFactory = this.componentFactoryResolver.resolveComponentFactory(loaded.symbol);
// use ComponentFactory to create component
this.viewContainerRef.createComponent(componentFatory);
});
}
}
Actually this code will not work, because ComponentFactoryResolver
cannot find ComponentFactory
for component loaded from another module.
To load ComponentFactory
correctly, you have to write something like
// take ComponentFactoryResolver from the lazy loaded module
const componentFactoryResolver = loaded.injector.get(ComponentFactoryResolver);
// use retrieved ComponentFactoryResolver to find ComponentFactory by the component class
const componentFactory = componentFactoryResolver.resolveComponentFactory(loaded.symbol);
// use ComponentFactory to create component
this.viewContainerRef.createComponent(componentFatory);
The same problem will be with services, if you need to resolve service defined in lazy loaded module you have to use returned injector
.
const someServiceDefinedInLazyLoadedModule = loaded.injector.get(loaded.symbol);
Limitations
@softeq/angular-lazy
relies on @angular/router
. For example, the following code
this.lazyRegistry.load('lazy-loaded', 'test')
tries to open route /lazy-loaded/@lazy-activator
. @lazy-activator
route is defined by lazyActivatorRoute
value. Actually, this route will never be opened, because @lazy-activator
defines CanActivate
service which always returns false
.
The only purpose of
@lazy-activator
route is to registerLAZY_SYMBOLS
There are two known limitations related to this behavior.
- You cannot use
load
function while you in in process of routing.
For example, you should not useload
function inCanActivate
orCanDeativate
services, becauseload
function will try to start another route. This way you have current route and starts another one. This can break expected behavior and you can get hard to resolve errors. - It is a common practice to use
CanDeactivate
handler to suppress leaving of page or some other behavior. When you callload
function,@angular/router
tries to leave current route and call itsCanDeactivate
services. Probably, this hardly expected behavior, because@lazy-activator
route will never be opened. It is recommended to ignore opening of@lazy-activator
route in yourCanDeactivate
handlers.@softeq/angular-lazy
simplifies this task by providingisRouterStateLazyActivator
function which determines if url belongs to@lazy-activator
.@Injectable() export class CanDeactivateSample implements CanDeactivate<any> { canDeactivate(component: any, route: ActivatedRouteSnapshot, state: RouterStateSnapshot, nextState?: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean { // always allow any lazy activator route as it never leads to location change if (nextState && isRouterStateLazyActivator(nextState)) { return true; } ... } }
Do you really need this library?
In general this library proposes kind of workaround and should be considered as extreme measure. May be it is better to solve initial problem in the field of UX. I believe, even example given here above points to bad UX.