Spec-Zone.ru › Angular 2

Внедрение зависимостей

Система внедрения зависимостей Angular создаёт и предоставляет зависимые сервисы «в режиме реального времени».

Внедрение зависимостей — важный шаблон проектирования приложений. Angular имеет собственный фреймворк внедрения зависимостей, и вы не сможете построить приложение Angular без него. Он настолько широко используется, что почти все называют его просто DI.

Эта страница описывает, что такое DI, почему оно так полезно, и как его использовать в приложении Angular.

Содержание

  • Почему внедрение зависимостей?
  • Внедрение зависимостей в Angular
    • Настройка инжектора
    • Регистрация поставщиков в NgModule
    • Регистрация поставщиков в компоненте
    • Когда использовать NgModule вместо компонента приложения
    • Подготовка HeroListComponent для инъекции
    • Неявное создание инжектора
    • Сервисы типа «один экземпляр»
    • Тестирование компонента
    • Когда сервис нуждается в сервисе
    • Почему @Injectable()?
  • Создание и регистрация сервиса регистрации событий
  • Поставщики инжектора
    • Класс Provider и объект-литерал provide
    • Альтернативные поставщики классов
    • Поставщик класса с зависимостями
    • Поставщики классов с псевдонимами
    • Поставщики значений
    • Поставщики-фабрики
  • Маркеры внедрения зависимостей
    • Независимые компоненты, не являющиеся классами
    • OpaqueToken
  • Необязательные зависимости
  • Резюме
  • Приложение: Работа с инжекторами напрямую

Запустите пример.

Почему внедрение зависимостей?

Чтобы понять, почему внедрение зависимостей так важно, рассмотрим пример без него. Представьте, что вы пишете следующий код:

src/app/car/car.ts (без DI)

export class Car {

  public engine: Engine;
  public tires: Tires;
  public description = 'No DI';

  constructor() {
    this.engine = new Engine();
    this.tires = new Tires();
  }

  // Method using the engine and tires
  drive() {
    return `${this.description} car with ` +
      `${this.engine.cylinders} cylinders and ${this.tires.make} tires.`;
  }
}

Класс Car создаёт всё, что ему нужно, внутри своего конструктора. В чём проблема? Проблема в том, что класс Car хрупкий, негибкий и трудно тестируется.

Этот Car нуждается в двигателе и шинах. Вместо того, чтобы запросить их, конструктор Car создаёт собственные копии из очень специфических классов Engine и Tires.

Что, если класс Engine эволюционирует, и его конструктору потребуется параметр? Это сломает класс Car, и он останется сломанным, пока вы не перепишете его в соответствии с this.engine = new Engine(theNewParameter). Параметры конструктора Engine даже не рассматривались, когда вы впервые написали Car. Вы можете даже сейчас не предусмотреть их. Но вам придётся начать этим заниматься, потому что при изменении определения Engine класс Car должен измениться. Это делает Car хрупким.

Что, если вы хотите установить другой бренд шин на свой Car? К сожалению, вы заперты на том бренде, который создаёт класс Tires. Это делает класс Car негибким.

Сейчас каждый новый автомобиль получает свой engine. Он не может совместно использовать engine с другими автомобилями. Хотя это имеет смысл для двигателя автомобиля, наверняка вы можете придумать другие зависимости, которые должны быть общими, такие как беспроводное подключение к сервисному центру производителя. Этот Car лишен гибкости для совместного использования сервисов, которые были созданы ранее для других потребителей.

При написании тестов для Car вы находитесь во власти его скрытых зависимостей. Возможно ли создать новый Engine в тестовой среде? От чего зависит Engine? От чего зависит эта зависимость? Будет ли новый экземпляр Engine делать асинхронный вызов на сервер? Вы определённо не хотите этого во время тестов.

Что, если Car должен подавать звуковой сигнал тревоги, когда давление в шинах низкое? Как вы подтвердите, что он действительно подаёт сигнал тревоги, если вы не можете заменить шины с низким давлением во время теста?

Вы не контролируете скрытые зависимости автомобиля. Когда вы не можете контролировать зависимости, класс становится трудно тестируемым.

Как можно сделать Car более надёжным, гибким и тестируемым?

Это очень просто. Измените конструктор Car на версию с DI:

src/app/car/car.ts (фрагмент с DI)
public description = 'DI';

constructor(public engine: Engine, public tires: Tires) { }
src/app/car/car.ts (фрагмент без DI)
public engine: Engine;
public tires: Tires;
public description = 'No DI';

