Common confusions with modules in Angular

Angular Modules is such a complex topic that the Angular development team has written several tutorials on ngModules on their website. These tutorials clearly cover most of Modules, but there are still some things missing, leading many developers to be misled. I see a lot of developers who don’t know how Modules works internally, so they often get the concepts wrong and use the Modules API in the wrong posture.

This article will explain the inner workings of Modules in depth and try to help you clear up some of the common misconceptions I see people asking on StackOverflow.

Module encapsulation

Angular introduces the concept of module encapsulation, which is similar to the ES module concept. The concept of ES Modules (see Modules in TypeScript’s Chinese language) basically means that all declared types, including components, directives, and pipes, can only be used within the current module by other declared components. For example, if I use the A-comp component of module A in the App component:

@Component({
  selector: 'my-app',
  template: ` 

Hello {{name}}

`
}) export class AppComponent { } Copy the code

The Angular compiler throws an error:

Template parse errors: ‘a-comp’ is not a known element

This is because the A-comp component is not declared in the App module. If I want to use this component, I have to import the A module, like this:

@NgModule({
  imports: [..., AModule]
})
export class AppModule { }
Copy the code

Module encapsulation is described above. Furthermore, if you want the A-comp component to work properly, you have to make it publicly accessible by exporting it from the EXPORTS property of the A module:

@NgModule({... declarations: [AComponent], exports: [AComponent] })export class AModule { }
Copy the code

The same applies to directives and pipes:

@NgModule({... declarations: [ PublicPipe, PrivatePipe, PublicDirective, PrivateDirective ], exports: [PublicPipe, PublicDirective] })export class AModule {}
Copy the code

Note that module encapsulation does not apply to components registered in the entryComponents property. If you use dynamic views to instantiate dynamic components in the way described in this article about Angular dynamic Components, There is no need to export the A-COMp component in the EXPORTS property of the A module. Of course, you have to import module A.

Most beginners think providers have encapsulation rules, but they don’t. Any provider declared in a non-lazy-loaded module can be accessed anywhere within the program, as explained in more detail below.

Module level

One of the biggest misconceptions among beginners is that a module imports other modules into a module hierarchy, and that this module becomes the parent of those imported modules, thus forming a module tree hierarchy, which is reasonable, of course. But in reality, there is no such module hierarchy. Because all modules are merged at compile time, there is no hierarchy between imported and imported modules.

Just like components, the Angular compiler generates a module factory for the root module, which you pass to the bootstrapModule() method in main.ts:

platformBrowserDynamic().bootstrapModule(AppModule);
Copy the code

The Angular compiler creates the module factory using the createNgModuleFactory method, which takes several parameters. The latest version does not include the third dependency parameter. :

  • module class reference
  • bootstrap components
  • component factory resolver with entry components
  • definition factory with merged module providers

