This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.

As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article focuses on module design and modular organization in Angular.

Modules in Angular

Module design was introduced after the AngularJS upgrade to Angular (version 2+). When developing Angular applications, we always need modules, including the generic modules that Come with Angular and the root module that launches the application.

When it comes to modularity, front-end development starts with ES6 modules, and the two are not really related:

  • ES6 module takes the file as the unit; An Angular module is an NgModule.
  • The ES6 module is used for cross-file function calls; Angular modules are used to organize meaningful chunks of functionality.
  • ES6 module in the compilation stage to confirm the dependency of each module, module flat relations; Angular modules can have deep hierarchies.

NgModules definition

In Angular, NgModules are used for module organization and management.

NgModule is a class with an @NgModule decorator, whose argument is a metadata object that describes how to compile the component’s template and how to create an injector at run time. It identifies the module’s own components, directives, and pipes, exposing some of them through the Exports property so that external components can use them. For metadata and decorators, see the metadata and decorators section.

Ngmodules package components, directives, and pipes into cohesive chunks of functionality, each focused on a feature area, business domain, workflow, or common tool. At runtime, module-related information is stored in NgModuleDef:

// NgModuleDef is an internal data structure used at runtime to assemble components, directives, pipes, and injectors
export interface NgModuleDef<T> {
  // Represents the module's token, used by DI
  type: T;
  // List of components to boot
  bootstrap: Type<any> [] | (() = > Type<any> []);// A list of components, directives, and pipes declared by this module
  declarations: Type<any> [] | (() = > Type<any> []);// The list of modules or ModuleWithProviders imported by this module
  imports: Type<any> [] | (() = > Type<any> []);// The list of modules, ModuleWithProviders, components, directives, or pipes that this module exports
  exports: Type<any> [] | (() = > Type<any> []);// Cache value of transitiveCompileScopes calculated for the module
  transitiveCompileScopes: NgModuleTransitiveScopes|null;
  // Declare a set of modes for the elements allowed in an NgModule
  schemas: SchemaMetadata[]|null;
  // It should be the unique ID of its registration module
  id: string|null;
}
Copy the code

At a macro level, NgModules are a way to organize Angular applications, and they do so through metadata in the @NgModule decorator, which can be divided into three categories:

  • Static: compiler configuration, passeddeclarationsArray to configure. A selector that tells the compiler where to apply the instruction in the template by selector matching
  • Run time: PassprovidersArray provides configuration to the injector
  • Combination/grouping: PassimportsandexportsArray to put multiple NgModules together and make them available

As you can see, an NgModules module makes a cohesive block of functionality by declaring its components, directives, and pipes and importing other modules and services through imports. Ngmodules can also add service providers to an application’s dependency injector, as described in the dependency injection section below.

Modular organization

Every Angular app has at least one module, called the root AppModule. Angular applications start from the root module, as described in dependency injection bootstrap.

For a simple Angular application, a single root module is sufficient to manage the functionality of the entire application. For complex applications, it can be divided into modules based on functionality, with each module focusing on a particular function or business domain, workflow or navigation flow, common toolset, or being one or more service providers.

In Angular, the recommended modules can be classified by type:

  • Domain modules: Domain modules are organized around features, business domains, or user experience
  • Routed module: The top-level component of the module acts as the destination for the router to access this part of the route
  • Routing configuration module: A routing configuration module provides routing configuration for another module
  • Service modules: Service modules provide utility services, such as data access and messaging
  • Widgets: A widget module can provide certain components, instructions, or pipes to other modules
  • Shared modules: Shared modules can provide a collection of components, directives, and pipes to other modules

As you can see, modules can be organized in different ways, including components, directives, pipes, and services, or just one of them. For example, HttpClientModule is a module organized only by the provider:

@NgModule({
  // Optional configuration of XSRF protection
  imports: [
    HttpClientXsrfModule.withOptions({
      cookieName: 'XSRF-TOKEN'.headerName: 'X-XSRF-TOKEN',})],// Configure DI and import it along with support services for HTTP communication
  providers: [
    HttpClient,
    {provide: HttpHandler, useClass: HttpInterceptingHandler},
    HttpXhrBackend,
    {provide: HttpBackend, useExisting: HttpXhrBackend},
    BrowserXhr,
    {provide: XhrFactory, useExisting: BrowserXhr},
  ],
})
export class HttpClientModule {}Copy the code

Module ability

Now that we know that an NgModule is a cohesive bundle of components, directives, and pipes, how do you manage them inside an NgModule?

Modules and Components