constructor() {
  this.engine = new Engine();
  this.tires = new Tires();
}

Вы видите, что произошло? Определение зависимостей теперь находится в конструкторе. Класс Car больше не создаёт engine или tires . Он просто их потребляет.

В этом примере используется синтаксис конструктора TypeScript для одновременного объявления параметров и свойств.

Теперь вы можете создать автомобиль, передав двигатель и шины в конструктор.

// Simple car with 4 cylinders and Flintstone tires.
let car = new Car(new Engine(), new Tires());

Как здорово? Определения зависимостей engine и tire отвязаны от класса Car. Вы можете передать любой engine или tires, если они соответствуют общим требованиям API к engine или tires.

Теперь, если кто-то расширит класс Engine, это не проблема Car.

Проблема у потребителя Car. Потребитель должен обновить код создания автомобиля на что-то вроде этого:

class Engine2 {
  constructor(public cylinders: number) { }
}
// Super car with 12 cylinders and Flintstone tires.
let bigCylinders = 12;
let car = new Car(new Engine2(bigCylinders), new Tires());

Существенный момент заключается в том, что классу Car не нужно было изменяться. Вы скоро позаботитесь о проблеме потребителя.

Класс Car теперь намного проще тестировать, потому что вы полностью контролируете его зависимости. Вы можете передать подмены в конструктор, которые будут делать ровно то, что вы хотите, во время каждого теста:

class MockEngine extends Engine { cylinders = 8; }
class MockTires  extends Tires  { make = 'YokoGoodStone'; }

// Test car with 8 cylinders and YokoGoodStone tires.
let car = new Car(new MockEngine(), new MockTires());

Вы только что узнали, что такое внедрение зависимостей.

Это шаблон программирования, в котором класс получает свои зависимости из внешних источников, а не создаёт их сам.

Отлично! Но что с бедным потребителем? Любой, кто хочет Car , теперь должен создать все три части: Car, Engine и Tires. Класс Car сбросил свои проблемы за счёт потребителя. Вам нужно что-то, что позаботится о сборке этих частей.

Вы могли написать огромный класс для этого:

src/app/car/car-factory.ts

import { Engine, Tires, Car } from './car';

// BAD pattern!
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();
  }
}

Сейчас это не так уж и плохо с тремя методами создания. Но поддерживать это будет трудно по мере роста приложения. Эта фабрика превратится в огромную паутину взаимозависимых методов фабрики!

Разве не было бы здорово, если бы вы могли просто перечислить вещи, которые вы хотите построить, не определяя, какая зависимость вставляется в какую?

Вот где вступает в игру фреймворк внедрения зависимостей. Представьте, что фреймворк имеет нечто, называемое инжектором. Вы регистрируете некоторые классы в этом инжекторе, и он определяет, как их создать.

Когда вам нужен Car, вы просто просите инжектор получить его для вас, и всё готово.

let car = injector.get(Car);

Все выигрывают. Класс Car ничего не знает о создании Engine или Tires. Потребитель ничего не знает о создании Car. У вас нет огромного класса фабрики, за которым нужно следить. И Car , и потребитель просто запрашивают то, что им нужно, а инжектор предоставляет.

Вот что такое фреймворк внедрения зависимостей.

Теперь, когда вы знаете, что такое внедрение зависимостей и понимаете его преимущества, переходите к тому, как оно реализовано в Angular.

Внедрение зависимостей в Angular

Angular поставляется со своим собственным фреймворком внедрения зависимостей. Этот фреймворк также может использоваться как автономный модуль другими приложениями и фреймворками.

Чтобы увидеть, что он может сделать при создании компонентов в Angular, начните с упрощенной версии HeroesComponent из Тура по героям.

src/app/heroes/heroes.component.ts
import { Component }          from '@angular/core';

@Component({
  selector: 'my-heroes',
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }
src/app/heroes/hero-list.component.ts
import { Component }   from '@angular/core';

import { HEROES }      from './mock-heroes';

@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes = HEROES;
}
src/app/heroes/hero.ts
export class Hero {
  id: number;
  name: string;
  isSecret = false;
}
src/app/heroes/mock-heroes.ts
import { Hero } from './hero';

