Requests tracking in Angular application with child module injectors without lazy loading

Vlad Sharikov
Angular In Depth
Published in
12 min readOct 8, 2019

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

In our team, we, Nikita Balakirev and Vlad Sharikov, faced an issue that probably was a problem for other people too. So, below we are sharing our experience.

The problem

Imagine you have a fully-featured administration panel or any other elaborate web-site. This web application may have a header with user data, dynamic menu (where menu items depend on user roles), dashboard (summary, reports, profiles, etc.) and other elements. And one day you will want to know which view initiated which http request? So, you will be facing the request tracking (or “monitoring”, or “audit”) problem.

Example of an admin panel

Why is it a problem? Because, until you solve it you don’t know:

  • what view uses the most part of the server’s resources;
  • if users requests are secure, safe and allowed;
  • how to change some of your metrics to improve user experience and website conversion.

Possible solutions

The most obvious solution is to add a marker to each request in request params or in request headers. However, obvious does not mean optimal: in our project we have about ~2300 GET http calls & ~1000 POST http calls (among others) and adding markers to every request is just not possible.

A better solution is to automatically add a marker for each http request initiated by a client. In Angular applications it can be done with an http interceptor. But an interceptor can’t determine on its own what view initiated a particular request.

In our team, we thought of the following ways.

The Zone.js way

Generally speaking, Zone.js as a part of the Angular infrastructure can be used to separate the contexts of different code fragments. Putting the Zone.current.name global variable in the right place should help out our interceptor. We tried to wrap our views in different zones (fork NgZone and run code of the components), but faced the following critical issues:

  • The Angular change detection mechanism starts with AppRef.tick often (aka NgZone.run) and Zone.current.name will often be angular, which is not what we want.
  • All calls of the run method from a zone forked from NgZone will trigger AppRef.tick significantly slowing down the performance, and if we run outside NgZone, the change detection will not work at all.

It looks like Angular can use Zone.js for change detection purposes only at the moment. We tried to implement some solution by ourselves, but had no luck. We created an issue to angular github repo requesting a feature that would make it possible to compile different parts of an application in certain zones.

The injectors way

Specifying different providers (aka “tokens”) for different module injectors is another way to “separate” logic in our application. This way we can identify the parent module of each entity (component, directive, service, etc). After that, we can use these tokens in HttpInterceptors to track requests.

Structure of NgModules with injected tokens

We wrote a simple example. There are 3 modules: AppModule (root), BusinessModule and AnotherBusinessModule. The business modules are eagerly loaded into AppModule. We assume that each business module implements some separate business logic. Each of it consist of a single component. Each component initiates some requests. We want to mark these requests with business and another-business tokens. Also, AppModule is marked with the root token. There is http interceptor which resolves token and adds it to the requests.

But we have a problem because Angular has only one module-level injector for eager modules and only one token will be registered in the final NgFactory. In other words, the token provided in second imported module will override the first one. The token provided in the root module will override the previous tokens. You can find information on the eager module behavior in the Angular documentation. For an even deeper understanding, you can read Max’s Koretskyi article describing how this is implemented under the hood.

An example of AppModuleNgFactory with a single CUSTOM_INJECTOR_TOKEN

This is a fragment of the generated NgFactory code for the example above.The BusinessModule and AnotherBusinessModule tokens are ignored. Only the token from AppModule is registered in NgFactory.

If you want to dive deeper in it, here is an article by Alexey Zuev about different types of injectors in the Angular framework.

As you know, Angular modules can be downloaded eagerly or lazily.

The Angular documentation also says:

Angular gives a lazy-loaded module its own child injector.

Child module injectors will allow us to provide different tokens for different NgModules. These modules can contain different chunks of business logic of our application. With child injector modules, each module injector (each module) contains its own token. Each request initiated by any component declared in such modules will be marked with a particular token.

As a result, we want to get some NgFactories with different tokens provided into them:

var AppModuleNgFactory = /*@__PURE__*/ /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__[
"ɵcmf"
](
_with_custom_injector_inner_module__WEBPACK_IMPORTED_MODULE_1__[
"AppModule"
],
[],
function(_l) {
return _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmod"]([
// ...
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmpd"](
256,
_tokens__WEBPACK_IMPORTED_MODULE_7__["CUSTOM_INJECTOR_TOKEN"],
"root",
[]
)
// ...
]);
}
);

