2
头图

作者图.png

1 Injection, a component tree-level communication pattern & design pattern

1.1 Component Communication Mode

In Angular project development, we usually use Input property binding and Output event binding for component communication, but Input and Output can only pass information between parent and child components. Components form a component tree according to the calling relationship. If there are only property binding and event binding, then two non-direct relationship components need to communicate through each connection point itself, and the middleman needs to continuously process and pass some things that it does not need to know. information (Figure 1 left). The Injectable Service provided in Angular can be provided in modules, components or directives, etc., with injection in the constructor, which can solve this problem (right in Figure 1).

图1.png

Figure 1 Component Communication Mode

The left image only transmits information through the parent and child components, and the communication between node a and node b needs to go through many nodes; if node c wants to control node b through some configuration, the nodes between them must also set additional properties or events to transparently transmit the corresponding Information. The dependency injection mode node c in the right figure can provide a service for nodes a and b to communicate. Node a directly communicates with node c, and node b also communicates directly with the service provided by node c. Finally, the communication is simplified. The node is also not coupled with this part of the content, and has no obvious perception of the communication between the upper and lower components.

1.2 Inversion of Control Using Dependency Injection

Dependency injection (DI) is not unique to Angular, it is a means of implementing the Inversion of Control (IOC) design pattern. The emergence of dependency injection solves the problem of excessive coupling of manual instantiation. All resources are not managed by both parties using resources, but by There are many benefits to using the Resource Center or third-party providers. First, centralized management of resources to achieve configurable and easy management of resources. Second, it reduces the degree of dependence on both parties using resources, which is what we call coupling.

The analogy to the real world is that when we go to buy a product such as a pencil, we only need to find a store to buy a product of the type pencil, we don’t care where the pencil is made, how the wood and the pencil lead are bonded, We only need it to complete the writing function of the pencil, and we will not have any connection with the specific pencil manufacturer or factory. As for the store, it can go to the appropriate channel to purchase pencils by itself to realize the configurability of resources.

Combined with coding scenarios, more specifically, users can inject and use instances without explicitly creating instances (new operation), and the creation of instances is determined by providers. The management of resources is through tokens. Since they do not care about the provider and the creation of instances, the user can use some local injection means (secondary configuration of the token), and finally realize the replacement of the instance, the dependency injection mode applications and Aspect Programming (AOP) complement each other.

2 Dependency Injection in Angular

Dependency injection is one of the most important core modules of the Angular framework. Angular not only provides service type injection, but its own component tree is an injection dependency tree, and functions and values can also be injected. That is to say, in the Angular framework, the child component can inject the parent component instance through the parent component's token (usually the class name). In component library development, there are a large number of cases in which interaction and communication are achieved by injecting parent components, including parameter mounting, state sharing, and even obtaining the DOM of the node where the parent component is located.

2.1 Resolving dependencies

To use Angular's injection, you must first understand its injection and parsing process. Similar to the parsing process of node_modules, when no dependencies are found, they will bubble up to the parent layer to find dependencies. The old version (before v6 ) of Angular will divide the process of injection resolution into multi-level module injector, multi-level component injector and element injector. The new version (after v9 ) is simplified to a two-level model. The first query chain is the element injector and component injector at the static DOM level, which are collectively referred to as element injector, and the other query chain is the module injector. The order of parsing and the default value after parsing failure are clearly explained in the official code comment document ( provider_flag ).

图2.png

Figure 2 Two-level injector lookup dependency process ( image source )

That is to say, the component/directive and the injection content provided at the component/directive level will first look for dependencies in the element in the component view until the root element, if not found, then refer to the module where the element is currently located, reference (including module reference and routing lazy loading) References) The module's parent module goes up to the root module and the platform module at a time.

Note that the injector here is inherited, the element injector can create and inherit the lookup function of the parent element's injector, and the module injector is similar. After continuous inheritance, it is a bit like the prototype chain of js objects.

2.2 Configuring the provider

