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, passed
declarations
Array to configure. A selector that tells the compiler where to apply the instruction in the template by selector matching - Run time: Pass
providers
Array provides configuration to the injector - Combination/grouping: Pass
imports
andexports
Array 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