// ...

var
BusinessModuleNgFactory = /*@__PURE__*/ /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__[
"ɵcmf"
](
_with_custom_injector_inner_module__WEBPACK_IMPORTED_MODULE_1__[
"BusinessModule"
],
[],
function(_l) {
return _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmod"]([
// ...
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmpd"](
256,
_tokens__WEBPACK_IMPORTED_MODULE_7__["CUSTOM_INJECTOR_TOKEN"],
"business",
[]
)
// ...
]);
}
);

// ...

var
AnotherBusinessModuleNgFactory = /*@__PURE__*/ /*@__PURE__*/ _angular_core__WEBPACK_IMPORTED_MODULE_0__[
"ɵcmf"
](
_with_custom_injector_inner_module__WEBPACK_IMPORTED_MODULE_1__[
"AnotherBusinessModule"
],
[],
function(_l) {
return _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmod"]([
// ...
_angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵmpd"](
256,
_tokens__WEBPACK_IMPORTED_MODULE_7__["CUSTOM_INJECTOR_TOKEN"],
"another-business",
[]
)
// ...
]);
}
);

So, lazy loading and child module injectors can get us closer to our goal. When we use the Angular Router all we have to do is describe routes, specify modules and components and it will create child injectors for any each lazily loaded module.

What about RouterModule?

RouterModule does not seem to fit our requirements because of:

  • we don’t need lazy modules, but only with lazy modules RouterModule can create child module injectors;
  • we want to solve the requests tracking problem, not a routing problem. With RouterModule any part of our application we want to log we must wrap in Route and use <router-outlet>
  • RouterModule is not good solution for huge hybrid (AngularJS + Angular) applications (1kk+ loc)

But we need something similiar, so let’s try to understand how RouterModule works and how it creates child injectors.

Moving deeper

Long story short, when you request some url defined in your Router configuration, the Angular Router will:

  1. Lazily load the module of the routte
  2. Compile the module (get NgFactory of the module)
  3. Create a module with a child injector
  4. Render the component in router-outlet.

Let’s have a closer look at step 3.

This article by Craig Spence helps a lot to figure out how a RouterModule works.

He explains how the Angular Router works in AOT builds and, more to the point, how Angular Compiler Plugin (ACP) understands what angular modules must be compiled as separate ng factories.

We also found Angular Compiler input option can be used to extend lazy routes collected by the Angular Compiler and AngularCompilerPlugin. This option is not described in the available documentation, but it is specified in AngularCompilerPluginOptions interface and it is public, so we can use it.

Armed with this knowledge, we now can try and build our own solution that will use a child module injector without resorting to an Angular router.

Here is our plan:

  1. Somehow tell Angular to create NgFactories.
  2. Use the systemjs loader to get the module NgFactory.
  3. Create a module from this factory.
  4. Render the component using ComponentFactoryResolver from the created module.

Max Koretskyi in his article explains how to get child injector modules without an Angular Router and using lazy loading.

But how to get child injector modules without lazy loading at all? We wrote a simple application to show how this could be done.

Firstly, we wrote a simple abstraction (something like a Router Module but which will only solve the creating child injector modules problem without lazy loading). We called it ChildInjectorModule. It is used like in example below:

// parent module
ChildInjectorModule.forModules([
WithCustomInjectorModule, AnotherWithCustomInjectorModule]
])
// WithCustomInjectorModule module imports
ChildInjectorModule.forChildModule([
WithCustomInjectorComponent
])
// AnotherModuleWithCustomInjectorModule module imports
ChildInjectorModule.forChildModule([
AnotherWithCustomInjectorComponent
])

It is very similar to a router module, isn’t it?

The module code is as follows:

@NgModule({
imports: [CommonModule],
declarations: [ChildInjectorComponent],
entryComponents: [ChildInjectorComponent],
exports: [ChildInjectorComponent]
})
export class ChildInjectorModule {
static forModules(modules: IChildInjectorModules): ModuleWithProviders {
return {
ngModule: ChildInjectorModule,
providers: [
{
provide: CHILD_INJECTOR_MODULES,
useValue: modules,
multi: true
},
{
provide: CHILD_INJECTOR_COMPILED_MODULES,
useFactory: childInjectorModulesFactory,
deps: [CHILD_INJECTOR_MODULES, Compiler, Injector]
}
]
};
}

static forChildModule<T>(components: Array<T>): ModuleWithProviders {
return {
ngModule: ChildInjectorModule,
providers: [{ provide: CHILD_INJECTOR_ENTRY_COMPONENTS, useValue: components }]
};
}
}

