Home>Article>Web Front-end> In-depth understanding of the dependency injection pattern in Angular (play case)
This article will give you an in-depth understanding of the dependency injection mode inAngular, and share application and gameplay cases of the dependency injection mode. I hope it will be helpful to everyone!
In Angular project development, we usually use Input property binding and Output event binding for component communication. However, Input and Output can only pass information between parent and child components. Components form a component tree based on the calling relationship. If there are only property bindings and event bindings, then two non-direct relationship components need to communicate through each connection point itself. 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 instructions, and combined with injection in the constructor, can solve this problem (right in Figure 1). [Related tutorial recommendations: "angular tutorial"]
Figure 1 Component communication mode
The picture on the left only transmits information through parent-child components , node a and node b need to communicate through many nodes; if node c wants to control node b through some configuration, the nodes between them must also set additional attributes or events to transparently transmit the corresponding information. The dependency injection mode node c in the picture on the right can provide a service for nodes a and b to communicate. Node a directly communicates with the service provided by node c, and node b also directly communicates with the service provided by node c. Finally, the communication is simplified, and the middle The node is not coupled to this part of the content, and has no obvious awareness of the communication that occurs between the upper and lower components.
Dependency Injection (DI) is not unique to Angular, it is a means to implement the Inversion of Control (IOC) design pattern , The emergence of dependency injection solves the problem of over-coupling of manual instantiation. All resources are not managed by the two parties using the resources, but are provided by the resource center or a third party, which can bring many benefits. First, centralized management of resources makes resources configurable and easy to manage. Second, it reduces the degree of dependence between the two parties using resources, which is what we call coupling.
The analogy to the real world is that when we 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 produced. The wood and pencil lead are both How it is bonded? We only need it to complete the writing function of the pencil. We will not have any contact with the specific pencil manufacturer or factory. As for the store, it can purchase pencils from the appropriate channels and realize the configurability of resources.
Combined with coding scenarios, more specifically, users do not need to explicitly create an instance (new operation) to inject and use instances. The creation of instances is determined by providers. Resource management is through tokens. Since it does not care about the provider or the creation of instances, the user can use some local injection methods (secondary configuration of tokens) to finally achieve instance replacement and dependency injection mode. Applications and aspect programming (AOP) complement each other.
Dependency injection is one of the most important core modules of the Angular framework. Angular not only provides Service type injection, but also its own component tree It is an injection dependency tree, and functions and values can also be injected. That is to say, in the Angular framework, child components can inject parent component instances through the parent component's token (usually the class name). In component library development, there are a large number of cases where 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, etc.
To use Angular injection, you must first understand its injection resolution process. Similar to the parsing process of node_modules, when no dependencies are found, the dependencies will always bubble up to the parent layer to find dependencies. The old version of Angular (beforev6) divides the injection parsing process into multi-level module injectors, multi-level component injectors and element injectors. The new version (afterv9) is simplified to a two-level model. The first query chain is the static DOM level element injector, component injector, etc., collectively called element injectors, and the other query chain is the module injector. The order of parsing and the default value after parsing failure are explained more clearly in the official code comment document (provider_flag).
Figure 2 Two-level injector search dependency process (Picture source)
That is to say, the component/instruction and the component/instruction level provide The injected content will first search for dependencies in the element in the component view all the way to the root element. If it is not found, it will then search in the module where the element is currently located and reference (including module reference and routing lazy loading reference) the parent module of the module until it is found. Root module and platform module.
Note that the injector here has inheritance. The element injector can create and inherit the search function of the injector of the parent element, and the module injector is similar. After continuous inheritance, it becomes a bit like the prototype chain of js objects.
Understanding the order priority of dependency resolution, we can provide content at the appropriate level. We already know that it comes in two types: module injection and element injection.
Module injector: You can configure providers in the metadata attribute of @NgModule, and you can also use the @Injectable statement provided after v6. provideIn is declared as the module name, 'root', etc. (Actually, there are two injectors above the root module, Platform and Null. They will not be discussed here.)
Element injector: In the metadata attribute of component @Component You can configure providers, viewProviders, or providers in the @Directive metadata of the directive.
In addition, in fact, in addition to using the declaration module injector, the @Injectable decorator can also Declared as an element injector. More often it will be declared as provided at root to implement a singleton. It integrates metadata through the class itself to avoid modules or components directly explicitly declaring the provider. In this way, if the class does not have any component directive service and other classes to inject it, there will be no code linked to the type declaration and it can be ignored by the compiler, thus achieving Shake the tree.
Another way to provide it is to directly give the value when declaring 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('my-inject-token', { providedIn: 'root', factory: () => { return new MyClass(); } });
Different options for providing dependency locations will bring some differences, which ultimately affect the size of the package and the dependencies that can be injected. Scope and dependency lifecycle. There are different applicable solutions for different scenarios, such as singleton (root), service isolation (module), multiple editing windows (component), etc. You should choose a reasonable location to avoid inappropriate shared information or redundant code packaging. .
If you only provide instance injection, it will 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, and useFactory is constructed through functions, with specified deps and specified constructor parameters. These combinations can be very versatile. . You can cut off the token token of a class and replace it with another instance you have prepared. You can create a token to save the value or instance first, and then replace it again when you need to use it later. You can even use the factory function to return it. The local information of the instance is mapped to another object or attribute value. The gameplay here will be explained through the following cases, so I won’t go into it here. The official website also has many examples for reference.
Injection in Angular can be injected in the constructor, or you can get the injector to obtain the existing one through the get method Inject elements.
Angular supports adding decorators to mark when injecting,
Here is an article "@Self or @Optional @Host? The visual guide to Angular DI decorators." which very vividly shows what will happen if different decorators are used between parent and child components. What is the difference between hit instances.
Figure 3 Filtering results of different injection decorators
Among these decorators, @Host may be the hardest to understand. Here are some specific instructions for @Host. The official explanation of the @Host decorator is
...retrieve a dependency from any injector until reaching the host element
Host here means host. The @Host decorator will limit the scope of the query to within the host element. What is a host element? If component B is a component used by component A's template, then component A's instance is the host element of component B's instance. The content generated by the component template is called a View. 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 (red box part) formed by the template content of A is the embedded view of component A, and component B is within this view, so For B, this view is B's host view. The decorator @Host limits the search scope to the host view. If it is not found, it will not bubble up.
Figure 4 Embedded view and host view
Below we use real Let’s take a case to see how dependency injection works, how to troubleshoot errors, and how to play.
The modal window component of the DevUI component library provides a service ModalService, which A modal box can pop up and can be configured as a custom component. Business students often report errors when using this component, saying that the package cannot find the custom component.
For example, the following error report:
Figure 5 When using ModalService, the error report of creating a component that references EditorX cannot find the corresponding service provider
Analyze how ModalService creates custom components,ModalService source code Open functionLines 52 and 95. You can see that ifcomponentFactoryResolver
is not passed in, thecomponentFactoryResolver
injected by ModalService will be used. 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 current situation in Figure 6 is like this. According to Figure 6, there is no EditorXModuleService in the injector of ModalService.
Figure 6 Module service provision relationship diagram
According to the inheritance of the injector, there are four solutions:
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 homepage module, and the result is a subpage The content that needs to be used is placed in the AppModule. The large rich text module is loaded on the first load, which aggravates the FMP (First Meaningful Paint) and cannot be adopted.
Introduce ModalService in the module that introduces EditorXModule and uses ModalService - it is advisable. There is only one situation that is not advisable, that is, calling ModalService is another top-level public Service, which still puts unnecessary modules on the upper layer for loading.
When triggering the component that uses ModalService, inject thecomponentFactoryResolver
of the current module and pass it to the open function parameter of ModalService - It is advisable to use it where it is actually used. Introducing EditorXModule.
In the module used, manually provide a ModalService - it is advisable to solve the problem of injected search.
The four methods are actually solving the problem of EditorXModuleService in the internal chain of the injector used by ModalService. By ensuring that the search chain is at two levels, this problem can be solved.
: Module injector inheritance and search scope.
3.2 Case 2: CdkVirtualScrollFor cannot find CdkVirtualScrollViewportFigure 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 inherits the DOM of the static AST relationship, and the dynamic one is not possible. Therefore, the following query behavior occurs, and the search fails.
Figure 8 Element Injector Query Chain Search Range
Final solution: Either 1) keep the original code position unchanged, or 2) need to change the entire You can find it embedded in the template.
Figure 9 Embed the whole module so that CdkVitualScrollFo can find CdkVirtualScrollViewport (Solution 2)
Summary of knowledge points: Element injection The query chain of the controller is the DOM element ancestor of the static template.
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 reasons we encapsulate the addresses of the three fields into a component for reuse.
Figure 10 Encapsulate the three address fields of the form into a sub-component
At this time we will find that an error is reported,ngModelGroup
AControlContainer
inside the host is required, which is the content provided by the ngForm directive.
Figure 11 ngModelGroup cannot find ControlContainer
Looking at the ngModelGroup code, you can see that it only adds the restriction of the host decorator.
Figure 12 ng_model_group.ts limits the scope of injected ControlContainer
Here you can use viewProvider with usingExisting to add the Provider of ControlContainer to the host view of AddressComponent
Figure 13 Use viewProviders to provide external Providers for nested components
Summary of knowledge points: The wonderful combination of viewProvider and usingExisting.
The internal business platform involves cross-border Dragging of multiple modules involves lazy loading of loadChildren. Each module will separately package the DragDropModule of the DevUI component library, which provides a DragDropService. Drag-and-drop instructions are divided into Draggable instructions and Droppable instructions. The two instructions communicate through DragDropService. Originally, it was possible to communicate by introducing the same module and using the Service provided by the module. However, after lazy loading, the DragDropModule module was packaged twice, which also resulted in two isolated instances. At this time, the Draggable instruction in a lazy-loaded module cannot communicate with the Droppable instruction in another lazy-loaded module, because the DragDropService is not the same instance at this time.
Figure 14 Lazy loading of modules leads to services not being the same instance/single case
It is obvious that our statement requires a singleton, and the singleton approach UsuallyproviderIn: 'root'
is fine, then why don't the DragDropService of the component library be provided at the module level and directly provide the root domain. But if you think about it carefully, there are other problems here. The component library itself is provided for use by a variety of businesses. If some businesses have two corresponding drag and drop groups in two places on the page, they do not want to be linked. At this time, the singleton destroys the natural isolation based on the module.
Then it would be more reasonable to implement singleton replacement by the business side. Remember the dependency query chain we mentioned earlier. The element injector is searched first. If it is not found, the module injector is started. So the replacement idea is that we can provide element-level providers.
Figure 15 Use the extension method to obtain a new DragDropService and mark it as provided at the root level
Figure 16 You can use the same selector to superimpose repeated instructions, superimpose an additional instruction on 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
For example Figures 15 and 16, we use the element injector to superimpose instructions and replace the DragDropService token with an instance of our own global singleton. At this time, where we need to use the global singleton DragDropService, we only need to introduce the module that declares and exports these two extra instructions to enable the Draggable instruction Droppable instruction of the component library to communicate across lazy loading modules.
Summary of knowledge points: Element injectors have higher priority than module injectors.
The theming of the DevUI component library uses CSS custom attributes (css variables ) declares: root’s css variable value to achieve theme switching. If we want to display previews of different themes at the same time in one interface, we can re-declare css variables locally in the DOM element to achieve the function of local themes. When I was making a theme dither generator before, I used this method to locally apply a theme.
Figure 17 Partial theme function
But just applying css variable values locally is not enough. There are some drop-down pop-up layers that are attached to the end of the body by default. , that is to say, its attachment layer is outside the local variable, which will cause a very embarrassing problem. The drop-down box of the local theme component displays the style of the external theme.
Figure 18 The overlay drop-down box theme attached to the external component in the local theme is incorrect
What should I 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 Angular CDK. After a round of analysis, we replaced it with injection as follows:
1) First, we inherit OverlayContainer and implement our own ElementOverlayContainer is shown below.
Figure 19 Customize ElementOverlayContainer and replace _createContainer logic
2) Then directly provide our new ElementOverlayContainer on the component side of the preview, and provide new Overlay so that the new Overlay can use our OverlayContainer. Originally Overlay and OverlayContainer are provided on root, here we need to cover these two.
##Figure 20 Replace OverlayContainer with a custom ElementOverlayContainer and provide a new Overlay At this time, go to preview the website, and the DOM of the pop-up layer will be successfully Attached to the component-preview element. 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 inside the DevUI component library Some components and modal box drawer benches also need to be replaced accordingly. Finally, pop-up layers and other pop-up layers can be realized to perfectly support local themes.Summary of knowledge points: Good abstract patterns can make modules replaceable and achieve elegant aspect programming.
is not explicitly called anywhere. So what about the cdkScrollable instruction? What about adding it? The solution is as follows. Some of the code is hidden here and only the core code is left.
Figure 23 Create an instance through injection and manually call the life cycle
Here, an instance of cdkScrollable is generated through injection, and the life cycle is called synchronously during the component's life cycle stage.
This solution is not a formal method, but it does solve the problem. It is left here as an idea and exploration for the readers to taste.
Summary of knowledge points: Dependency injection configuration provider can create instances, but please note that instances will be treated as ordinary Service classes and cannot have a complete life cycle.
You can refer to this blog post: "Rendering Angular applications in Terminal》
##Figure 24 Replace the RendererFactory2 renderer and other contents to let Angular run on the terminal The author replaces the RendererFactory2 and so on Renderer allows Angular applications to run on the terminal. This is the flexibility of Angular design. Even the platform can be replaced. It is powerful and flexible. Detailed replacement details can be found in the original article and will not be expanded upon 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.
Programming Video! !
The above is the detailed content of In-depth understanding of the dependency injection pattern in Angular (play case). For more information, please follow other related articles on the PHP Chinese website!