Knowing the order priority of dependency resolution, we can serve content at the appropriate level. We already know that it has two types: module injection and element injection.

  • Module injector: Providers can be configured in the metadata properties of @NgModule, and the @Injectable declaration provided after v6 can be used to declare provideIn as module name, 'root', etc. (There are actually two more injectors on top of the root module, Platform and Null, which are not discussed here.)
  • Element injector: Providers, viewProviders can be configured in the component's @Component metadata property, or providers in the directive's @Directive metadata.

In addition, the @Injectable decorator can actually be declared as an element injector in addition to declaring a module injector. It is more often declared to be provided at root, to implement a singleton. It integrates metadata by the class itself to avoid the module or component directly declaring the provider explicitly, so that if the class does not have any component instruction service and other classes injected into it, there is no code linked to the type declaration, which can be ignored by the compiler, thus achieving Shake the tree.

Another way to provide it is to give the value directly when declaring the InjectionToken.

Here are the shorthand templates for these methods:

 @NgModule({
  providers: [
    // 模块注入器
  ]
})
export class MyModule {}
 @Component({
  providers: [
    // 元素注入器 - 组件
  ],
  viewProviders: [
    // 元素注入器- 组件视图
  ]
})
export class MyComponent {}
 @Directive({
  providers: [
   // 元素注入器 - 指令
 ]
})
export class MyDirective {}
 @Injectable({
 providedIn: 'root'
})
export class MyService {}
 export const MY_INJECT_TOKEN = new InjectionToken<MyClass>('my-inject-token', {
 providedIn: 'root',
 factory: () => {
   return new MyClass();
 }
});

Different choices of where to provide dependencies will introduce some differences, which ultimately affect the size of the package, the scope of the dependencies that can be injected, and the life cycle of the dependencies. For different scenarios, such as singleton (root), service isolation (module), multiple editing windows (component), etc., there are different applicable solutions, and a reasonable location should be selected to avoid improper sharing of information or redundant code packaging .

2.3 Various value function tools

If it only provides instance injection, it does not show the flexibility of Angular framework dependency injection. Angular provides many flexible injection tools, useClass automatically creates new instances, useValue uses static values, useExisting can reuse existing instances, useFactory is constructed by functions, and specified deps are used to specify constructor parameters. These combinations can be very tricky. . You can cut off the token token of a class and replace it with another instance prepared by yourself. You can create a token to save the value or instance first, and then replace it again when you need it later, or even return it with a factory function. The local information of the instance is mapped to another object or property value. The gameplay here will be explained through the following cases, which will not be expanded here. There are also many examples on the official website.

2.4 Injecting consumers and decorators

Injection in Angular can be injected in the constructor constructor, or you can get the injector injector to obtain the existing injected elements through the get method.

Angular supports adding decorators to mark when injecting,

  • @Host() to limit bubbling
  • @Self() is restricted to the element itself
  • @SkipSelf() is limited to above the element itself
  • @Optional() is marked as optional
  • @Inject() is restricted to custom Tokens

Here is an article " @Self or @Optional @Host? The visual guide to Angular DI decorators. " It is very vivid to show the difference in the final hit instances if different decorators are used between parent and child components.

图3.png

Figure 3 Screening results of different injected decorators

2.4.1 Supplement: Host View and @Host

Among these decorators, the most difficult to understand may be @Host. Here are some specific instructions for @Host.
The official explanation for the @Host decorator is

...retrieve a dependency from any injector until reaching the host element

Host here means host, and the @Host decorator will limit the scope of the query to the host element. What is a host element? If the B component is the component used by the A component template, then the A component instance is the host element of the B component instance. The content generated by the component template is called View (view), and the same View may be different views for different components. If component A uses component B within its own template scope (see Figure 4), the view formed by the template content of A (the red box) is the inline view of component A for component A, and component B is in this view, so For B, this view is B's host view. The decorator @Host is to limit the search scope to the host view, and it will not bubble up if it is not found.

图4.png