export function childInjectorModulesFactory(
modulesOfModules: Array<IChildInjectorModules> = [],
compiler: Compiler,
injector: Injector
): Array<IChildInjectorCompiledModules<Type<any>, Type<any>>> {
const modulesOfModulesResult = modulesOfModules.map(modules => {
const modulesMapResult = modules.map(ngModuleWebpackModule => {
if (ngModuleWebpackModule.compiled) {
return ngModuleWebpackModule.compiled;
}

const [name, factory]: INgModuleFactoryLoaderResult = NgFactoryResolver.resolve(ngModuleWebpackModule, compiler);
const module = factory.create(injector);
const components = module.injector.get(CHILD_INJECTOR_ENTRY_COMPONENTS);

ngModuleWebpackModule.compiled = { name, module, components };

return { name, module, components };
});

return modulesMapResult;
});
return modulesOfModulesResult;
}

The code again is similar to that of a Router Module but simpler.

How does it works? When we call the forModules static method in any module, a module instance with its providers is created. It specifies some providers for the module and declares theChildInjectorComponent component. ChildInjectorComponent is similar to <router-outlet> component. The component code must be familiar to you. Max mentioned something like this in one of his articles (see the link to his article above):

constructor(
@Inject(CHILD_INJECTOR_COMPILED_MODULES)
private compiledModules: IChildInjectorCompiledModules<any, T>,
private vc: ViewContainerRef
) {
}

ngOnInit() {
// ...
const compiledModule = (this.compiledModules || []).reduce(
(res, modules: any) => {
if (res) {
return res;
}
return modules.find((module: IChildInjectorCompiledModule<any, T>) =>
module.components.some(component => component === this.component)
);
},
null
);

if (!compiledModule) {
throw new Error(`[ChildInjectorComponent]: can not find compiled module for component ${(this.component as any).name}`);
}

const factory: ComponentFactory<T> = compiledModule.module.componentFactoryResolver.resolveComponentFactory(
this.component
);
this.componentRef = this.vc.createComponent(factory);
const { instance, location } = this.componentRef;

// ...
}
// ...

We use theCHILD_INJECTOR_COMPILED_MODULES token to get the compiled modules. When the component is created, childInjectorModulesFactory from our module will be called. It will call the magic resolve static method of NgFactoryResolver.

What is it? It is a function that returns NgModuleFactory. It runs differently in AOT and JIT modes.

static resolve(ngModuleWebpackModule: any, compiler: Compiler) {
const offlineMode = compiler instanceof Compiler;
return offlineMode
// in AOT we just resolve NgFactory
? NgFactoryResolver.resolveFactory(ngModuleWebpackModule)
// in JIT we have to compile NgFactory
: NgFactoryResolver.resolveAndCompileFactory(ngModuleWebpackModule, compiler);
}

This function checks if we have the compiler, and if so, we call theresolveFactory method, otherwise we call resolveAndCompileFactory. The same as in RouterModule.

Here is JIT implementation. We get a module and compile it to get NgFactory:

resolveAndCompileFactory(ngModuleWebpackModule, compiler) {
const moduleName = ngModuleWebpackModule.name;
return [moduleName, compiler.compileModuleSync(ngModuleWebpackModule)];
}

In JIT we have to get a module reference and compile it using a compiler. This will give us NgModuleFactory of module.

And here is anAOT implementation:

resolveFactory(ngModuleWebpackModule) {
const moduleName = Object.keys(ngModuleWebpackModule).find(key => key.endsWith('ModuleNgFactory'));
return [moduleName.replace('NgFactory', ''), ngModuleWebpackModule[moduleName]];
}

In AOT the code is different. We don’t need to compile a module (and actually we can’t even do it according to Craig’s article, since we don’t have Compiler at runtime - this is how Angular Compiler and AOT mode works). We only need to return the module factory from it. But how can it be possible if we used forModules with static imports to modules?

