In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-02-25 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article mainly introduces "how to use TypeScript to achieve an IoC container". In daily operation, I believe many people have doubts about how to use TypeScript to achieve an IoC container. Xiaobian consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts about "how to use TypeScript to achieve an IoC container". Next, please follow the editor to study!
I. Overview of background
Before introducing what an IoC container is, let's take a common scenario in our daily work, that is, creating an instance of a specified class.
The simplest case is that the class does not rely on other classes, but the reality is often cruel. When we create an instance of a class, we need to rely on the corresponding instances of different classes. In order to enable friends to better understand the above content, Brother Po to give an example.
A car? It usually consists of four parts: engine, chassis, car body and electrical equipment. The internal structure of automotive electrical equipment is very complex, for simplicity, we only consider three parts: the engine, the chassis and the body.
In real life, it is very difficult to build a car. In the world of software, this doesn't stop us.
Before we start building a car, we have to take a look at the "drawings":
After reading the "drawings" above, we will start the journey of building a car right away. In the first step, let's define the body class:
1. Define the body class
Export default class Body {}
two。 Define chassis CLA
Export default class Chassis {}
3. Define engine classes
Export default class Engine {start () {console.log ("engine started");}}
4. Define the automobile class
Import Engine from'. / engine'; import Chassis from'. / chassis'; import Body from'. / body'; export default class Car {engine: Engine; chassis: Chassis; body: Body; constructor () {this.engine = new Engine (); this.body = new Body (); this.chassis = new Chassis ();} run () {this.engine.start ();}}
Everything is ready, let's build a car right away:
Const car = new Car (); / / Po builds a new car car.run (); / / console output: engine starts
Now, although the car is ready to start, there are the following problems:
Problem 1: when building a car, you can't choose the configuration. For example, if you want to change the car engine, according to the current plan, it will not be possible.
Problem 2: inside the car class, you need to manually create the parts of the car in the constructor.
To solve the first problem and provide a more flexible solution, we can ReFactor the defined car classes as follows:
Export default class Car {body: Body; engine: Engine; chassis: Chassis; constructor (engine, body, chassis) {this.engine = engine; this.body = body; this.chassis = chassis;} run () {this.engine.start ();}
After refactoring the car class, let's build a new car:
Const engine = new NewEngine (); const body = new Body (); const chassis = new Chassis (); const newCar = newCar (engine, body, chassis); newCar.run ()
Now that we have solved the first problem mentioned above, to solve the second problem, we need to look at the concept of IoC (inversion of Control).
What is IoC?
IoC (Inversion of Control), that is, "inversion of control". In development, IoC means that the object you design is under the control of the container, rather than using the traditional way of direct control inside the object.
How to understand IoC well? The key to a good understanding of IoC is to make clear "who controls whom, controls what, why it is reversed, and which aspects are reversed". Let's analyze it in depth.
Who controls who and what: in traditional programming, we create objects directly inside the objects through new, and the program takes the initiative to create dependent objects, while IoC has a special container to create these objects, that is, the IoC container controls the creation of objects.
Who controls who? Of course, it is the IoC container that controls the object; controls what? The main purpose is to control the acquisition of external resources (dependent objects).
Why is it reversed and which aspects are reversed: there is a reversal, there is a positive reversal, and the traditional application is controlled actively by ourselves in the program to obtain the dependent object, that is, the positive reversal; while the inversion is helped by the container to create and inject the dependent object.
Why the reversal? Because the container helps us find and inject the dependent object, the object only passively accepts the dependent object, so it is reversed; which aspects have been reversed? The acquisition of dependent objects has been reversed.
What can IoC do?
IoC is not a technology, but an idea, a design principle in object-oriented programming, which can be used to reduce the coupling between computer code.
In traditional applications, we take the initiative to create dependent objects within the class, resulting in high coupling between classes and difficult to test; with the IoC container, the control of creating and finding dependent objects is given to the container, and the container injects composite objects, so the objects are loosely coupled. This is also convenient for testing, conducive to functional reuse, and more importantly, makes the whole architecture of the program very flexible.
In fact, the biggest change that IoC brings to programming is not from the code, but from the idea, there has been a "master-slave transposition" change. The application is supposed to be the boss, and it is an active attack to get any resources, but in the idea of IoC, the application becomes passive, passively waiting for the IoC container to create and inject the resources it needs.
IV. The relationship between IoC and DI
For control inversion, the most common method is called dependency injection, or DI (Dependency Injection) for short.
The dependency between components is determined by the container at run time, that is, a dependency is dynamically injected into the component by the container. The purpose of dependency injection is not to bring more functions to the software system, but to increase the frequency of component reuse and to build a flexible and scalable platform for the system.
Through the dependency injection mechanism, we only need to specify the resources needed by the target and complete our own business logic through simple configuration without any code, without caring about where the specific resources come from and by whom.
The key to understanding DI is "who depends on whom, why, who injects whom, and what":
Who depends on whom: of course, the application depends on the IoC container
Why do you need dependency: applications need IoC containers to provide external resources (including objects, resources, and constant data) that objects need.
Who injects who: it's obviously the object that the IoC container injection application depends on.
What is injected: the external resources (including objects, resources, constant data) needed to inject an object.
So what does IoC have to do with DI? In fact, they are described from different angles of the same concept, because the concept of control inversion is relatively vague (it may only be understood as the container control object level, it is difficult to think of who will maintain the dependency), so in 2004, the master Martin Fowler gave a new name: "dependency injection". Relative to IoC, "dependency injection" clearly describes the injected object dependency IoC container configuration dependency object.
Generally speaking, Inversion of Control refers to the transfer of control over the creation of objects. In the past, the initiative and timing of creating objects were controlled by the application, but now this right is transferred to the IoC container, which is a factory dedicated to creating objects. It gives you whatever objects you need.
With the IoC container, the dependencies change, and the original dependencies are gone. They all rely on the IoC container, and the relationship between them is established through the IoC container.
After introducing so many concepts, let's take a look at the obvious difference between not using a dependency injection framework and using a dependency injection framework.
4.1 do not use the dependency injection framework
Suppose our service A depends on service B, that is, before we can use service A, we need to create service B. The specific process is shown in the following figure:
As you can see from the figure above, when the dependency injection framework is not used, the consumers of the service need to care about how the service itself and its dependent objects are created, and need to maintain dependencies manually. If the service itself needs to rely on multiple objects, it will increase the difficulty of use and the cost of later maintenance.
For the above problems, we can consider introducing a dependency injection framework. Let's take a look at what happens to the overall process when a dependency injection framework is introduced.
4.2 use the dependency injection framework
After using the dependency injection framework, the services in the system are uniformly registered in the IoC container, and the dependencies need to be declared if the services have dependencies on other services. When a user needs to use a specific service, the IoC container is responsible for the creation and management of the service and its dependent objects. The specific process is shown in the following figure:
So far, we have introduced the concepts and characteristics of IoC and DI, and then we will introduce the application of DI.
5. The application of DI
DI has corresponding applications in both front-end and server-side. For example, the representatives in the front-end field are AngularJS and Angular, while in the server-side field is the well-known NestJS in the Node.js ecology. Next, we will briefly introduce the application of DI in AngularJS/Angular and NestJS.
Application of DI in AngularJS
Dependency injection is one of the core features of AngularJS. There are three ways to declare dependencies in AngularJS:
/ / method 1: use the $inject annotation method let fn = function (a, b) {}; fn.$inject = ['averse,' b']; / / method 2: use the array-style annotations method let fn = ['a, b) {}]; / / method 3: use the implicit declaration method let fn = function (a, b) {}; / / not recommended
For the above code, I believe that the boys who have used AngularJS will be no stranger. DI, as the core functional feature of AngularJS, is still quite powerful, but with the popularity of AngularJS and the increasing complexity of application, the problems of AngularJS DI system are exposed.
Here, Brother Po briefly introduces several problems existing in AngularJS DI system:
Internal caching: all dependencies in AngularJS applications are singletons, and we cannot control whether or not to use new instances
Namespace conflict: in the system, we use strings to identify the name of the service, assuming that we already have a CarService in the project, but the same service is introduced into the third-party library, which is prone to confusion.
Because of the above problems in AngularJS DI, a new DI system is redesigned in the subsequent Angular.
5.2 Application of DI in Angular
In the case of the previous car, for example, we can think of the car, engine, chassis and body as a "service", so they will be registered with the DI system as a service provider. In order to distinguish different services, we need to use different tokens (Token) to identify them. Then we will create an injector object based on the registered service provider.
Then, when we need to get the specified service, we can get the token dependency object from the injector object through the token corresponding to the service. The details of the above process are shown in the following figure:
OK, after understanding the above process. Let's take a look at how to "build a car" using Angular's built-in DI system.
5.2.1 car.ts
/ / car.ts import {Injectable, ReflectiveInjector} from'@ angular/core'; / / configure Provider @ Injectable ({providedIn: 'root',}) export class Body {} @ Injectable ({providedIn:' root',}) export class Chassis {} @ Injectable ({providedIn: 'root',}) export class Engine {start () {console.log (' engine started') } @ Injectable () export default class Car {/ / injects dependent objects constructor (private engine: Engine, private body: Body, private chassis: Chassis) {} run () {this.engine.start ();}} const injector = ReflectiveInjector.resolveAndCreate ([Car, Engine, Chassis, Body,]); const car = injector.get (Car); car.run ()
In the above code, we call the resolveAndCreate method of the ReflectiveInjector object to create the injector manually, and then get the corresponding dependent object according to the Token of the vehicle. By looking at the above code, you can see that we no longer need to manually manage and maintain dependent objects, and these "dirty work" and "hard work" have been left to the injector.
In addition, if we want to get the car object normally, we need to declare that Car corresponds to Provider in the app.module.ts file, as shown below:
5.2.2 app.module.ts
Import {BrowserModule} from'@ angular/platform-browser'; import {NgModule} from'@ angular/core'; import {AppComponent} from'. / app.component'; import Car, {Body, Chassis, Engine} from'. / car'; @ NgModule ({declarations: [AppComponent], imports: [BrowserModule], providers: [{provide: Car, deps: [Engine, Body, Chassis]}], bootstrap: [AppComponent],}) export class AppModule {}
5.3 Application of DI in NestJS
NestJS is a framework for building efficient and extensible Node.js Web applications. It uses modern JavaScript or TypeScript (which maintains compatibility with pure JavaScript) and combines elements of OOP (object-oriented programming), FP (functional programming) and FRP (functional responsive programming).
At the bottom, Nest uses Express, but it also provides compatibility with a variety of other libraries, such as Fastify, making it easy to use a variety of available third-party plug-ins.
In recent years, Node.js,JavaScript has become the "common language" of Web front-end and back-end applications, resulting in refreshing projects such as Angular, React, Vue and so on. These projects improve the productivity of developers and make it possible to quickly build testable and scalable front-end applications. However, on the server side, although there are many excellent libraries, helper, and Node tools, they do not effectively solve the main problem-architecture.
NestJS is designed to provide an out-of-the-box application architecture that allows you to easily create applications that are highly testable, scalable, loosely coupled, and easy to maintain. Dependency injection is also provided for us developers in NestJS. Here we use an example from the official website to demonstrate the function of dependency injection.
5.3.1 app.service.ts
Import {Injectable} from'@ nestjs/common'; @ Injectable () export class AppService {getHello (): string {return 'Hello Worldwaters;}}
5.3.2 app.controller.ts
Import {Get, Controller, Render} from'@ nestjs/common'; import {AppService} from'. / app.service'; @ Controller () export class AppController {constructor (private readonly appService: AppService) {} @ Get () @ Render ('index') render () {const message = this.appService.getHello (); return {message};}}
In AppController, we inject the AppService object through construction injection. When the user visits the home page, we call the getHello method of the AppService object to get the 'Hello wordings'. Message and returns the message to the user. Of course, in order to ensure that dependency injection can work properly, we also need to declare providers and controllers in AppModule, as follows:
Import {Module} from'@ nestjs/common'; import {AppController} from'. / app.controller'; import {AppService} from'. / app.service'; @ Module ({imports: [], controllers: [AppController], providers: [AppService],}) export class AppModule {}
In fact, DI is not unique to AngularJS/Angular and NestJS. If you want to use the features of DI/IoC in other projects, Po recommends that you use InversifyJS, which is a powerful, lightweight IoC container that can be used in JavaScript and Node.js applications.
If you are interested in InversifyJS, you can learn about it for yourself, and Brother Po will not continue to introduce it. Next, we will move on to the focus of this article, that is, how to implement a simple IoC container using TypeScript, which implements the following functions:
Handwritten IoC container
In order to better understand the implementation code of the IoC container, the editor first introduces some related pre-knowledge.
6.1 decorator
If you have ever used Angular or NestJS, I believe you are no stranger to the following code.
@ Injectable () export class HttpService {constructor (private httpClient: HttpClient) {}}
In the above code, we used the Injectable decorator. This decorator is used to indicate that this class can automatically inject its dependencies. The @ symbol in @ Injectable () belongs to grammatical sugar.
A decorator is a function that wraps a class, function, or method and adds behavior to it. This is useful for defining the metadata associated with an object. There are four categories of decorators:
Class decorator (Class decorators)
Attribute decorator (Property decorators)
Method decorator (Method decorators)
Parameter decorator (Parameter decorators)
The @ Injectable () decorator used in the previous example belongs to the class decorator. In the HttpService class decorated by this class decorator, we inject the HttpClient dependent object used to process the HTTP request by constructing injection.
6.2 reflection
@ Injectable () export class HttpService {constructor (private httpClient: HttpClient) {}}
If the above code sets the target of compilation to ES5, the following code is generated:
/ / ignore codes such as _ _ decorate function var _ _ metadata = (this & & this.__metadata) | | function (k, v) {if (typeof Reflect = "object" & & typeof Reflect.metadata = "function") return Reflect.metadata (k, v);}; var HttpService = / * @ class * / (function () {function HttpService (httpClient) {this.httpClient = httpClient;} var _ a HttpService = _ _ decorate ([Injectable (), _ _ metadata ("design:paramtypes", [typeof (_ a = typeof HttpClient! = = "undefined" & & HttpClient) = "function"? _ a: Object]), HttpService); return HttpService;} ()
By looking at the above code, you can see that the type of the httpClient parameter in the HttpService constructor is erased because JavaScript is a weakly typed language. So how do you ensure that the correct type of dependent objects are injected at run time? Here TypeScript uses reflect-metadata, a third-party library, to store additional type information.
The library reflect-metadata provides a lot of API for manipulating meta-information. Here we will briefly introduce a few commonly used API:
/ / define metadata on an object or property Reflect.defineMetadata (metadataKey, metadataValue, target); Reflect.defineMetadata (metadataKey, metadataValue, target, propertyKey); / / check for presence of a metadata key on the prototype chain of an object or property let result = Reflect.hasMetadata (metadataKey, target); let result = Reflect.hasMetadata (metadataKey, target, propertyKey); / / get metadata value of a metadata key on the prototype chain of an object or property let result = Reflect.getMetadata (metadataKey, target); let result = Reflect.getMetadata (metadataKey, target, propertyKey) / delete metadata from an object or property let result = Reflect.deleteMetadata (metadataKey, target); let result = Reflect.deleteMetadata (metadataKey, target, propertyKey); / / apply metadata via a decorator to a constructor @ Reflect.metadata (metadataKey, metadataValue) class C {/ / apply metadata via a decorator to a method (property) @ Reflect.metadata (metadataKey, metadataValue) method () {}}
You only need to have a brief understanding of the above API. In the following content, we will show you how to use it. Here we need to pay attention to the following two issues:
For classes or functions, we need to decorate them with decorators so that the metadata can be saved.
Only classes, enumerations, or raw data types can be recorded. Interfaces and federated types appear as objects. This is because these types disappear completely after compilation, while classes always exist.
6.3Definitions Token and Provider
After learning the basics of decorators and reflection, let's start implementing the IoC container. Our IoC container will use two main concepts: Token and Provider. Tokens are identifiers of objects to be created by the IoC container, and providers are used to describe how to create these objects.
The smallest common interface for the IoC container is as follows:
Export class Container {addProvider (provider: Provider) {} / / TODO inject (type: Token): t {} / / TODO}
Next, let's define Token:
/ / type.ts interface Type extends Function {new (. Args: any []): t;} / / provider.ts class InjectionToken {constructor (public injectionIdentifier: string) {}} type Token = Type | InjectionToken
The Token type is a federated type, which can be either a function type or an InjectionToken type. The use of strings as Token in AngularJS may, in some cases, cause conflicts. Therefore, in order to solve this problem, we define the InjectionToken class to avoid naming conflicts.
After defining the Token type, let's define three different types of Provider:
ClassProvider: provides a class for creating dependent objects
ValueProvider: provide an existing value as a dependent object
FactoryProvider: provides a factory method for creating dependent objects.
/ / provider.ts export type Factory = () = > T; export interface BaseProvider {provide: Token;} export interface ClassProvider extends BaseProvider {provide: Token; useClass: Type;} export interface ValueProvider extends BaseProvider {provide: Token; useValue: t;} export interface FactoryProvider extends BaseProvider {provide: Token; useFactory: Factory;} export type Provider = | ClassProvider | ValueProvider | FactoryProvider
In order to more easily distinguish between these three different types of Provider, we have customized three types of guard functions:
/ / provider.ts export function isClassProvider (provider: BaseProvider): provider isClassProvider {return (provider as any). UseClass! = = undefined;} export function isValueProvider (provider: BaseProvider): provider isValueProvider {return (provider as any). UseValue! = undefined;} export function isFactoryProvider (provider: BaseProvider): provider isFactoryProvider {return (provider as any). UseFactory! = = undefined;}
6.4 define decorator
As we mentioned earlier, for classes or functions, we need to decorate them with decorators so that the metadata can be saved. So next let's create the Injectable and Inject decorators, respectively.
6.4.1 Injectable decorator
The Injectable decorator, which belongs to the class decorator, is used to indicate that this class can automatically inject its dependencies. In TypeScript, the class decorator is declared as follows:
Declare type ClassDecorator = (target: TFunction) = > TFunction | void
Class decorator, as its name implies, is used to decorate classes. It takes a parameter: target: TFunction, which represents the decorated class. Let's take a look at the implementation of the Injectable decorator:
/ / Injectable.ts import {Type} from ". / type"; import "reflect-metadata"; const INJECTABLE_METADATA_KEY = Symbol ("INJECTABLE_KEY"); export function Injectable () {return function (target: any) {Reflect.defineMetadata (INJECTABLE_METADATA_KEY, true, target); return target;};}
In the above code, when the Injectable function is called, a new function is returned. In the new function, we use the defineMetadata API provided by the library reflect-metadata to save meta-information, and the use of defineMetadata API is as follows:
/ / define metadata on an object or property Reflect.defineMetadata (metadataKey, metadataValue, target); Reflect.defineMetadata (metadataKey, metadataValue, target, propertyKey)
The Injectable class decorator is also easy to use, just use the @ Injectable () syntax sugar above the decorated class to apply the decorator:
@ Injectable () export class HttpService {constructor (private httpClient: HttpClient) {}}
In the above example, we are injecting a HttpClient object of type Type. But in the actual project, it is often more complicated. In addition to injecting dependent objects of type Type, we may also inject other types of dependent objects, such as we want to inject the API address of the remote server into the HttpService service. For this situation, we need to use the Inject decorator.
6.4.2 Inject decorator
Next let's create the Inject decorator, which belongs to the parameter decorator. In TypeScript, the parameter decorator is declared as follows:
Declare type ParameterDecorator = (target: Object, propertyKey: string | symbol, parameterIndex: number) = > void
As the name implies, the parameter decorator is used to decorate function parameters, and it receives three parameters:
Target: Object-the class that is decorated
PropertyKey: string | symbol-method name
ParameterIndex: number-- the index value of the parameter in the method.
Let's take a look at the implementation of the Inject decorator:
/ / Inject.ts import {Token} from'. / provider'; import 'reflect-metadata'; const INJECT_METADATA_KEY = Symbol (' INJECT_KEY'); export function Inject (token: Token) {return function (target: any, _: string | symbol, index: number) {Reflect.defineMetadata (INJECT_METADATA_KEY, token, target, `index-$ {index} `); return target;};}
In the above code, when the Inject function is called, a new function is returned. In the new function, we use the defineMetadata API provided by the library reflect-metadata to save meta-information about parameters. Here is to save index index information and Token information.
After defining the Inject decorator, we can use it to inject the API address of the remote server we mentioned earlier, as follows:
Const API_URL = new InjectionToken ('apiUrl'); @ Injectable () export class HttpService {constructor (private httpClient: HttpClient, @ Inject (API_URL) private apiUrl: string) {}}
6.5 implement IoC container
So far, we have defined Token, Provider, Injectable, and Inject decorators. Next, let's implement the API of the IoC container mentioned earlier:
Export class Container {addProvider (provider: Provider) {} / / TODO inject (type: Token): t {} / / TODO}
6.5.1 implementing the addProvider method
The implementation of the addProvider () method is simple, and we use Map to store the relationship between Token and Provider:
Export class Container {private providers = new Map (); addProvider (provider: Provider) {this.assertInjectableIfClassProvider (provider); this.providers.set (provider.provide, provider);}}
Inside the addProvider () method, in addition to saving the corresponding information between Token and Provider in the providers object, we define an assertInjectableIfClassProvider method to ensure that the added ClassProvider is injectable. The specific implementation of this method is as follows:
Private assertInjectableIfClassProvider (provider: Provider) {if (isClassProvider (provider) & &! isInjectable (provider.useClass)) {throw new Error (`Cannot provide ${this.getTokenName (provider.provide)} using class ${this.getTokenName (provider.useClass)}, ${this.getTokenName (provider.useClass)} isn't table`);}}
In the body of the assertInjectableIfClassProvider method, we use the isClassProvider type guard function described earlier to determine whether it is ClassProvider. If so, we will determine whether the ClassProvider is injectable, specifically using the isInjectable function, which is defined as follows:
Export function isInjectable (target: Type) {return Reflect.getMetadata (INJECTABLE_METADATA_KEY, target) = true;}
In the isInjectable function, we use the getMetadata API provided by the reflect-metadata library to get the meta-information saved in the class. To better understand the above code, let's review the previous Injectable decorator:
Const INJECTABLE_METADATA_KEY = Symbol ("INJECTABLE_KEY"); export function Injectable () {return function (target: any) {Reflect.defineMetadata (INJECTABLE_METADATA_KEY, true, target); return target;};}
If the added Provider is ClassProvider, but the class corresponding to Provider is not injectable, an exception is thrown. To make exception messages more friendly and intuitive. We define a getTokenName method to get the name of the Token:
Private getTokenName (token: Token) {return token instanceof InjectionToken? Token.injectionIdentifier: token.name;}
Now that we have implemented the addProvider method of the Container class, we can use it to add three different types of Provider:
Const container = new Container (); const input = {x: 200}; class BasicClass {} / / register ClassProvider container.addProvider ({provide: BasicClass, useClass: BasicClass}); / / register ValueProvider container.addProvider ({provide: BasicClass, useValue: input}); / / register FactoryProvider container.addProvider ({provide: BasicClass, useFactory: () = > input})
It is important to note that the registration of three different types of Provider in the above example uses the same Token only for demonstration purposes. Let's implement the core inject method in the Container class.
6.5.2 implement the inject method
Before we look at the specific implementation of the inject method, let's take a look at what the method does:
Const container = new Container (); const input = {x: 200}; container.addProvider ({provide: BasicClass, useValue: input}); const output = container.inject (BasicClass); expect (input) .tobe (output); / / true
Looking at the above test cases, we can see that the function of the inject method in the Container class is to obtain the corresponding object according to the Token. In the addProvider method we implemented earlier, we saved the Token and the corresponding Provider of the Token in the providers Map object. So in the inject method, we can first get the corresponding Provider object of the Token from the providers object, and then get the corresponding object according to different types of Provider.
OK, let's take a look at the implementation of the inject method:
Inject (type: Token): t {let provider = this.providers.get (type); / / handling classes decorated with Injectable decorator if (provider = undefined & &! (type instanceof InjectionToken)) {provider = {provide: type, useClass: type}; this.assertInjectableIfClassProvider (provider);} return this.injectWithProvider (type, provider);}
In the above code, in addition to dealing with the normal process. We also deal with a special scenario where instead of registering Provider with the addProvider method, we use the Injectable decorator to decorate a class. For this particular scenario, we will create a provider object based on the passed type parameters, and then call the injectWithProvider method to create the object. The specific implementation of this method is as follows:
Private injectWithProvider (type: Token, provider?: Provider): t {if (provider = undefined) {throw new Error (`No provider for type ${this.getTokenName (type)} `);} if (isClassProvider (provider)) {return this.injectClass (provider as ClassProvider);} else if (isValueProvider (provider)) {return this.injectValue (provider as ValueProvider);} else {return this.injectFactory (provider as FactoryProvider);}}
Inside the injectWithProvider method, we use the previously defined type guard function to distinguish between three different types of Provider to handle different Provider. Let's first take a look at the simplest ValueProvider. When it is found that the ValueProvider type is injected, the injectValue method is called to get the corresponding object:
/ / {provide: API_URL, useValue: 'https://www.semlinker.com/'} private injectValue (valueProvider: ValueProvider): t {return valueProvider.useValue;}
Next, let's take a look at how to deal with Provider of type FactoryProvider. If it is found to be a type of FactoryProvider, the injectFactory method is called to get its corresponding object. The implementation of this method is also very simple:
/ / const input = {x: 200}; / / container.addProvider ({provide: BasicClass, useFactory: () = > input}); private injectFactory (valueProvider: FactoryProvider): t {return valueProvider.useFactory ();}
Finally, let's analyze how to deal with ClassProvider. For the ClassProvider class, through the useClass property of the Provider object, we can directly get the corresponding constructor of the class. The simplest case is that the class does not depend on other objects, but in most scenarios, the service class that is about to be instantiated depends on other objects. So before instantiating the service class, we need to construct the object on which it depends.
So now the question is, how do you get the objects on which the class depends? Let's first analyze the following code:
Const API_URL = new InjectionToken ('apiUrl'); @ Injectable () export class HttpService {constructor (private httpClient: HttpClient, @ Inject (API_URL) private apiUrl: string) {}}
If the above code sets the target of compilation to ES5, the following code is generated:
/ / the definition of _ _ decorate function var _ _ metadata = (this & & this.__metadata) | | function (k, v) {if (typeof Reflect = "object" & & typeof Reflect.metadata = "function") return Reflect.metadata (k, v);}; var _ param = (this & & this.__param) | | function (paramIndex, decorator) {return function (target, key) {decorator (target, key, paramIndex);}} Var HttpService = / * * @ class * / (function () {function HttpService (httpClient, apiUrl) {this.httpClient = httpClient; this.apiUrl = apiUrl;} var _ a HttpService = _ _ decorate ([Injectable (), _ _ param (1, Inject (API_URL)), _ _ metadata ("design:paramtypes", [typeof (_ a = typeof HttpClient! = = "undefined" & & HttpClient) = "function"? _ a: Object, String]), HttpService); return HttpService;} ())
Will you feel a little dizzy if you look at the above code? Don't worry, Brother Po will analyze the two parameters in HttpService one by one. First, let's analyze the apiUrl parameters:
In the figure, we can clearly see that the Token corresponding to API_URL will eventually be saved through Reflect.defineMetadata API, and the Key used is Symbol ('INJECT_KEY'). For another parameter, httpClient, the Key it uses is "design:paramtypes", which modifies the parameter type of the target object method.
In addition to "design:paramtypes", there are other metadataKey, such as design:type and design:returntype, which are used to modify the type of the target object and the type of value returned by the target object method, respectively.
As you can see from the figure above, the parameter types of the HttpService constructor will eventually be stored using Reflect.metadata API. With all this knowledge, let's define a getInjectedParams method to get the dependent objects declared in the class constructor. The implementation of this method is as follows:
Type InjectableParam = Type; const REFLECT_PARAMS = "design:paramtypes"; private getInjectedParams (target: Type) {/ / get the type of parameter const argTypes = Reflect.getMetadata (REFLECT_PARAMS, target) as (| InjectableParam | undefined) []; if (argTypes = undefined) {return [];} return argTypes.map ((argType, index) = > {/ / The reflect-metadata API fails on circular dependencies, and will return undefined / / for the argument instead. If (argType = undefined) {throw new Error (`Injection error. Recursive dependency detected in constructor for type ${target.name} with parameter at index ${index} `);} const overrideToken = getInjectionToken (target, index); const actualToken = overrideToken = undefined? ArgType: overrideToken; let provider = this.providers.get (actualToken); return this.injectWithProvider (actualToken, provider);};}
Because the type of our Token is Type | InjectionToken union type, we also need to consider the case of InjectionToken in the getInjectedParams method, so we define a getInjectionToken method to get the Token registered with the @ Inject decorator. The implementation of this method is simple:
Export function getInjectionToken (target: any, index: number) {return Reflect.getMetadata (INJECT_METADATA_KEY, target, `index-$ {index} `) as Token | undefined;}
Now that we can get the dependent objects in the class constructor, based on the getInjectedParams method defined earlier, let's define an injectClass method that instantiates the class registered by ClassProvider.
/ / {provide: HttpClient, useClass: HttpClient} private injectClass (classProvider: ClassProvider): t {const target = classProvider.useClass; const params = this.getInjectedParams (target); return Reflect.construct (target, params);}
Now that the two methods defined in the IoC container have been implemented, let's take a look at the complete code of the IoC container:
/ / container.ts type InjectableParam = Type; const REFLECT_PARAMS = "design:paramtypes"; export class Container {private providers = new Map (); addProvider (provider: Provider) {this.assertInjectableIfClassProvider (provider); this.providers.set (provider.provide, provider);} inject (type: Token): t {let provider = this.providers.get (type); if (provider = = undefined & &! (type instanceof InjectionToken)) {provider = {provide: type, useClass: type} This.assertInjectableIfClassProvider (provider);} return this.injectWithProvider (type, provider);} private injectWithProvider (type: Token, provider?: Provider): t {if (provider = undefined) {throw new Error (`No provider for type ${this.getTokenName (type)} `);} if (isClassProvider (provider)) {return this.injectClass (provider as ClassProvider) } else if (isValueProvider (provider)) {return this.injectValue (provider as ValueProvider);} else {/ / Factory provider by process of elimination return this.injectFactory (provider as FactoryProvider) }} private assertInjectableIfClassProvider (provider: Provider) {if (isClassProvider (provider) & &! isInjectable (provider.useClass)) {throw new Error (`Cannot provide ${this.getTokenName (provider.provide)} using class ${this.getTokenName (provider.useClass)}, ${this.getTokenName (provider.useClass)} isn't roomtable`) }} private injectClass (classProvider: ClassProvider): t {const target = classProvider.useClass; const params = this.getInjectedParams (target); return Reflect.construct (target, params);} private injectValue (valueProvider: ValueProvider): t {return valueProvider.useValue;} private injectFactory (valueProvider: FactoryProvider): t {return valueProvider.useFactory () } private getInjectedParams (target: Type) {const argTypes = Reflect.getMetadata (REFLECT_PARAMS, target) as (| InjectableParam | undefined) []; if (argTypes = undefined) {return [];} return argTypes.map ((argType, index) = > {/ / The reflect-metadata API fails on circular dependencies, and will return undefined / / for the argument instead. If (argType = undefined) {throw new Error (`Injection error. Recursive dependency detected in constructor for type ${target.name} with parameter at index ${index} `);} const overrideToken = getInjectionToken (target, index); const actualToken = overrideToken = undefined? ArgType: overrideToken; let provider = this.providers.get (actualToken); return this.injectWithProvider (actualToken, provider);});} private getTokenName (token: Token) {return token instanceof InjectionToken? Token.injectionIdentifier: token.name;}}
Finally, let's briefly test the IoC container we developed earlier. The specific test code is as follows:
/ / container.test.ts import {Container} from ". / container"; import {Injectable} from ". / injectable"; import {Inject} from ". / inject"; import {InjectionToken} from ". / provider"; const API_URL = new InjectionToken ("apiUrl"); @ Injectable () class HttpClient {} @ Injectable () class HttpService {constructor (private httpClient: HttpClient, @ Inject (API_URL) private apiUrl: string) {} const container = new Container () Container.addProvider ({provide: API_URL, useValue: "https://www.semlinker.com/",}); container.addProvider ({provide: HttpClient, useClass: HttpClient}); container.addProvider ({provide: HttpService, useClass: HttpService}); const httpService = container.inject (HttpService); console.dir (httpService)
After the above code runs successfully, the console outputs the following results:
HttpService {httpClient: HttpClient {}, apiUrl: 'https://www.semlinker.com/'}
Obviously, this result is exactly what we expected, which means that our IoC container is working properly. Of course, in the actual project, a mature IoC container also has a lot of things to consider, if the partner wants to use in the project, it is recommended to consider using the InversifyJS library.
At this point, the study on "how to use TypeScript to implement an IoC container" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.