Pluggable Angular UI (Part I)
In this blog post, I would like to share my thoughts how to build a pluggable Angular Frontend with modules which you can combine together like a puzzle. Modules should not know each other — that means, they don’t have cross-dependencies. Modules should be lazy loaded at runtime by means of backend configuration which specifies what modules your UI has exactly. In other words, the functionality scope depends on the logged in user, more exactly on the user role, such as “customer”, “developer”, etc. You got the idea.
In the first part, I will explain the bootstrapping process of such lazy loaded modules. In the second part, I will try to describe how the modules communicate. This inter-module communication happens by so called extension points — a well-known concept (design pattern) from the Eclipse RCP world. In the picture above, you see a diagram from my current web project called “SSP UI”. There is a core module. This is a core part doing bootstrapping. The core module is always present in the application. Every other separate module may reference this core module. The core module on his part doesn’t know about specific code of separate modules (no references) because such modules are loaded at runtime. Otherwise, the project would not be compilable.
The first step is to create separate chunks for all available modules which can potentially be lazy loaded. This task can be accomplished in
angular.json file. The
lazyModules section contains paths to all available Angular’s module files. Example:
Netanel Basal has already blogged about how to load non-routable modules in Angular: “The Need for Speed: Lazy Load Non-Routable Modules in Angular”. He used a special loader —
SystemJsNgModuleLoader and explained very well how to load modules by components. In his example, a modules’ root component was created and injected dynamically. But how to lazy load modules with providers? Imagine, you have a lot of services (and probably no components) in your modules which you want to instantiate with the help of the Angular dependency injection. Well, first, you need some configuration file where you reference modules in a similar manner as in the routes with loadChildren.
Second, you need a service, say
ConfigurationService, in order to be able to load the configuration. The configration should be loaded before the application is completely initialized. Angular has a special provider token for that — APP_INITIALIZER. Note, the factory method returns a
Promise. Once the
Promise is resolved, Angular starts the bootstrapping process.
The service itself looks like as follows:
The loaded configuration is saved in this service. In the module classes, we need two static fiels:
providers. The first one lists components shipped with the module (if any). The second one lists providers and their dependencies (other providers in constructors) in the same syntax as Angular does it. A typical module looks like as follows:
AppComponent uses the Facade Pattern. Note, that we’re doing the shutdown work within the window’s
beforeunload callback. Why not in
ngOnDestroy only works when the route is changed. We also need the root injector for our main goal— bootstrapping providers of lazy loaded modules.
The facade utilizes the
ModuleLifecycleService which I will show in a moment. This service exposes two methods:
shutdownApplication. The first method returns a boolean
Promise notifying the caller if all modules could be successfully loaded or not.
Take a breath at this point and look how the app logs lazy loaded modules with providers and components :-)
After this short pause, we continue our adventure. Let’s define two interfaces.
bootstrapApplication method in the
ModuleLifecycleService accesses the mentioned above configuration. Every
NgModuleFactory is loaded through the
NgModuleFactoryLoader. Here is the full code. We’re going to discuss the handling of module factories shortly after you have looked into the code.
For every module factory we’re doing repetitive handling:
NgModuleRefby means of the root injector.
NgModuleRefrepresents an instance of an
NgModule. We need it in the next steps.
- Check if the module has components (remember the static field
components?). If it’s true, a
ComponentFactoryis created and saved in the model. The
ComponentFactoryis needed later if you would like to create the corresponding component dynamically. See Angular documentation for more details.
- Check if the module has providers (remember the static field
providers?). If it’s true, the providers are registered and instantiated programmatically. This can be completed with the help of
Injector.create(providers: StaticProvider, parent?: Injector). See Angular documentation for more details.
If you make a mistake in the configuration, e.g. a typo in the module’s path or name, you will face an exception.
If a module has no providers, it’s a little bit strange. Right? In this case, you will face a warning.
That’s all. Stay tuned, the second part with extension points is coming soon!