If you look into how the Angular Compiler Plugin works, you can see that it makes some transformations to typescript files. Alexey Zuev has article about how it works in Angular.

This means we can write a simple transformer that will change module imports to module NgFactories imports. Here is the code to our transformer.

If you use a custom webpack config, you just need to add that transformer to the ACP array of transformers. If you use @angular/cli, you don’t have a wepback config, but you can get one using a custom builder. Here is how a transformer can be added:

const AngularCompilerPluginInstance = initial.plugins.find(plugin => plugin instanceof AngularCompilerPlugin);  const defaultsTransformers = AngularCompilerPluginInstance._transformers;  AngularCompilerPluginInstance._transformers = [ngModulePathTransformer(), ...defaultsTransformers];

In our example we use the @angular-builders/custom-webpack builder to get our own webpack config. You can read more about builders there.

What happens in the transformer?

The transformer only processes *.module.ngfactory.ts files generated by the Angular Compiler. These files are compiled sources of NgModules with all their dependencies. So, the transformer finds provided CHILD_INJECTOR_MODULES token value. We pass it to the forModules method call. In our case, it is an array of modules:

[
ModuleA, ModuleB
]

In the factory, this will look as follows

import * as iN from './path/to/a/module';
import * as iM from './path/to/b/module';
...
i0.ɵmpd(256, i11.CHILD_INJECTOR_MODULES, [
[iN.ModuleA, iM.ModuleB],
...
], [])

In other words, in the transformer, we find the iN.ModuleA-like expressions and swap these expressions to iN. After the compilation and transformation we have an object with the ModuleANgFactory property. This is a compiled NgFactory of some module.

After the transformation, the factory will look as follows:

import * as iN from './path/to/a/module.ngfactory';
import * as iM from './path/to/b/module.ngfactory';
...
i0.ɵmpd(256, i11.CHILD_INJECTOR_MODULES, [
[iN, iM],
...
], [])

Why don’t we just change iN.Module1 to iN.Module1NgFactory? If we do so, the problem will appear at runtime in the relsolve method in AOT mode. The production build has optimizations and variables and function names will be mangled and we won't be able to get the module name from such names, that is why we need the entire object.

After these manipulations we have child injector modules without lazy loading and can inject custom tokens in such modules (example). These tokens are resolved in our http interceptor and they can be added to each request as a header.

How will the code of you application change?

Let’s talk about AppModule and AppComponent. Since you don’t eagerly import modules with features (you use ChildInjectorModule.forModules now), you can’t use declared components in your templates. We wrote this simple abstraction ChildInjectorComponent and we have to use it now in our AppComponent. Here is an example:

<app-child-injector module="WithCustomInjectorModule" [inputs]="inputs"></app-child-injector>
<app-child-injector module="AnotherModuleWithCustomInjectorModule"></app-child-injector>

Now, let’s test that everything works in JIT. A full example with ChildInjectorModule and transformations can be found here.

Clone the repo and go to examples/production-ready-child-modules-injector-example. Install the dependencies with thenpm package manager.

Execute ng serve command.

After webpack-dev-server starts, go to localhost:4200.

You will see:

Everything is working. Do you still remember why we digged so deep? :) We wanted requests in different parts of our application to be automatically marked. If you open the browser console, you will see the output of the http interceptor:

What about AOT? For us, it was the most important part.

Execute ng serve --aot=true in the console.

When the app starts and you will open it in browser, you will see exactly the same results. In the AOT mode everything is working perfectly too!

You can find more examples in our repo. In other examples, we used another approach to get resolver of NgFactories. We used the webpack aliases mechanism to detect what function (for JIT or for AOT) will be called at runtime. This approach could save you some bits in your final bundles.

We also put the code of ChildModuleInjector to the npm package. The code is available in this github repo. If you want to try child injector modules without RouterModule and without lazy-loading you can just install next packages and it will work in your project. Core part is in package @easy-two/ngx-child-injector. Install it with yarn or npm to use module and component. This will work in JIT mode. If you want AOT mode working install @easy-two/ngx-child-injector-transformer and add transformer like in the example.

This is how we solved our business problem and recreated child injector modules withtout lazy loading. Note that this approach is not only suitable for string tokens and http interceptors. You can provide anything you want.

--

--