The last two points explain why providers and Entry Components don’t have module encapsulation rules, because instead of having multiple modules after compilation, there is only one merged module. And at compile time, the compiler doesn’t know how you’re going to use providers and dynamic components, so the compiler controls encapsulation. However, during component template parsing at compile time, the compiler knows how you use components, directives, and pipes, so the compiler can control their private declarations. (note: Providers and entry Components are dynamic content. The Angular compiler does not know how they are used, but components, directives, and pipes written in templates are static content. The Angular compiler knows how it is used at compile time. This is important to understand the inner workings of Angular.

Let’s look at an example of generating A module factory. Suppose you have modules A and B, and each module defines A provider and an entry Component:

@NgModule({
  providers: [{provide: 'a', useValue: 'a'}],
  declarations: [AComponent],
  entryComponents: [AComponent]
})
export class AModule {}

@NgModule({
  providers: [{provide: 'b', useValue: 'b'}],
  declarations: [BComponent],
  entryComponents: [BComponent]
})
export class BModule {}
Copy the code

The root module App also defines A provider and A root component App, and imports modules A and B:

@NgModule({
  imports: [AModule, BModule],
  declarations: [AppComponent],
  providers: [{provide: 'root', useValue: 'root'}],
  bootstrap: [AppComponent]
})
export class AppModule {}
Copy the code

When the compiler compiles the App root module to generate a module factory, the compiler merges all module providers and creates a module factory only for the merged module. The following code shows how a module factory is generated:

createNgModuleFactory(
    // reference to the AppModule class
    AppModule,

    // reference to the AppComponent that is used
    // to bootstrap the application
    [AppComponent],

    // module definition with merged providers
    moduleDef([
        ...

        // reference to component factory resolver
        // with the merged entry components
        moduleProvideDef(512, jit_ComponentFactoryResolver_5, ... , [ ComponentFactory_<BComponent>, ComponentFactory_<AComponent>, ComponentFactory_<AppComponent> ])// references to the merged module classes 
        // and their providers
        moduleProvideDef(512, AModule, AModule, []),
        moduleProvideDef(512, BModule, BModule, []),
        moduleProvideDef(512, AppModule, AppModule, []),
        moduleProvideDef(256.'a'.'a', []),
        moduleProvideDef(256.'b'.'b', []),
        moduleProvideDef(256.'root'.'root'], []));Copy the code

As we know from the code above, all module providers and entry components are merged and passed to the moduleDef() method, so no matter how many modules are imported, the compiler only merges modules and generates a module factory. The module factory uses the module injector to generate the merged module object (see L232), but since there is only one merged module, Angular will only use the providers to generate a singleton root injector.

Now you might be wondering what happens if the same Provider token is defined in two modules.

The first rule is that providers defined in modules importing other modules always win. For example, define a provider in the AppModule as well:

@NgModule({... providers: [{provide:'a', useValue: 'root'}],})export class AppModule {}
Copy the code

View the generated module factory code:

moduleDef([
     ...
     moduleProvideDef(256.'a'.'root', []),
     moduleProvideDef(256.'b'.'b'[]),]);Copy the code

As you can see, the last merged module factory contains moduleProvideDef(256, ‘a’, ‘root’, []), which overrides {provide: ‘A ‘, useValue: ‘a’} defined in AModule.

The second rule is that the last imported module’s providers overwrite the previous imported module’s providers. Also, define a provider in the BModule:

@NgModule({... providers: [{provide:'a', useValue: 'b'}],})export class BModule {}
Copy the code

Then import the AModule and BModule in AppModule in the following order:

@NgModule({
  imports: [AModule, BModule],
  ...
})
export class AppModule {}
Copy the code

View the generated module factory code:

moduleDef([
     ...
     moduleProvideDef(256.'a'.'b', []),
     moduleProvideDef(256.'root'.'root'[]),]);Copy the code

So the code above already verifies the second rule. We define {provide: ‘a’, useValue: ‘b’} in BModule, now let’s switch module import order:

@NgModule({
  imports: [BModule, AModule],
  ...
})
export class AppModule {}
Copy the code

View the generated module factory code:

moduleDef([
     ...
     moduleProvideDef(256.'a'.'a', []),
     moduleProvideDef(256.'root'.'root'[]),]);Copy the code

As expected, because of the switch in module import order, AModule’s {provide: ‘A ‘, useValue: ‘a’} now overrides BModule’s {provide: ‘a’, useValue: ‘b’}.

Note: The author provided the AppModule code compiled by @Angular/Compiler above, and analyzed the compiled code for multiple modules’ providers to be merged. Json, where./node_modules/. Bin/NGC is the CLI command provided by @angular/compiler-cli. We can create a new project using ng New Module. My version is 6.0.5. Json, copy the tsconfig.json from the root of the project, and add a module.ts file. Module. ts contains the root AppModule, and two modules, AModule and BModule, AModule provides AService, {provide:’ A ‘, value:’a’} and {provide:’b’, value:’b’} services, while BModule provides BService and {provide: ‘b’, useValue: ‘c’}. The AModule and BModule import the root AppModule in sequence. The complete code is as follows:

import {Component, Inject, Input, NgModule} from '@angular/core';
import "./goog"; // goog.d.ts source file copy to/TMP folder import"hammerjs";
import {platformBrowserDynamic} from '@angular/platform-browser-dynamic';
export class AService {
}
@NgModule({
  providers: [
    AService,
    {provide: 'a', useValue: 'a'},
    {provide: 'b', useValue: 'b'},]})export class AModule {
}
export class BService {
}
@NgModule({
  providers: [
    BService,
    {provide: 'b', useValue: 'c'}]})export class BModule {
}
@Component({
  selector: 'app', template: ` <p>{{name}}</p> <! --<a-comp></a-comp>--> ` })export class AppComp {
  name = 'lx1036';
}

export class AppService {
}

@NgModule({
  imports: [AModule, BModule],
  declarations: [AppComp],
  providers: [
    AppService,
    {provide: 'a', useValue: 'b'}
  ],
  bootstrap: [AppComp]
})
export class AppModule {
}

platformBrowserDynamic().bootstrapModule(AppModule).then(ngModuleRef => console.log(ngModuleRef));
Copy the code

/ TMP /tsconfig.json Compiling the module.ts file with @angular/compiler generates multiple files, including module.js and module.factory.js. Take a look at module.js. The AppModule class will compile to the following code and find that the metadata we wrote in the @NgModule class decorator will be assigned to the AppModule.decorators property or, if it is a property decorator, to the propDecorators property:

var AppModule = /** @class */ (function () {
    function AppModule() {
    }
    AppModule.decorators = [
        { type: core_1.NgModule, args: [{
                    imports: [AModule, BModule],
                    declarations: [AppComp],
                    providers: [
                        AppService,
                        { provide: 'a', useValue: 'b' }
                    ],
                    bootstrap: [AppComp]
                },] },
    ];
    returnAppModule; } ()); exports.AppModule = AppModule;Copy the code

Then take a look at the module.factory.js file, which is important for this article about module providers merging. The file AppModuleNgFactory object contains the combined will, these will come from AppModule, AModule, BModule, And providers in AppModule override the providers of other modules. Providers in BModule override the providers of AModule because BModule imports them after AModule. You can switch the import order and see what happens. ɵ CMF = createNgModuleFactory, ɵmod = moduleDef, ɵ MPD = moduleProvideDef, moduleProvideDef ɵ MPD (256, “a”, “a”, []) specifies that TypeValueProvider is a value type.

Object.defineProperty(exports, "__esModule", { value: true });
var i0 = require("@angular/core");
var i1 = require("./module"); Typicaltypicalcmf (i1.amodule, [],function (_l) {
    returnI0. ɵ mod ([i0 ɵ MPD (512, i0.Com ponentFactoryResolver, i0. ɵ CodegenComponentFactoryResolver, [[8, []], [3, I0.Com ponentFactoryResolver], i0 NgModuleRef]), i0. ɵ MPD (4608, i1. AService, i1. AService, []), i0. ɵ MPD (1073742336, I1. AModule, i1 AModule, []), i0. ɵ MPD (256,"a"."a", []), i0 ɵ MPD (256,"b"."b"], [])); }); exports.AModuleNgFactory = AModuleNgFactory; [], [], [], [],function (_l) {
    returnI0. ɵ mod ([i0 ɵ MPD (512, i0.Com ponentFactoryResolver, i0. ɵ CodegenComponentFactoryResolver, [[8, []], [3, I0.Com ponentFactoryResolver], i0 NgModuleRef]), i0. ɵ MPD (4608, i1. BService, i1. BService, []), i0. ɵ MPD (1073742336, I1. BModule, i1 BModule, []), i0. ɵ MPD (256,"b"."c"], [])); }); exports.BModuleNgFactory = BModuleNgFactory; ɵ CMF (i1.appModule, [i1.appComp], // bootstrapComponnets of AppModulefunction (_l) {
    returnI0. ɵ mod ([i0 ɵ MPD (512, i0.Com ponentFactoryResolver, i0. ɵ CodegenComponentFactoryResolver, [[8, [AppCompNgFactory]], [3, I0.Com ponentFactoryResolver], i0 NgModuleRef]), i0. ɵ MPD (4608, i1. AService, i1. AService, []), i0. ɵ MPD (4608, i1. BService, ɵ MPD (1073742336, i1.amodule, i1.amodule, []), [], i0.ɵ MPD (1073742336, i1.amodule, i1.amodule, []), ɵ MPD (1073742336, usemodule, usemodule, []), usemodule (usemodule, usemodule, usemodule), usemodule (usemodule, usemodule, usemodule, usemodule), usemodule (usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule, usemodule)"a"."b", []), i0 ɵ MPD (256,"b"."c"], [])); }); exports.AppModuleNgFactory = AppModuleNgFactory;Copy the code