export var HEROES: Hero[] = [
  { id: 11, isSecret: false, name: 'Mr. Nice' },
  { id: 12, isSecret: false, name: 'Narco' },
  { id: 13, isSecret: false, name: 'Bombasto' },
  { id: 14, isSecret: false, name: 'Celeritas' },
  { id: 15, isSecret: false, name: 'Magneta' },
  { id: 16, isSecret: false, name: 'RubberMan' },
  { id: 17, isSecret: false, name: 'Dynama' },
  { id: 18, isSecret: true,  name: 'Dr IQ' },
  { id: 19, isSecret: true,  name: 'Magma' },
  { id: 20, isSecret: true,  name: 'Tornado' }
];

Компонент HeroesComponent является корневым компонентом области функциональности Heroes. Он управляет всеми дочерними компонентами этой области. В этой упрощенной версии есть только один дочерний элемент, HeroListComponent, который отображает список героев.

Сейчас HeroListComponent получает героев из HEROES, внутренней памяти, определённой в другом файле. Это может быть достаточно на ранних этапах разработки, но это далеко от идеала. Как только вы попытаетесь протестировать этот компонент или захотите получить данные героев с удалённого сервера, вам придётся изменить реализацию heroes и исправить все остальные места использования данных симуляции HEROES.

Лучше создать сервис, который скрывает то, как приложение получает данные героев.

Учитывая, что сервис является отдельной единицей, следует написать код сервиса в отдельном файле.

См. эти замечания для получения подробностей.

Следующий HeroService экспонирует метод getHeroes, который возвращает те же данные симуляции, что и раньше, но ни один из его потребителей не должен об этом знать.

src/app/heroes/hero.service.ts

import { Injectable } from '@angular/core';

import { HEROES }     from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes() { return HEROES; }
}

Декоратор @Injectable() над классом сервиса описывается ниже.

Конечно, это не реальная служба. Если приложение фактически получало данные с удаленного сервера, API должно было бы быть асинхронным, возможно, возвращая Promise. Вам также пришлось бы переписать способ, которым компоненты потребляют службу. Это важно в общем случае, но не в этом примере.

Служба — это всего лишь класс в Angular. Она остаётся всего лишь классом, пока вы не зарегистрируете её с помощью инжектора Angular.

Настройка инжектора

Вам не нужно создавать инжектор Angular. Angular создаёт инжектор для всего приложения во время процесса загрузки.

src/main.ts (bootstrap)

platformBrowserDynamic().bootstrapModule(AppModule);

Вам необходимо настроить инжектор, зарегистрировав провайдеры, которые создают службы, необходимые приложению. Это руководство объясняет, что такое провайдеры позже.

Вы можете зарегистрировать провайдер как в NgModule, так и в компонентах приложения.

Регистрация провайдеров в NgModule

Вот AppModule, который регистрирует два провайдера, UserService и провайдер APP_CONFIG, в массиве providers.

src/app/app.module.ts (excerpt)