Figure 4 Inline view and host view

3 Cases and how to play

Let's take a look at how dependency injection works, how to troubleshoot errors, and how to play through real cases.

3.1 Case 1: The modal window creates a dynamic component, but the component cannot be found

The modal window component of the DevUI component library provides a service ModalService, which can pop up a modal box and can be configured as a custom component. Business students often report errors when using this component, and the package cannot find the custom component.

For example the following error:

图5.png

Figure 5 Error when creating a component that references EditorX when using ModalService, the corresponding service provider cannot be found

Analyze how ModalService creates custom components, line 52 and 95 of the ModalService source code Open function . As you can see, componentFactoryResolver If there is no incoming, use ModalService to inject componentFactoryResolver . In most cases, the business will introduce DevUIModule once in the root module, but will not introduce ModalModule in the current module. That is, the status quo in Figure 6 is like this. According to Figure 6, there is no EditorXModuleService in the injector of ModalService.

图6.png

Figure 6 Module service provisioning diagram

According to the inheritance of the injector, there are four solutions:

  1. Put EditorXModule where ModalModule is declared, so that the injector can find the EditorModuleService provided by EditorXModule - this is the worst solution. The lazy loading implemented by loadChildren itself is to reduce the loading of the home page module. The content used is placed in the AppModule, and the large module with rich text is loaded for the first time, which increases the FMP (First Meaningful Paint) and cannot be used.
  2. Introduce ModalService in modules that import EditorXModule and use ModalService - desirable. There is only one situation that is not desirable, that is, the ModalService is called by another public service on the top level, so the unnecessary modules are still placed in the upper layer to be loaded.
  3. When triggering a component that uses ModalService, inject the current module componentFactoryResolver and pass it to the open function parameter of ModalService - it is desirable, and EditorXModule can be introduced where it is actually used.
  4. In the module used, manually provide a ModalService - advisable, solves the problem of injection search.

The four methods are actually to solve the problem of EditorXModuleService on the injector internal chain used by componentFactoryResolver . Guaranteed to be on a two-layer search chain, this problem can be solved.

Summary of Knowledge Points : Module Injector Inheritance and Lookup Scope.

3.2 Case 2: CdkVirtualScrollFor cannot find CdkVirtualScrollViewport

Usually, when we use the same template in multiple places, we will extract the common part through the template. When the DevUI Select component was developed before, the developer wanted to extract the common part and reported an error.

图7a.png

图7b.png

Figure 7 Code movement and injection error not found

This is because the CdkVirtualScrollFor instruction needs to inject a CdkVirtualScrollViewport. However, the element injection injector inheritance system is the DOM that inherits the static AST relationship, which is not dynamic. Therefore, the following query behavior occurs, and the search report fails.

图8.png

Figure 8 Element injector query chain lookup range

The final solution: either 1) keep the original code position unchanged, or 2) you need to inline the entire template to find it.

图9.png

Figure 9 Embedded block module enables CdkVitualScrollFo to find CdkVirtualScrollViewport (solution 2)

Summary of knowledge points : The query chain of the element injector is the DOM element ancestor of the static template.

3.3 Case 3: The form validation component is encapsulated into a sub-component and cannot be validated

This case comes from this blog " Angular: Nested template driven form ".

We also encountered the same problem when using form validation. As shown in Figure 10, for some reason we encapsulate the addresses of the three fields into a component for reuse.

图10.png

Figure 10 Encapsulate the three fields of the address of the form into a subcomponent

At this time, we will find an error, ngModelGroup requires an internal host ControlContainer , which is the content provided by the ngForm instruction.

图11.png

Figure 11 ngModelGroup can't find ControlContainer

Looking at the ngModelGroup code you can see that it only adds the constraints of the host decorator.

图12.png

Figure 12 ng_model_group.ts limits the scope of injection ControlContainer

Here you can use viewProvider with usingExisting to add the Provider of ControlContainer to the host view of AddressComponent

图13.png