In practice, compiling it yourself will be much more efficient than just reading the article’s explanation.

Lazy loading module

Now there is another area of confusion – lazy loading modules. Here’s what the official document says:

Angular creates a lazy-loaded module with its own Injector, a child of the root Injector… So a lazy-loaded module that imports that shared module makes its own copy of the service.

So we know that Angular creates its own injector for lazy-loaded modules because the Angular compiler generates a separate component factory for each lazy-loaded module compilation. Providers defined in the lazy module are not merged into the main module’s injector, so if the lazy module defines the same provider as the main module, the Angular compiler creates a new service object for that provider.

So lazily loading modules also creates a hierarchy, but the injector hierarchy, not the module hierarchy. In lazy-loaded modules, all imported modules are also merged into one at compile time, just as in non-lazy-loaded modules above.

The logic is in the @Angular/Router package’s RouterConfigLoader code, which shows how to load modules and create injectors:

export class RouterConfigLoader {

  load(parentInjector, route) {
    ...
    const moduleFactory$ = this.loadModuleFactory(route.loadChildren);
    return moduleFactory$.pipe(map((factory: NgModuleFactory<any>) = >{...const module= factory.create(parentInjector); . })); } private loadModuleFactory(loadChildren) { ...return this.loader.load(loadChildren)
  }
}
Copy the code

