Переопределение зависимостей в иерархии инжекторов Angular
Фреймворк Angular упрощает определение направления зависимостей потоков в приложении, тем самым упрощая отладку.
Angular позволяет использовать зависимости, предоставленные через инжектор родительского компонента, среди его дочерних компонентов, внедряя их в конструкторы дочерних компонентов.
Чтобы лучше понять это, давайте рассмотрим практический подход к внедрению зависимостей от родителей к детям.
Чтобы следовать этому руководству, у вас должны быть:
- Node.js v10.x
- Знание Angular
- Знание TypeScript
Начало
Выполните следующую команду, чтобы создать новое приложение Angular:
ng new phone-book
CD в каталог телефонной книги и выполните следующую команду в терминале, чтобы создать модуль для правильной структуры нашего приложения:
ng generate module contacts
Создаем app/contacts/contact.model.ts
//app/contacts/contact.model.ts
export interface Contact {
id: number;
name: string;
phone_no: string;
}
В приведенном выше фрагменте кода используется интерфейс TypeScript. Он создает модель для проверки данных, которые будут возвращены службой продукта.
Выполните следующую команду, чтобы создать службу контактов:
ng generate service contacts/contact
Обновляем contact.service.ts
//app/contacts/contact.service.ts
import { Injectable } from '@angular/core';
import { Contact } from './contact.model';
@Injectable({
providedIn: 'root'
})
export class ContactService {
constructor() { }
getContacts(): Contact[] {
return [
{ id: 1, name: 'Peter', phone_no: '09033940948' },
{ id: 2, name: 'Sam', phone_no: '07033945948'},
{ id: 3, name: 'Bryce', phone_no: '08033740948' },
{ id: 4, name: 'Lee', phone_no: '090339409321' },
{ id: 5, name: 'Albert', phone_no: '09066894948' }
];
}
}
Свойство providedIn
создает поставщик для службы. В этом случае в providedIn: 'root'
указано, что Angular должен предоставлять услугу в корневом инжекторе (т. е. сделать ее доступной во всем приложении).
Теперь ContactService
можно ввести в любом месте нашего приложения.
Обновление contact-list.component.ts
//app/contacts/contact-list.component.ts
import { Component, OnInit } from '@angular/core';
import { Contact } from '../contact.model';
import { ContactService } from '../contact.service';
@Component({
selector: 'app-contact-list',
templateUrl: './contact-list.component.html',
styleUrls: ['./contact-list.component.css'],
providers: [ContactService]
})
export class ContactListComponent implements OnInit {
contacts: Contact[];
constructor(private contactService: ContactService) { }
ngOnInit(): void {
this.contacts = this.contactService.getContacts();
}
}
Приведенный выше фрагмент создает экземпляр частного свойства ContactService
с использованием ключевого слова new в конструкторе компонента, вызывая метод getContacts
внутри contactService
метода ngOnInit
и присваивает возвращаемое значение свойству contacts
.
Обновление contact-list.component.html
//app/contacts/contact-list.component.html
<h3>My Contact List</h3>
<ul>
<li *ngFor="let contact of contacts">
{{contact.name}}
</li>
</ul>
<app-recent-contact></app-recent-contact>
Приведенный выше фрагмент использует директиву ngFor
для отображения списка контактов, а также принимает RecentContact
в качестве своего прямого дочернего компонента.
Обновление app/contacts/recent-contact.component.ts
//app/contacts/recent-contact.component.ts
import { Component, OnInit } from '@angular/core';
import { ContactService } from '../contact.service';
import { Contact } from '../contact.model';
@Component({
selector: 'app-recent-contact',
templateUrl: './recent-contact.component.html',
styleUrls: ['./recent-contact.component.css']
})
export class RecentContactComponent implements OnInit {
contacts: Contact[];
constructor(private contactService: ContactService) { }
ngOnInit(): void {
this.contacts = this.contactService.getContacts();
}
}
RecentContactComponent
, будучи прямым дочерним компонентом ContactListComponent
, имеет доступ к предоставленной зависимости ContactListComponent
, даже не предоставляя ее через свойство providers
декоратора @component
.
Обновление app/contacts/recent-contact.component.html
//app/contacts/recent-contact.component.html
<h3>My Recent Contacts</h3>
<ul>
<li *ngFor="let contact of contacts | slice:0:3">
{{contact.name}}
</li>
</ul>
Здесь мы применяем SlicePipe
к оператору ngFor
, чтобы отобразить только последние три контакта.
Запустив приложение с помощью команды ng serve
, мы должны иметь список всех контактов и последних контактов, отображаемых в браузере.
Переопределение зависимостей в Angular
Для переопределения зависимостей в Angular требуются два ключевых свойства:
provide
- это указывает на зависимость, которую вы хотите переопределитьuseClass
- это указывает на новую зависимость, которая переопределит существующую зависимость в свойствеproviders
Это имеет простой формат:
@Component({
providers: [
{ provide: Service, useClass : FakeService }
]
})
Когда переопределять зависимости
Бывают случаи, когда вы хотите переопределить разрешение вашей конструкции по умолчанию.
Вот несколько типичных случаев:
- Переопределение поставщика при написании модульного теста
- Добавление уникальной функции в зависимость без внесения в нее изменений
Последнее достигается путем создания новой зависимости из существующей зависимости с помощью ключевого слова extend
.
Давайте рассмотрим практический подход к последнему случаю, создав новую службу RecentContactService
которая будет расширять ContactService
и отфильтровывать данные с помощью метода массива slice
, а не канала в шаблоне.
Обновление recent-contact.service.ts
//app/contacts/recent-contact.service.ts
import { Injectable } from '@angular/core';
import { ContactService } from './contact.service';
import { Contact } from './contact.model';
@Injectable({
providedIn: 'root'
})
export class RecentContactService extends ContactService {
constructor() {
super();
}
getContacts(): Contact[] {
return super.getContacts().slice(Math.max(super.getContacts().length - 3, 0))
}
}
Здесь метод getContacts
возвращает последние три элемента из результирующего массива из ContactService
.
Теперь мы можем добавить эту новую услугу (зависимость) providers
к свойствуRecentContactComponent
Обновление recent-contact.component.ts
//app/contact/recent-contact.component.ts
...
import { RecentContactService } from '../recent-contact.service';
@Component({
selector: 'app-recent-contact',
templateUrl: './recent-contact.component.html',
styleUrls: ['./recent-contact.component.css'],
providers: [{
provide: ContactService,
useClass: RecentContactService
}]
})
export class RecentContactComponent implements OnInit {
contacts: Contact[];
constructor(private recentContactService: RecentContactService) {}
ngOnInit(): void {
this.contacts = this.recentContactService.getContacts();
}
}
Здесь свойство useClass
позволяет Angular переопределить нашу предыдущую зависимость (ContactService
) нашей новой зависимостью (RecentContactService
).
Переопределение зависимостей строковыми значениями
В случае, когда зависимость, которую мы хотим переопределить, является строкой, синтаксис useValue
будет удобен.
Скажем, у нашего приложения есть файл DBconfig
:
//app/db.config.ts
export interface DBConfig {
name: string;
version: number;
}
export const databaseSettings: DBConfig = {
name: 'MongoDB',
version: 2.0
};
Чтобы сделать конфигурации базы данных доступными в нашем приложении, нам необходимо предоставить объект InjectionToken
:
//app/db.config.ts
import { InjectionToken } from '@angular/core';
export const DB_CONFIG = new InjectionToken<DBConfig>('db.config');
...
Теперь наш компонент может получить доступ к конфигурации базы данных с помощью декоратора @Inject
:
//app/app.component.ts
import { Component, Inject } from '@angular/core';
import { DB_CONFIG, databaseSettings, DBConfig } from './db.config';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
providers: [{
provide: DB_CONFIG,
useValue: databaseSettings
}]
})
export class AppComponent {
name: string;
version: number;
constructor(@Inject(DB_CONFIG) config: DBConfig) {
this.name = config.name;
this.version = config.version;
}
}
//app/app.component.html
<h4>This app uses {{name}} Database v. {{version}}</h4>
<app-contact-list></app-contact-list>
Переопределение зависимостей во время выполнения
Ключевое слово useFactory
позволяет Angular решить, какую зависимость вводить в конструкцию во время выполнения:
import { RecentContactService } from './recent-contact.service';
import { ContactService } from './contact.service';
export function contactFactory(isRecent: boolean) {
return () => {
if (isRecent) {
return new RecentContactService();
}
return new ContactService();
};
}
Приведенный выше фрагмент - это фабрика, которая возвращает либо RecentContactService
либо ContactService
в зависимости от логического условия.
Теперь мы можем изменить свойство поставщиков RecentContactComponent
следующим образом:
...
providers: [{
provide: ContactService,
useClass: contactFactory(true)
}]
...
Благодаря этому мы можем отменять любую зависимость столько раз, сколько это возможно. Это еще проще, поскольку нам нужно только добавить новые зависимости к contactFactory
, настроить условия и, наконец, настроить свойство useClass
соответствующего компонента новых зависимостей..
Ограничение переопределения зависимостей во время выполнения
Если какая-либо из этих зависимостей вводит другие зависимости в свой конструктор с предыдущей реализацией contactFactory
, Angular выдаст ошибку.
Предполагая, что и RecentContactService
, и ContactService
зависят от зависимости AuthService
, нам придется настроить contactFactory
следующим образом:
export function contactFactory(isRecent: boolean) {
return (auth: AuthService) => {
if (isRecent) {
return new RecentContactService();
}
return new ContactService();
};
}
Затем мы должны добавить зависимость AuthService
к свойству deps
объекта providers
в компоненте Recent Contact
следующим образом:
...
providers: [{
...
deps: [AuthService]
}]
...
Заключение
Работа над крупномасштабным приложением Angular требует хорошего понимания того, как работает внедрение зависимостей. Знание того, как и когда переопределить зависимость в Angular, необходимо при работе с крупномасштабными приложениями Angular, поскольку это помогает избежать ненужного дублирования кода в вашем приложении.
Исходный код этой статьи доступен на GitHub.