📜 ⬆️ ⬇️

Angular 2 and dependency injection

The second version of the angular is getting closer to the release and more and more people are becoming interested in it. So far, the information on the framework is not so much, especially in Russian, and more and more often questions arise on some topics.


One of the topics that causes a lot of questions is dependency injection. Some people have not come across this technology. Others do not fully understand how it works within the framework of Angular 2, as they are accustomed to other implementations that occur in other frameworks.


And the answer lies in the fact that the DI in the second angular is indeed somewhat different from the others, and this is primarily due to the general approach and philosophy of the 2nd version. It lies in the fact that the entities from which the entire application is built are components . The service layer, the router, the dependency injection system are secondary and they make sense only within the component. This is a very important point that underlies the understanding of the architecture of the new framework.


Introduction


This is a retelling of 2 pages from the office. documentation regarding dependency injection in Angular 2: this one and this one .


Why typescript

In the article I will use Typescript. Why?
The framework itself is written in Typescript, and the information on the Angular2 + Typescript bundle is the most.
The syntax-based Typescript code is a fresh implementation of the ES standard, additional typing, and some minor features. However, applications can be written on both Javascript and Dart. In the JS version, you can not use ES6 + syntax, however, the brevity and clarity of the code is lost. And if you configure Babel to support fresh features, then syntactically everything will be very similar to the TS code: classes, annotations / decorators, etc. Well, only without the types, so dependency injection will look a little different.


Dependency problem


Imagine that we are writing a kind of abstract application, dividing the code into small logical pieces (to avoid confusion with the angular language, I will not call them "components," let it be just the service classes that contain business logic).


export class Engine { public cylinders = 4; // default } export class Tires { public make = 'Flintstone'; public model = 'Square'; } export class Car { public engine: Engine; public tires: Tires; constructor() { this.engine = new Engine(); this.tires = new Tires(); } drive() {} } 

Of course, there is no logic at all, but for illustration it is quite suitable.


So what's the problem? At the moment, Car strictly dependent on 2 services that are manually created in its constructor. From the point of view of the consumer of the Car service, this is good, because the Car addiction itself took care of its dependencies. But, if we, for example, want to make sure that the required parameter is passed to the Engine constructor, then the code of the Car itself will have to be changed:


 export class Engine2 { constructor(public cylinders: number) { } } export class Car { public engine: Engine; public tires: Tires; constructor() { this.engine = new Engine2(8); this.tires = new Tires(); } } 

Constructors in TS
 //  ,          //       : export class Engine2 { public cylinders constructor(cylinders: number) { this.cylinders = cylinders } } 

Therefore, creating instances of dependencies in the consumer is not so good.


Rewrite the code so that instances of the Car dependencies are transmitted from the outside:


 export class Car { constructor(public engine: Engine, public tires: Tires) { } } 

Already better. The code of the service itself has decreased, and the service itself has become more flexible. It is easier to test and configure:


 class MockEngine extends Engine { cylinders = 8; } class MockTires extends Tires { make = "YokoGoodStone"; } let car = new Car(new Engine(), new Tires()); let supercar = new Car(new Engine2(12), new Tires()); var mockCar = new Car(new MockEngine(), new MockTires()); 

However, now the problems start at the consumer of the Car service: you need to create not only the service itself, but all its dependencies, and then transfer the instances of the created service dependencies to the Car designer.


And with each new component and each new dependency, it is becoming more and more difficult to create instances of services. You can, of course, make a factory into which all the logic for creating a Car service is put into:


 export class CarFactory { createCar() { let car = new Car(this.createEngine(), this.createTires()); car.description = 'Factory'; return car; } createEngine() { return new Engine(); } createTires() { return new Tires(); } } 

But the problems will not become much less: we will need to manually maintain the factory up to date when the Car dependencies change.


On the road to implementation


How can I improve the code? Every consumer knows what services he needs. But in order to reduce the connectivity of the system, the consumer should not create them himself. You can create a singleton class in which the instances of all our services are created and stored. In this class, we determine how to create the necessary services, and you can receive them, for example, by a certain key. Then in the services it will be enough just to somehow get a copy of such a singleton, and from it already get ready-made dependency instances. Such a pattern is called a ServiceLocator. This is one type of inversion control. Then the code would look something like this:


 import {ServiceLocator} from 'service-locator.ts'; // ... let computer = ServiceLocator.instance.getService(Car) //      

On the one hand, we got rid of the rigid connection of the consumer and his dependencies: all services are created outside of consumers. But on the other hand, now consumers are now tightly connected with a service locator: every consumer should know where the service locator instance is located. Well, the creation of services is still manual.


I would like to be able to simply somehow indicate in the consumer his dependencies and the variable in which the dependency instance will be placed, and make the creation and implementation of the services themselves automatic.


This is what DI frameworks do. They manage the life cycle of the implemented dependencies, track the places where these dependencies are required and implement them, i.e. transfer to the consumer the created dependency instance that the consumer has requested. A hard dependence on the service locator disappears from consumers: now the DI framework works with the locator.


The essence of the work is approximately as follows:



And depending on the DI framework, these items will look different in the code.


Angulyar number 1


For a better understanding of the device of the second version of this framework, in particular, DI, I would like to describe a little how the first part works.


The life cycle of an application consists of several stages. I would like to highlight 2 stages:



At the top level are the modules. A module, in essence, is simply an object in which various parts of an application can be registered and stored: services, controllers, directives, filters. Also, the module can have config- and run-kolbek, which will be launched at the appropriate stages of the application.


So, how dependency injection in the first version looks like:


Code
  // - function factory() { var privateField = 2; return { publicField: 'public', publicMethod: function (arg) { return arg * privateField; } }; } var module = angular.module('foo', []); //   //     'MyService'    //   ,  ,   - (2- )      module.factory('MyService', factory); //     'MyController' //            module.controller('MyController', function (MyService) { console.log(MyService.publicMethod(21)); //    }) 

Yes, there are a lot of nuances. For example, in the first angulyar there are already 5 different types of services , so that you can register a service in different ways. When minifying code, function arguments may change, so it’s better to use a different syntax for declaring dependencies ...


But I do not want to go deep into the jungle of the first angular, I will write only the main points:



Angular 2: new way


The second version of the angular was announced as a new framework written from scratch, which took into account all the errors of the first part. As far as using the 2nd version, I got exactly this impression. Unnecessary entities and concepts have disappeared. What remains is only better and more convenient, and innovations fit well and look logical.


The first angular was, in fact, simply a set of useful techniques, techniques, and patterns glued together with DI. But its separate parts were somehow in themselves, were slightly fragmented. There was no single concept.



As a result, the structure of the application could be completely different. But instead of freedom, it usually meant blending concepts.


Component approach


What is a component in Angular 2? This is just a class with certain metadata and an associated presentation layer (template). To make a component of a class, you need to add to it the most specific metadata. The easiest way is to wrap it in the @Component decorator, which links the view with its ViewModel (that is, by the class itself). And from the point of view of type hierarchy, a component is a special case of a directive (which is defined using the @Directive decorator), which has a template:


 @Component({ selector: 'app', template: `<h1>Hello, {{ greetings }}</h1>` }) export class AppComponent { greetings: string = 'World'; } 

The decorator must pass an object that must contain at least 2 required fields: selector and template .


The selector field contains a string that will be used as a css selector to search for a component in the DOM. You can pass any valid selector, but most often use a selector tag that is not included in the standard set of HTML tags. Thus, custom tags are created.


The template field contains a template string that replaces the contents of the DOM element found by the selector. Instead of a string with a template, you can pass a string with the path to the template file (only the field will be called templateUrl ). You can read more about the syntax of templates for the docks page or its Russian translation .


Component Hierarchy


What was wrong with the first angulyar? There was a hierarchy of scopes, but the service layer was common to all. The services were set up once and for all before the launch of the application, and they were also singletones.


There were also problems with routers. The original was rather poor, did not allow to create a normal hierarchy. The UI-router was richer in features, allowed the use of multiple views, knew how to build a hierarchy of states.
But the main problem of both routers was that the whole hierarchy of paths was absolutely in no way connected with the hierarchy of scopes and was extremely non-flexible.


What did the second version do? At the heart of the second angulyar, as I said, are the components. The entire application consists only of components that form a tree-like hierarchical structure. The root component is loaded using the bootstrap function on the HTML page (if the browser is used as a target platform). All other components are placed inside the root and form a tree of components.


How to make so that, on the one hand, each component could be as independent as possible, reusable and self-sufficient, while avoiding duplication of code?
To ensure the independence of the component, it has metadata that allows you to fully describe everything that is needed for this component to work: setting up routing, a list of used directives, pipes and services. In order not to be connected through the service layer, each component now has its own router and its own injector. And they, in turn, also form a hierarchy, which is always associated with the hierarchy of components.


This is what distinguishes DI in Angular2 from other DI frameworks: in an angular, an application does not have one injector, each component can have its own injector


Deploy dependencies in Angular2


What does dependency injection in the second angular look like? Services are now implemented by type. Implementation usually takes place in the user’s constructor.


Services


The service in Angular 2 is a simple class.