@NgModule({
  imports: [
    BrowserModule
  ],
  declarations: [
    AppComponent,
    CarComponent,
    HeroesComponent,
    /* . . . */
  ],
  providers: [
    UserService,
    { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

Поскольку HeroService используется только внутри HeroesComponent и его подкомпонентов, уровень HeroesComponent является идеальным местом для его регистрации.

Регистрация провайдеров в компоненте

Вот переработанный HeroesComponent, который регистрирует HeroService в массиве providers.

src/app/heroes/heroes.component.ts

import { Component }          from '@angular/core';

import { HeroService }        from './hero.service';

@Component({
  selector: 'my-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }

Когда использовать NgModule по сравнению с компонентом приложения

С одной стороны, провайдер в NgModule регистрируется в корневом инжекторе. Это означает, что каждый провайдер, зарегистрированный в NgModule, будет доступен во всём приложении.

С другой стороны, провайдер, зарегистрированный в компоненте приложения, доступен только в этом компоненте и всех его дочерних компонентах.

Здесь служба APP_CONFIG должна быть доступна во всём приложении, поэтому она регистрируется в массиве AppModule @NgModule providers. Но поскольку HeroService используется только в области функциональности Heroes и нигде больше, имеет смысл зарегистрировать её в HeroesComponent.

Также см. "Нужно ли добавлять глобальные провайдеры в корневой AppModule или корневой AppComponent?" в FAQ по NgModule.

Подготовка HeroListComponent к инъекции

HeroListComponent должен получать героев из введённого HeroService. В соответствии с принципом инъекции зависимостей, компонент должен запросить службу в своём конструкторе, как обсуждалось ранее ранее. Это небольшое изменение:

src/app/heroes/hero-list.component (с DI)
import { Component }   from '@angular/core';

import { Hero }        from './hero';
import { HeroService } from './hero.service';

@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes: Hero[];

  constructor(heroService: HeroService) {
    this.heroes = heroService.getHeroes();
  }
}
src/app/heroes/hero-list.component (без DI)
import { Component }   from '@angular/core';

import { HEROES }      from './mock-heroes';

@Component({
  selector: 'hero-list',
  template: `
  <div *ngFor="let hero of heroes">
    {{hero.id}} - {{hero.name}}
  </div>
  `
})
export class HeroListComponent {
  heroes = HEROES;
}

Фокус на конструкторе

Добавление параметра в конструктор — это не всё, что происходит здесь.

constructor(heroService: HeroService) {
  this.heroes = heroService.getHeroes();
}

Обратите внимание, что параметр конструктора имеет тип HeroService, а класс HeroListComponent имеет декоратор @Component (прокрутите вверх, чтобы подтвердить этот факт). Также вспомните, что родительский компонент (HeroesComponent) имеет информацию providers для HeroService.

Тип параметра конструктора, декоратор @Component, и информация родителя providers объединяются, чтобы сказать инжектору Angular, что нужно инжектировать экземпляр HeroService всякий раз, когда он создаёт новый HeroListComponent.

Неявное создание инжектора

Вы видели, как использовать инжектор для создания нового Car ранее в этом руководстве. Вы могли создать такой инжектор явно:

  injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]);
  let car = injector.get(Car);

Вы не найдёте такого кода в Туре героев или в других примерах документации. Вы могли написать код, который явным образом создаёт инжектор, если бы это было необходимо, но это не всегда лучший выбор. Angular заботится о создании и вызове инжекторов, когда создаёт компоненты для вас — будь то через разметку HTML, как в <hero-list></hero-list>, или после перехода к компоненту с помощью маршрутизатора. Если вы позволите Angular сделать свою работу, вы получите преимущества автоматической инъекции зависимостей.

Службы-синглтоны

Зависимости являются синглтонами в области инжектора. В примере этого руководства один экземпляр HeroService используется совместно между HeroesComponent и его дочерними элементами HeroListComponent.

Однако, система инъекции зависимостей Angular — это иерархическая система инъекции, что означает, что вложенные инжекторы могут создавать свои собственные экземпляры службы. Для получения дополнительной информации см. Иерархические инжекторы.

Тестирование компонента

Ранее вы видели, что проектирование класса для инъекции зависимостей делает класс проще для тестирования. Перечисление зависимостей в качестве параметров конструктора может быть всё, что вам нужно для эффективного тестирования частей приложения.

Например, вы можете создать новый HeroListComponent с эмулирующей службой, которую можно изменять во время тестирования:

let expectedHeroes = [{name: 'A'}, {name: 'B'}]
let mockService = <HeroService> {getHeroes: () => expectedHeroes }

it('should have heroes when HeroListComponent created', () => {
  let hlc = new HeroListComponent(mockService);
  expect(hlc.heroes.length).toEqual(expectedHeroes.length);
});

Узнайте больше в Тестировании.

Когда служба нуждается в службе

Служба HeroService очень простая. У неё нет собственных зависимостей.

Что, если бы у неё была зависимость? Что, если бы она сообщала о своей деятельности через службу регистрации? Вы бы применили ту же схему инъекции конструктора, добавив конструктор, принимающий параметр Logger.

Вот переработка по сравнению с оригиналом.

src/app/heroes/hero.service (v2)
import { Injectable } from '@angular/core';

import { HEROES }     from './mock-heroes';
import { Logger }     from '../logger.service';

@Injectable()
export class HeroService {

  constructor(private logger: Logger) {  }

  getHeroes() {
    this.logger.log('Getting heroes ...');
    return HEROES;
  }
}
src/app/heroes/hero.service (v1)
import { Injectable } from '@angular/core';

import { HEROES }     from './mock-heroes';

@Injectable()
export class HeroService {
  getHeroes() { return HEROES; }
}

Теперь конструктор запрашивает введённый экземпляр Logger и сохраняет его в закрытом свойстве, называемом logger. Вы вызываете это свойство в методе getHeroes() при запросе героев.

Почему @Injectable()?

@Injectable() помечает класс как доступный для инжектора для инициализации. В общем случае инжектор возвращает ошибку при попытке инициализировать класс, который не помечен как @Injectable().

Так уж получилось, что вы могли бы опустить @Injectable() из первой версии HeroService, потому что в ней не было введённых параметров. Но теперь, когда в службе есть зависимость, вы должны её иметь. Это необходимо, потому что Angular требует метаданных параметра конструктора для инъекции Logger.

Рекомендация: добавить @Injectable() к каждому классу службы

Подумайте о добавлении @Injectable() к каждому классу службы, даже к тем, у которых нет зависимостей и, следовательно, которые технически этого не требуют. Вот почему:

  • Будущее: Вам не придётся запоминать @Injectable() при добавлении зависимости позже.
  • Согласованность: Все службы следуют одним правилам, и вы не будете гадать, почему декоратор отсутствует.

Инжекторы также отвечают за инициализацию компонентов, таких как HeroesComponent. Так почему у HeroesComponent нет @Injectable()?

Вы можете добавить его, если хотите. Это не нужно, потому что HeroesComponent уже помечен как @Component, а этот класс декоратора (как @Directive и @Pipe, о которых вы узнаете позже) является подтипом @Injectable(). На самом деле это декораторы @Injectable() определяют класс как цель для инициализации инжектором.

При выполнении инжекторы могут читать метаданные класса в скомпилированном коде JavaScript и использовать информацию о типе параметра конструктора для определения того, что нужно инжектировать.

Не каждый класс JavaScript имеет метаданные. Компилятор TypeScript по умолчанию отбрасывает метаданные. Если опция компилятора emitDecoratorMetadata включена (как должно быть в tsconfig.json ), компилятор добавляет метаданные в сгенерированный JavaScript для каждого класса с хотя бы одним декоратором.

Хотя любой декоратор вызовет этот эффект, пометьте класс службы декоратором @Injectable(), чтобы сделать намерение ясным.

Всегда включайте скобки

Всегда пишите @Injectable(), а не просто @Injectable. Приложение будет выдавать непонятные ошибки, если вы забудете скобки.

Создание и регистрация службы регистрации

Введите регистратор в HeroService в два шага:

  1. Создайте службу регистрации.
  2. Зарегистрируйте её в приложении.

Служба регистрации довольно простая:

src/app/logger.service.ts

import { Injectable } from '@angular/core';

@Injectable()
export class Logger {
  logs: string[] = []; // capture logs for testing

  log(message: string) {
    this.logs.push(message);
    console.log(message);
  }
}

Вам, вероятно, понадобится та же служба регистрации повсюду в вашем приложении, поэтому поместите её в папку проекта app и зарегистрируйте её в массиве providers модуля приложения, AppModule.

src/app/app.module.ts (excerpt)

providers: [Logger]

Если вы забудете зарегистрировать регистратор, Angular выбросит исключение при первом поиске регистратора:

EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)

Это Angular сообщает вам, что инжектор зависимостей не смог найти провайдер для регистратора. Ему был нужен этот провайдер, чтобы создать Logger для инъекции в новый HeroService, который ему нужно было создать и инжектировать в новый HeroListComponent.

Цепочка созданий началась с провайдера Logger. Провайдеры — тема следующего раздела.

Провайдеры инжектора

Провайдер предоставляет конкретную, рабочую версию значения зависимости. Инжектор полагается на провайдеры для создания экземпляров сервисов, которые инжектор вводит в компоненты и другие сервисы.

Вы должны зарегистрировать сервис-провайдер в инжекторе, иначе он не будет знать, как создать сервис.

Ранее вы зарегистрировали сервис Logger в массиве providers метаданных для AppModule следующим образом:

providers: [Logger]

Существует множество способов предоставить нечто, что выглядит и ведет себя как Logger. Класс Logger сам по себе является очевидным и естественным провайдером. Но это не единственный способ.

Вы можете настроить инжектор с альтернативными провайдерами, которые могут предоставить объект, который ведет себя как Logger. Вы можете предоставить замещающий класс. Вы можете предоставить объект-логгер. Вы можете предоставить провайдер, вызывающий функцию фабрики логгера. Любой из этих подходов может быть хорошим выбором в подходящих обстоятельствах.

Важно, чтобы у инжектора был провайдер, к которому он может обратиться, когда ему понадобится Logger.

Класс Provider и литерал объекта provide

Вы записали массив providers следующим образом:

providers: [Logger]

На самом деле это сокращенное выражение для регистрации провайдера, использующего литерал объекта провайдер с двумя свойствами:

[{ provide: Logger, useClass: Logger }]

Первое — это токен, который служит ключом для поиска значения зависимости и регистрации провайдера.

Второе — это объект определения провайдера, который можно рассматривать как рецепт для создания значения зависимости. Существует множество способов создания значений зависимости, так же как и множество способов написания рецепта.

Альтернативные провайдеры классов

Иногда вы попросите другой класс предоставить сервис. Следующий код сообщает инжектору возвращать BetterLogger при запросе Logger.

[{ provide: Logger, useClass: BetterLogger }]

Провайдер класса с зависимостями

Возможно, EvenBetterLogger мог отобразить имя пользователя в сообщении журнала. Этот логгер получает пользователя от введённого UserService, который также введён на уровне приложения.

@Injectable()
class EvenBetterLogger extends Logger {
  constructor(private userService: UserService) { super(); }

  log(message: string) {
    let name = this.userService.user.name;
    super.log(`Message to ${name}: ${message}`);
  }
}

Настройте его как BetterLogger.

[ UserService,
  { provide: Logger, useClass: EvenBetterLogger }]

Провайдеры класса с псевдонимами

Предположим, старый компонент зависит от класса OldLogger.

OldLogger имеет тот же интерфейс, что и NewLogger, но по какой-то причине вы не можете обновить старый компонент, чтобы он использовал его.

Когда старый компонент записывает сообщение с OldLogger, вы хотите, чтобы вместо него обрабатывал его одиночный экземпляр NewLogger.

Инжектор зависимостей должен внедрять этот одиночный экземпляр, когда компонент запрашивает либо новый, либо старый логгер.

OldLogger должен быть псевдонимом для NewLogger.

Вы определенно не хотите иметь два разных экземпляра NewLogger в вашем приложении. К сожалению, именно это вы получите, если попытаетесь присвоить OldLogger псевдоним NewLogger с useClass.

[ NewLogger,
  // Not aliased! Creates two instances of `NewLogger`
  { provide: OldLogger, useClass: NewLogger}]

Решение: использовать опцию useExisting.

[ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger}]

Провайдеры значений

Иногда проще предоставить готовый объект, чем просить инжектор создать его из класса.

// An object in the shape of the logger service
let silentLogger = {
  logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
  log: () => {}
};

Затем вы регистрируете провайдер с опцией useValue, которая заставляет этот объект выполнять роль логгера.

[{ provide: Logger, useValue: silentLogger }]

См. дополнительные примеры useValue в разделах Неклассовые зависимости и OpaqueToken.

Провайдеры-фабрики

Иногда вам нужно динамически создать зависимое значение, основываясь на информации, которая у вас не будет до последнего момента. Возможно, информация изменяется многократно в течение сеанса браузера.

Предположим также, что вводимый сервис не имеет прямого доступа к источнику этой информации.

Эта ситуация требует провайдера-фабрики.

Чтобы проиллюстрировать этот момент, добавьте новое бизнес-требование: HeroService должен скрывать секретных героев от обычных пользователей. Только авторизованные пользователи должны видеть секретных героев.

Как и EvenBetterLogger, HeroService нуждается в факте о пользователе. Ему нужно знать, авторизован ли пользователь для просмотра секретных героев. Эта авторизация может меняться в течение одного сеанса приложения, например, при входе в систему другого пользователя.

В отличие от EvenBetterLogger, вы не можете внедрить UserService в HeroService.

HeroService не будет иметь прямого доступа к информации о пользователе, чтобы решить, кто авторизован, а кто нет.

Вместо этого конструктор HeroService принимает булево значение для управления отображением секретных героев.

src/app/heroes/hero.service.ts (excerpt)

constructor(
  private logger: Logger,
  private isAuthorized: boolean) { }

getHeroes() {
  let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
  this.logger.log(`Getting heroes for ${auth} user.`);
  return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}

Вы можете внедрить Logger, но вы не можете внедрить булево значение isAuthorized. Вам придется взять на себя создание новых экземпляров этого HeroService с помощью провайдера-фабрики.

Провайдер-фабрика нуждается в фабричной функции:

src/app/heroes/hero.service.provider.ts (excerpt)

let heroServiceFactory = (logger: Logger, userService: UserService) => {
  return new HeroService(logger, userService.user.isAuthorized);
};

Хотя HeroService не имеет доступа к UserService, фабричная функция имеет.

Вы внедряете как Logger, так и UserService в провайдер-фабрику и позволяете инжектору передать их в функцию-фабрику:

src/app/heroes/hero.service.provider.ts (excerpt)

export let heroServiceProvider =
  { provide: HeroService,
    useFactory: heroServiceFactory,
    deps: [Logger, UserService]
  };

Поле useFactory сообщает Angular, что провайдер — это фабричная функция, реализация которой — heroServiceFactory.

Свойство deps — это массив токенов провайдеров. Классы Logger и UserService служат токенами для своих собственных провайдеров классов. Инжектор разрешает эти токены и вводит соответствующие сервисы в параметры соответствующей фабричной функции.

Обратите внимание, что вы сохранили провайдера-фабрику в экспортированной переменной heroServiceProvider.

Эта дополнительная операция делает провайдера-фабрику многократно используемой. Вы можете зарегистрировать HeroService с этой переменной, где вам это нужно.

В этом примере вам это нужно только в HeroesComponent, где он заменяет предыдущую регистрацию HeroService в массиве метаданных providers.

src/app/heroes/heroes.component (v3)
import { Component }          from '@angular/core';

import { heroServiceProvider } from './hero.service.provider';

@Component({
  selector: 'my-heroes',
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `,
  providers: [heroServiceProvider]
})
export class HeroesComponent { }
src/app/heroes/heroes.component (v2)
import { Component }          from '@angular/core';

import { HeroService }        from './hero.service';

@Component({
  selector: 'my-heroes',
  providers: [HeroService],
  template: `
  <h2>Heroes</h2>
  <hero-list></hero-list>
  `
})
export class HeroesComponent { }

Токены инъекции зависимостей

Когда вы регистрируете провайдер с инжектором, вы связываете этот провайдер с токеном инъекции зависимостей. Инжектор сохраняет внутреннюю карту токен-провайдер, к которой он обращается, когда запрашивается зависимость. Токен — это ключ к карте.

Во всех предыдущих примерах значение зависимости было экземпляром класса, а тип класса служил своим собственным ключом поиска.

Вот как вы получаете HeroService напрямую от инжектора, передавая тип HeroService в качестве токена:

heroService: HeroService = this.injector.get(HeroService);

У вас есть аналогичная удача, когда вы пишете конструктор, который требует введённой зависимости на основе класса. Когда вы определяете параметр конструктора с типом класса HeroService , Angular знает, что должен внедрить сервис, связанный с токеном класса HeroService:

constructor(heroService: HeroService)

Это особенно удобно, если учесть, что большинство значений зависимостей предоставляются классами.

Независимости от класса

Что, если значение зависимости не является классом? Иногда искомое значение — это строка, функция или объект.

Приложения часто определяют объекты конфигурации с множеством небольших фактов (например, заголовок приложения или адрес веб-API-точки конечного пункта), но эти объекты конфигурации не всегда являются экземплярами класса. Они могут быть литералами объектов, такими как этот:

src/app/app-config.ts (excerpt)

export interface AppConfig {
  apiEndpoint: string;
  title: string;
}

export const HERO_DI_CONFIG: AppConfig = {
  apiEndpoint: 'api.heroes.com',
  title: 'Dependency Injection'
};

Что, если вам нужно сделать этот объект конфигурации доступным для внедрения? Вы знаете, что можете зарегистрировать объект с помощью провайдера значений.

Но что использовать в качестве токена? У вас нет класса, который мог бы служить токеном. Нет класса AppConfig.

TypeScript-интерфейсы — не допустимые токены

Константа HERO_DI_CONFIG имеет интерфейс AppConfig.

К сожалению, вы не можете использовать TypeScript-интерфейс в качестве токена:

// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }

Это кажется странным, если вы привыкли к инъекции зависимостей в строго типизированных языках, где интерфейс является предпочтительным ключом для поиска зависимостей.

Это не вина Angular. Интерфейс — это артефакт TypeScript на этапе разработки. JavaScript не имеет интерфейсов. Интерфейс TypeScript исчезает из сгенерированного JavaScript. Информация о типе интерфейса не сохраняется для Angular, чтобы её найти во время выполнения.

OpaqueToken

Одно из решений для выбора токена провайдера для зависимостей, не являющихся классами, — определение и использование OpaqueToken. Определение выглядит следующим образом:

import { OpaqueToken } from '@angular/core';

export let APP_CONFIG = new OpaqueToken('app.config');

Зарегистрируйте провайдер зависимости, используя объект OpaqueToken:

providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]

Теперь вы можете внедрить объект конфигурации в любой конструктор, которому он нужен, с помощью декоратора @Inject:

constructor(@Inject(APP_CONFIG) config: AppConfig) {
  this.title = config.title;
}

Хотя интерфейс AppConfig не играет никакой роли в инъекции зависимостей, он поддерживает типизацию объекта конфигурации в классе.

В качестве альтернативы вы можете предоставить и внедрить объект конфигурации в модуль ngModule, как AppModule.

src/app/app.module.ts (ngmodule-providers)

providers: [
  UserService,
  { provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
END_OF_DOCUMENT_MARKER

Дополнительные зависимости

HeroService требует Logger, но что, если обойтись без logger? Вы можете указать Angular, что зависимость является необязательной, аннотировав аргумент конструктора @Optional():

import { Optional } from '@angular/core';
constructor(@Optional() private logger: Logger) {
  if (this.logger) {
    this.logger.log(some_message);
  }
}

При использовании @Optional(), ваш код должен быть готов к значению null. Если вы не зарегистрируете logger где-то выше по цепочке, инжектор установит значение logger в null.

Обзор

На этой странице вы изучили основы инъекции зависимостей в Angular. Вы можете регистрировать различные типы поставщиков и знаете, как запросить инжектируемый объект (например, сервис), добавив параметр в конструктор.

Инъекция зависимостей в Angular обладает более широкими возможностями, чем описано в этом руководстве. Вы можете узнать больше об ее расширенных функциях, начиная с поддержки вложенных инжекторов в Иерархической инъекции зависимостей.

Приложение: Работа с инжекторами напрямую

Разработчики редко работают напрямую с инжектором, но вот пример, который это делает.

src/app/injector.component.ts

@Component({
  selector: 'my-injectors',
  template: `
  <h2>Other Injections</h2>
  <div id="car">{{car.drive()}}</div>
  <div id="hero">{{hero.name}}</div>
  <div id="rodent">{{rodent}}</div>
  `,
  providers: [Car, Engine, Tires, heroServiceProvider, Logger]
})
export class InjectorComponent {
  car: Car = this.injector.get(Car);

  heroService: HeroService = this.injector.get(HeroService);
  hero: Hero = this.heroService.getHeroes()[0];

  constructor(private injector: Injector) { }

  get rodent() {
    let rousDontExist = `R.O.U.S.'s? I don't think they exist!`;
    return this.injector.get(ROUS, rousDontExist);
  }
}

Сам инжектор является инжектируемым сервисом.

В этом примере Angular инжектирует собственный инжектор компонента в конструктор компонента. Затем компонент запрашивает у инжектированного инжектора нужные ему сервисы.

Обратите внимание, что сами сервисы не инжектируются в компонент. Они извлекаются с помощью вызова injector.get().

Метод get() выдает ошибку, если не может разрешить запрашиваемый сервис. Вы можете вызвать get() со вторым параметром, который представляет значение, возвращаемое в случае, если сервис не найден. Angular не может найти сервис, если он не зарегистрирован в этом или любом родительском инжекторе.

Этот метод — пример паттерна «локализатор сервисов» service locator pattern.

Избегайте использования этого метода, если это не абсолютно необходимо. Он способствует небрежному подходу «брать все подряд», как показано здесь. Сложно объяснить, понять и протестировать этот код. Из конструктора трудно понять, что требуется этому классу и что он будет делать. Он может получать сервисы из любого родительского компонента, а не только из собственного. Вам придется изучить реализацию, чтобы понять, что он делает.

Разработчики фреймворков могут применить этот подход, когда им необходимо получить сервисы обобщенно и динамически.

Приложение: Почему один класс на файл

Наличие нескольких классов в одном файле создает путаницу и лучше его избегать. Разработчики ожидают один класс на один файл. Сделайте их счастливыми.

Если вы объединяете класс HeroService и HeroesComponent в одном файле, определите компонент последним. Если вы определите компонент до сервиса, у вас возникнет ошибка обращения к null во время выполнения.

Вы можете определить компонент первым с помощью метода forwardRef(), как описано в этой статье блога. Но зачем рисковать? Избегайте проблемы, определяя компоненты и сервисы в отдельных файлах.

Следующий шаг

Синтаксис шаблонов

© 2010–2017 Google, Inc.
Licensed under the Creative Commons Attribution License 4.0.
https://v2.angular.io/docs/ts/latest/guide/dependency-injection.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API