Figure 13 Using viewProviders to provide external Providers to nested components

Summary of knowledge points : the magic of the combination of viewProvider and usingExisting.

3.4 Case 4: The service provided by the drag and drop module is not a singleton due to lazy loading, which makes it impossible to drag and drop each other

The internal business platform involves dragging and dropping across multiple modules. Due to the lazy loading of loadChildren, each module will package the DragDropModule of the DevUI component library separately, which provides a DragDropService. The drag and drop commands are divided into Draggable and droppable commands. The two commands communicate through DragDropService. Originally, the same module was introduced to use the service provided by the module to communicate, but after lazy loading, the DragDropModule module is packaged twice, which also generates two isolated instances. At this time, the Draggable instruction in one lazy-loading module cannot communicate with the Droppable instruction in another lazy-loading module, because the DragDropService is not the same instance at this time.

图14.png

Figure 14 Lazy loading of modules results in services not being the same instance/singleton

It is obvious here that our request requires a singleton, and the practice of a singleton is usually providerIn: 'root' just fine, then let the DragDropService of the component library not be provided at the module level, but directly at the root level. Great. But thinking about it carefully, there will be other problems here. The component library itself is provided for a variety of businesses. In case some businesses have two sets of corresponding drag and drop in two places on the page, they do not want to be linked. At this time, the singleton destroys this natural isolation based on modules.

Then it is more reasonable to replace the singleton by the business side. Remember the dependency query chain we mentioned earlier, the injector of the element is searched first, and the module injector is not found until it is not found. So the replacement idea is that we can provide an element-level provider.

图15.png

Figure 15 Using an extension method to get a new DragDropService and mark it as available at the root level

图16a.png

图16b.png

Figure 16 Use the same selector to superimpose repeated instructions, superimpose an additional instruction for the Draggable instruction and Droppable instruction of the component library, and replace the token of DragDropService with the DragDropGlobalService that has provided a singleton in the root.

As shown in Figures 15 and 16, we superimposed the directive through the element injector, replacing the DragDropService token with an instance of our own global singleton. At this time, where we need to use the DragDropService of this global singleton, we only need to introduce the module that declares and exports these two extra instructions, which enables the Draggable instruction Droppable instruction of the component library to communicate across lazy loaded modules.

Summary of knowledge points : Element injectors have higher priority than module injectors.

3.5 Case 5: How to make the drop-down menu attached to the local problem in the local theme function scene

The theme of the DevUI component library is to use the CSS custom property (css variable) declaration: the css variable value of the root to achieve theme switching. If we want to display previews of different themes at the same time in one interface, we can redeclare CSS variables locally in the DOM element to achieve the function of local themes. I used such a method to apply a theme locally when I was doing the theme dither generator before.

图17.png

Figure 17 Local theme function

But it's not enough to apply CSS variable values locally. There are some drop-down pop-up layers that are attached to the back of the body by default, which means that their attached layer is outside the local variables, which will cause a very embarrassing problem. The drop-down box of the component of the partial theme is the style of the external theme.

图18.png

Figure 18 The theme of the overlay drop-down box outside the component attachment in the partial theme is incorrect

What should we do at this time? We should move the attachment point back inside the local theme dom.

It is known that the Overlay of the DatePickerPro component of the DevUI component library uses the Overlay of the Angular CDK. After a round of analysis, we replace it with injection as follows:

1) First, we inherit OverlayContainer and implement our own ElementOverlayContainer as shown below.

图19.png

Figure 19 Customize ElementOverlayContainer and replace _createContainer logic

2) Then on the component side of the preview, directly provide our new ElementOverlayContainer and provide a new Overlay so that the new Overlay can use our OverlayContainer. Originally Overlay and OverlayContainer are both provided on the root, here we need to cover these two.

图20.png

Figure 20 Replace OverlayContainer with a custom ElementOverlayContainer to provide a new Overlay

At this time, go to preview the website again, and the DOM of the pop-up layer is successfully attached to the component-preview element.