Look at this line of code:

const module = factory.create(parentInjector);
Copy the code

Pass the parent injector to create a lazy-loaded module new object.

ForRoot and forChild static methods

Check out the official website for details:

Add a CoreModule. ForRoot method that configures the core UserService… Call forRoot only in the root application module, AppModule

This advice is reasonable, but if you don’t understand why you’re doing it, you’ll end up writing something like this:

@NgModule({
  imports: [
    SomeLibCarouselModule.forRoot(),
    SomeLibCheckboxModule.forRoot(),
    SomeLibCloseModule.forRoot(),
    SomeLibCollapseModule.forRoot(),
    SomeLibDatetimeModule.forRoot(),
    ...
  ]
})
export classSomeLibRootModule {... }Copy the code

Every imported module (CarouselModule, CheckboxModule, etc.) no longer defines any providers, but I see no reason to use forRoot here. Let’s see why we need forRoot in the first place.

When you import a module, you usually use a reference to that module:

@NgModule({ providers: [AService] })
export class A {}

@NgModule({ imports: [A] })
export class B {}
Copy the code

In this case, all module providers defined in module A are merged into the main injector and made available in the overall program context. I think you already know why — as explained above, all module providers are merged to create the injector.

Angular also supports another way to import modules with providers. Instead of importing them using a reference to the module, Angular passes an object that implements the ModuleWithProviders interface:

interface ModuleWithProviders { 
   ngModule: Type<any> providers? : Provider[] }Copy the code

We can rewrite it like this:

@NgModule({})
class A {}

const moduleWithProviders = {
    ngModule: A,
    providers: [AService]
};

@NgModule({
    imports: [moduleWithProviders]
})
export class B {}
Copy the code

It is better to use a static method inside a module object to return ModuleWithProviders instead of directly using an object of type ModuleWithProviders and using the forRoot method to refactor the code:

@NgModule({})
class A {
  static forRoot(): ModuleWithProviders {
    return{ngModule: A, providers: [AService]}; }}@NgModule({
  imports: [A.forRoot()]
})
export class B {}
Copy the code

Of course, there is no need to define the forRoot method to return a ModuleWithProviders object for this simple example, because providers can be defined directly in two modules or, as described above, by using a ModuleWithProviders object. This is just for demonstration purposes. However, if we want to split the providers and define them separately in the imported module, the above approach makes a lot of sense.

For example, if we want to define A global A service for non-lazily loaded modules and A B service for lazily loaded modules, we need to use the above approach. We use the forRoot method to return providers for non-lazy-loaded modules and the forChild method to return providers for lazy-loaded modules.

@NgModule({})
class A {
  static forRoot() {
    return {ngModule: A, providers: [AService]};
  }
  static forChild() {
    return{ngModule: A, providers: [BService]}; }}@NgModule({
  imports: [A.forRoot()]
})
export class NonLazyLoadedModule {}

@NgModule({
  imports: [A.forChild()]
})
export class LazyLoadedModule {}
Copy the code

Since non-lazy-loaded modules are merged, providers defined in forRoot are available globally. However, since lazy modules have their own injector, the providers you define in forChild are only available in the current lazy module.

Please note that the names of methods that you use to return ModuleWithProviders structure can be completely arbitrary. The names forChild and forRoot I used in the examples above are just conventional names recommended by Angular team and 2 in the RouterModuleimplementation. (note: namely forRoot and forChild method name can casually change.)

Okay, back to the original code:

@NgModule({
  imports: [
    SomeLibCarouselModule.forRoot(),
    SomeLibCheckboxModule.forRoot(),
    ...
Copy the code

There is no need to define a forRoot method for each module. Providers defined in multiple modules need to be available globally, and there is no provision of separate providers for lazy-loaded modules. There is no requirement to cut providers, but you use forRoot to force it. Even more confusing is the code written if an imported module does not define any providers.

Use forRoot/forChild convention only for shared modules with providers that are going to be imported into both eager and lazy module modules

It is also important to note that forRoot and forChild are just methods, so they can be passed as arguments. For example, the RouterModule in the @Angular/Router package defines the forRoot method and passes in additional arguments:

export class RouterModule {
  staticforRoot(routes: Routes, config? : ExtraOptions)Copy the code

The routes argument passed is used to register the Routes token:

staticforRoot(routes: Routes, config? : ExtraOptions) {return {
    ngModule: RouterModule,
    providers: [
      {provide: ROUTES, multi: true, useValue: routes}
Copy the code

The second optional parameter config is passed as a configuration option:

staticforRoot(routes: Routes, config? : ExtraOptions) {return {
    ngModule: RouterModule,
    providers: [
      {
        provide: PreloadingStrategy,
        useExisting: config.preloadingStrategy ?
          config.preloadingStrategy :
          NoPreloading
      }
Copy the code

As you can see, The RouterModule uses forRoot and forChild methods to separate providers and pass in parameters to configure the respective providers.

Module cache

There was a developer on Stackoverflow for a while who was concerned that if you import the same module from a non-lazy-loaded module as from a lazy-loaded module, the module code will be duplicated at runtime. This is an understandable concern, but not necessary, because all module loaders cache all loaded module objects.

When SystemJS loads a module, it caches the module. The next time a lazy module imports the module, the SystemJS module loader pulls the module from the cache, rather than performing a network request. This process applies to all modules. Angular has a built-in SystemJsNgModuleLoader module loader. For example, when you’re writing an Angular Component, import the Component decorator from the @angular/core package:

import { Component } from '@angular/core';
Copy the code

You reference the package multiple times in your application, but SystemJS doesn’t load the package every time, it only loads it once and caches it.

The same is true if you use Angular-CLI or configure Webpack yourself. It only loads once, caches it, and assigns it an ID that other modules use to find the module and access the various services it provides.