In Angular, each component should (and can only) declare in an NgModule class. Components belonging to the same NgModule share the same compile context, which is maintained by LocalModuleScopeRegistry:

export class LocalModuleScopeRegistry implements MetadataRegistry.ComponentScopeReader {...// Component mapping from the currently compiled units to the NgModule that declares them
  private declarationToModule = new Map<ClassDeclaration, DeclarationData>();
  // This maps from the directive/pipe class to the data mapping for each NgModule that declares the directive/pipe
  private duplicateDeclarations =
      new Map<ClassDeclaration, Map<ClassDeclaration, DeclarationData>>();
  private moduleToRef = new Map<ClassDeclaration, Reference<ClassDeclaration>>();
  // The LocalModuleScope cache computed for each NgModule declared in the current program
  private cache = new Map<ClassDeclaration, LocalModuleScope|null> ();// Add NgModule data to the registry
  registerNgModuleMetadata(data: NgModuleMeta): void {}
  // Get the scope for the component
  getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null {
    const scope = !this.declarationToModule.has(clazz) ?
        null :
        // Returns the scope of the NgModule
        this.getScopeOfModule(this.declarationToModule.get(clazz)! .ngModule);return scope;
  }
  // Collect registration data for the module and its directives/pipes and convert it into a full LocalModuleScope
  getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope|null {
    return this.moduleToRef.has(clazz) ?
        this.getScopeOfModuleReference(this.moduleToRef.get(clazz)!) :
        null; }}Copy the code

The LocalModuleScopeRegistry class implements NgModule declaration, import, and export logic, and can generate a set of directives and pipes for a given component that are “visible” in that component’s template. It collects information about local NgModules, directives, components, and pipes, and can generate a LocalModuleScope, which Outlines the scope of a component’s compilation.

When each NgModule compiles the @NgModule decorator metadata, it registers the module’s information with LocalModuleScopeRegistry:

export class NgModuleDecoratorHandler implements
    DecoratorHandler<Decorator.NgModuleAnalysis.NgModuleResolution> {
  register(node: ClassDeclaration, analysis: NgModuleAnalysis): void {
    // This ensures that during the compile() phase, the module's metadata is available for selector scoping calculations
    this.metaRegistry.registerNgModuleMetadata({
      ref: new Reference(node),
      schemas: analysis.schemas,
      declarations: analysis.declarations,
      imports: analysis.imports,
      exports: analysis.exports,
      rawDeclarations: analysis.rawDeclarations, }); . }Copy the code

When a Component compiles metadata for the @Component decorator, it checks to see if the Component is registered with the NgModule. If you are registered in a module, get the compilation scope of the module from LocalModuleScopeRegistry and compile it within the compilation scope of the module:

export class ComponentDecoratorHandler implements
    DecoratorHandler<Decorator.ComponentAnalysisData.ComponentResolutionData> {
  resolve(node: ClassDeclaration, analysis: Readonly<ComponentAnalysisData>):
      ResolveResult<ComponentResolutionData> {
    ...
    // Get the scope of the module
    const scope = this.scopeReader.getScopeForComponent(node); .if(scope ! = =null&& (! scope.compilation.isPoisoned ||this.usePoisonedData)) {
      // Process information in the scope of the module
      for (const dir of scope.compilation.directives) {
        if(dir.selector ! = =null) {
          matcher.addSelectables(CssSelector.parse(dir.selector), dir asMatchedDirective); }}const pipes = new Map<string, Reference<ClassDeclaration>>();
      for (const pipe ofscope.compilation.pipes) { pipes.set(pipe.name, pipe.ref); }... }}Copy the code

After obtaining the scope, the component then uses the R3TargetBinder binding component template AST, which is covered more in the Ivy compiler section.

By default, ngModules are acutely loaded, meaning that they are loaded as soon as the application loads, and this is true for all modules, whether or not they need to be used immediately. For large applications with many routes, consider using lazy loading: a mode of loading ngModules on demand. Lazy loading reduces the size of the initial package and thus the load time.

To lazily load Angular modules, you use AppRoutingModule, and lazy loading also supports preloading capabilities.

conclusion

Modules are the best way to organize Angular. Modules provide a set of capabilities that focus on the needs of a particular application, dividing the application into focused functional areas, such as user workflow, routing, or forms.

For NgModule modules, you can collaborate with the root module and other NgModule modules through services provided by the module and shared components, directives, and pipes. Angular resolves dependencies between modules by setting the import and export of modules. Circular dependencies between Angular modules are not allowed, so modules in an Angular application are ultimately rendered as a tree with the root module as the root node.

reference

  • Angular-NgModules