图21.png

Figure 21 The Overlay container of cdk is attached to the specified dom, and the partial theme preview is successful

There is also a custom OverlayContainerRef in the DevUI component library for some components and modal box drawer benches, which also need to be replaced accordingly. Finally, it can realize the perfect support for partial themes such as pop-up windows and pop-up layers.

Summary of knowledge points : A good abstract pattern can make modules replaceable and achieve elegant aspect programming.

3.6 Case 6: CdkOverlay requires the CdkScrollable command to be added to the scroll bar, but it is impossible to add this command to the outermost layer of the entry component. How to deal with it

When it comes to the last case, I would like to talk about a less formal approach so that everyone can understand the essence of the provider. The essence of configuring the provider is to make it instantiate or map to an existing instance for you.

We know that if we use cdkOverlay, if we want the pop-up box to follow the scroll bar to scroll in the correct position, we need to add the cdkScrollable command to the scroll bar.

The same scene as the previous example. Our entire page is loaded through routing. For simplicity, I wrote the scroll bar on the host of the component.

图22.png

Figure 22 The content overflow scrollbar writes overflow:auto in the component:host

In this way, we have encountered a difficult problem. The module is specified by the router definition, that is, there is no explicit call anywhere <app-theme-picker-customize></app-theme-picker-customize> , then how to add the cdkScrollable instruction? The solution is as follows, some of the code is hidden here and only the core code is left.

图23.png

Figure 23 Creating an instance by injection and calling the lifecycle manually

Here, an instance of cdkScrollable is generated by injection, and the lifecycle is called synchronously in the lifecycle phase of the component.

This solution is not a formal method, but it does solve the problem. It is left as a way of thinking and exploration to the reader's taste.

Summary of knowledge points : Dependency injection configuration providers can create instances, but it should be noted that instances are treated as ordinary Service classes and cannot have a complete life cycle.

3.7 More ways to play: Customize the replacement platform to realize the interaction of letting the Angular framework run on the terminal terminal

You can refer to this blog post: " Rendering Angular applications in Terminal "

图24.png

Figure 24 Replace the RendererFactory2 renderer, etc., and let Angular run on the terminal

By replacing renderers such as RendererFactory2, the author allows Angular applications to run on the terminal. This is the flexibility of Angular's design, and even the platform can be replaced with a powerful flexibility. Detailed replacement details can be found in the original article, which will not be expanded here.

Summary of knowledge points : The power of dependency injection is that the provider can configure it by itself, and finally implement the replacement logic.

4 Summary

This article introduces the dependency injection mode of inversion of control and its benefits, introduces how dependency injection in Angular finds dependencies, how to configure providers, and how to get the desired instance with the decorator that defines and filters, and further through N A case study of how to combine the knowledge points of dependency injection to solve problems encountered in development and programming.

By correctly understanding the dependency search process, we can configure the provider in the exact location (cases 1 and 2), replace other instances with singletons (cases 4 and 5), and even connect across the constraints of nested component packages. Provided instance (case 3) or use the provided method curve to implement instruction instantiation (case 6).

Among them, case 5 seems to be a simple replacement, but to be able to write a code structure that can be replaced, it is necessary to have a deep understanding of the injection mode, and have a good and reasonable abstraction of each function. The maximum effect of injection. The injection mode provides more possible space for modules to be pluggable, plug-in, and parts, reduce coupling, and increase flexibility, so that modules can work together more elegantly and harmoniously.

The powerful dependency injection function can not only optimize the communication path of components, but also realize inversion of control, exposing more aspects of programming to the packaged components, and the implementation of some business-specific logic can also become flexible. .

Recommended articles in the past

Custom Webpack configuration method and custom loader processing case practice under Angular CLI

Web Interface Dark Mode and Theming Development

20 lines of code to add DevUI theme switching capability to your project


DevUI团队
714 声望810 粉丝

DevUI,致力于打造业界领先的企业级UI组件库。[链接]