 interface User { username: string; email: string; } export class UserService { getCurrent(): User { return { username: 'Admin', email: 'admin@example.com' }; } } 

Registration of services


To be able to implement the service, you first need to register it. We do not need to manually create the injector, the angular itself creates a global injector when the bootstrap function is called:


 bootstrap(AppComponent); 

The second argument can be an array containing the providers. So one way to make a service available is to add its class to the list:


 bootstrap(AppComponent, [UserService]); 

This code will make our service available for the entire application. However, doing so is not always good. Framework developers advise to register only system providers in this place, and only if they are needed in the entire system. For example, providers of a router, forms and Http-services.


The second way to register a service is to add it to the component metadata in the providers field:


 import {Component} from 'angular2/core'; import {bootstrap} from 'angular2/platform/browser'; @Component({ selector: 'app', providers: [UserService], template: `<h1>App</h1>`, }) export class AppComponent { } bootstrap(AppComponent); 

The introduction of services in the component


The easiest way to implement a service is through a constructor. Since TypeScript supports types, it suffices to write this:


 @Component({ selector: 'app', providers: [UserService], template: ` <h1>App</h1> Username: {{ user.username }} <br> Email: {{ user.email }} `, }) export class AppComponent { user: User; constructor(userService: UserService) { this.user = userService.getCurrent(); } } bootstrap(AppComponent); 

And that's it! If the UserService was registered, then the angular will inject the necessary instance into the constructor argument.


The introduction of services in services


For the service to be able to inject dependencies itself, you need to wrap it with @Injectable decorator. Developers also recommend adding this decorator in general for any services, since you never know whether any dependencies within the service will ever be needed. So follow their advice.


 import {Injectable} from 'angular2/core'; @Injectable() //   export class Logger { logs: string[] = []; log(message: string) { this.logs.push(message); console.log(message); } } @Injectable() //   export class UserService { constructor(private _logger: Logger) {} //        getCurrent() { this._logger.log(' ...'); return { username: 'Admin', email: 'admin@example.com' }; } } 

Now you need to remember to register the service Logger , otherwise the angular will generate an error:


 EXCEPTION: No provider for Logger! (AppComponent -> UserService -> Logger) 

So add the Logger to the list of component providers:


 providers: [UserService, Logger], 

Optional dependencies


If the service being introduced is not required, add the @Optional annotation:


 import {Optional, Injectable} from 'angular2/core'; @Injectable() //   export class UserService { constructor(@Optional() private _logger: Logger) {} //        getCurrent() { this._logger.log(' ...'); return { username: 'Admin', email: 'admin@example.com' }; } } 

Now, even if you forget to register the Logger , errors will not occur.


Providers


The provider provides a specific version of the service being implemented in runtime. In fact, we always register not the service itself, but its provider. Just in most cases they are the same.
As part of the framework is a class Provider . He describes exactly how the injector should instantiate the dependency.


When we add a service class to the list of providers (component or bootstrap function), in practice this means the following:


 [Logger], //        [new Provider(Logger, {useClass: Logger})], //  ,   provide [provide(Logger, {useClass: Logger})], 

Both the Provider class constructor and the provide function take 2 arguments:



In fact, when I said that the introduction is of a type, I did not say the whole truth. The token over which an injection can occur can be not only a class, but more on that a little later.


Alternative Service Providers


Suppose we want to use an instance of the class BetterLogger as a service instead of the Logger class. There is no need to search and change the dependence of the Logger on BetterLogger throughout the application, it is enough to register the provider for the Logger with the useClass option:


 [provide(Logger, {useClass: BetterLogger})] 

Even if the alternative class has some kind of dependency, which the original service does not have:


 @Injectable() class EvenBetterLogger { logs:string[] = []; constructor(private _timeService: TimeService) { } log(message: string) { message = `${this._timeService.getTime()}: ${message}`; console.log(message); this.logs.push(message); } } 

We can still use it just the same, we just need to register the necessary dependencies:


 [ TimeService, provide(Logger, {useClass: EvenBetterLogger}) ] 

Aliases providers


Suppose we have some old component that depends on the old service of the OldLogger logger. This service has the same interface as the new logger NewLogger . But for some reason, we cannot change that old component. So we want to use the new one instead of the old logger. If we try to do this:


 [ NewLogger, provide(OldLogger, {useClass: NewLogger}) ] 

That is not what we wanted: 2 instances of a new logger will be created. One will be used where the old is being introduced, the other is where the new logger is being introduced. To create only 1 instance of the new logger, which would be used everywhere, register the provider with the useExisting option:


 [ NewLogger, provide(OldLogger, {useExisting: NewLogger}) ] 

Value providers


Sometimes it’s easier not to create a separate class to replace the service provider with it, but simply to use a ready-made value. For example:


 //   ,     ,    Logger let silentLogger = { logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'], log: () => {} } 

To use a ready-made object, register a provider with the useValue option:


 [provide(Logger, {useValue: silentLogger})] 

Provider Factory / Factory Provider


Sometimes you need to register your provider dynamically, using information that is not available from the very beginning. For example, this information can be obtained from a session and be different from time to time. Also assume that the service being deployed does not have independent access to this information.
In such cases, use the provider-factory / factory provider.


Let us have a certain service BookService , which as well as EvenBetterLogger needs information from another service. Suppose we want to check if a user is authorized using data from AuthService . But, unlike EvenBetterLogger we cannot implement a service directly, i.e. in this case, the BookService does not have access to the AuthService . Services look like this:


 @Injectable() export class AuthService { isLoggedIn: boolean = false; } @Injectable() export class BookService { books: any[]; // ,   extraBooks: any[]; // ,     constructor(private _logger: Logger, private _isLoggedIn: boolean) {} getBooks() { if (this._isLoggedIn) { this._logger.log(' '); return [...this.books, ...this.extraBooks]; } this._logger.log(' '); return this.books; } } 

Logger , boolean-.
- BookService , :


 let bookServiceFactory = (logger: Logger, authService: AuthService) => { return new BookService(logger, authService.isLoggedIn); } 

, , useFactory , deps — :


 [provide(BookService, {useFactory: bookServiceFactory, deps: [Logger, AuthService]}) 


- , , . , . , :


 let logger: Logger = this._injector.get(Logger); 

, - :


 constructor(private _logger: Logger) {} 

.



? , , , ..


, -, . , , - :


 export interface Config { apiEndpoint: string, title: string } export const CONFIG: Config = { apiEndpoint: 'api.heroes.com', title: 'Dependency Injection' }; 

, . :


 // FAIL [provide(Config, {useValue: CONFIG})] // FAIL constructor(private _config: Config) {} 

: .
, Java C# ( DI- ), . . , JavaScript. , interface — TypeScript, . , .


Solution to the problem


OpaqueToken , - :


 import {OpaqueToken} from 'angular2/core'; export let APP_CONFIG = new OpaqueToken('app.config'); 

OpaqueToken , .
:


 providers: [provide(APP_CONFIG, {useValue: CONFIG})] 

, @Inject :


 constructor(@Inject(APP_CONFIG) private _config: Config) {} 

, , .


, :


 [provide('Congig', {useValue: CONFIG})] //... constructor(@Inject('Config') private _config: Config) {} 


, Angular2- — . . .


? , -, , . , - , . , - . , providers .


, . - - . , - . . , : , . .


, . . , , .


? providers , . , bootstrap .
providers , Injector.resolveAndCreate([...]) , . parent , . , . , .


, :


Code
 import {bootstrap} from 'angular2/platform/browser'; import {Injectable, Component} from 'angular2/core'; @Injectable() class LoggerA { logs: string[] = []; log(message: string) { this.logs.push(message); console.log('Logger a: ' + message); } } @Injectable() class LoggerB { logs: string[] = []; log(message: string) { this.logs.push(message); console.log('Logger b: ' + message); } } @Component({ selector: 'child', providers: [LoggerA], template: ` <div> <h4>Child</h4> <button (click)="update()">Update</button> <p>Logs:</p> <strong>LogA: <pre>{{ logA.logs | json }}</pre></strong> <strong>LogB: <pre>{{ logB.logs | json }}</pre></strong> </div>` }) export class ChildComponent { constructor(public logA: LoggerA, public logB: LoggerB) {} update() { this.logA.log('Child: A'); this.logB.log('Child: B'); } } @Component({ selector: 'app', providers: [LoggerA, LoggerB], directives: [ChildComponent], template: ` <div> <div style="display: inline-block; vertical-align: top;"> <h3>App</h3> <button (click)="update()">Update</button> <p>Logs:</p> <strong>LogA: <pre>{{ logA.logs | json }}</pre></strong> <strong>LogB: <pre>{{ logB.logs | json }}</pre></strong> </div> <div style="display: inline-block; vertical-align: top;"> <child></child> </div> </div>` }) export class AppComponent { constructor(public logA: LoggerA, public logB: LoggerB) {} update() { this.logA.log('App: A'); this.logB.log('App: B'); } } bootstrap(AppComponent); 

http://plnkr.co/edit/nbpmh3wb5g34WetQ3AAE?p=preview


2 2 . 2 ( LoggerA LoggerB ), — LoggerA . Update , LogB , , LoggerB , . LoggerA . , — .


, Angular2 ? 1- . , .


findings



:



')

Source: https://habr.com/ru/post/281449/


All Articles