Pull to refresh

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

Reading time 19 min
Views 74K

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


Одна из тем, вызывающая много вопросов — внедрение зависимостей. Некоторые люди не сталкивались с подобной технологией. Другие не до конца понимают, как она работает в рамках Angular 2, так как привыкли к другим реализациям, которые бывают в других фреймворках.


А разгадка кроется в том, что DI во втором ангуляре действительно несколько отличается от других, и связано это в первую очередь с общим подходом и философией 2-й версии. Заключается она в том, что сущностями из которых строится всё приложение, являются компоненты. Сервисный слой, роутер, система внедрения зависимостей — вторичны и они имеют смысл только в рамках компонента. Это очень важный момент, который лежит в основе понимания архитектуры нового фреймворка.


Введение


Это пересказ 2-х страниц из оф. документации, касательно внедрению зависимостей в Angular 2: этой и этой.


Почему Typescript

В статье я буду использовать Typescript. Почему?
Сам фреймворк написан на Typescript, и информации по связке Angular2 + Typescript больше всего.
Код на Typescript с точки зрения синтаксиса — это свежая реализация стандарта ES, дополнительная типизация, и немного вспомогательных фишек. Тем не менее, приложения можно писать и на Javascript, и на Dart. В JS-версии можно не использовать ES6+ синтаксис, однако теряется лаконичность и ясность кода. А если настроить Babel на поддержку свежих фич, то синтаксически всё будет очень похоже на TS-код: классы, аннотации/декораторы, и т.д. Ну только без типов, так что внедрение зависимостей будет выглядеть немного по-другому.


Проблема зависимостей


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


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() {}
}

Конечно, логики тут нет совсем, но для иллюстрации вполне подойдёт.


Итак, в чём тут проблема? На данный момент Car жёстко зависит от 2-х сервисов, которые вручную создаются в его конструкторе. С точки зрения потребителя сервиса Car это хорошо, ведь зависимость Car сама позаботилась о своих зависимостях. Но, если мы, например, захотим сделать, чтобы в конструктор Engine передавался обязательный параметр, то придётся менять и код самого Car:


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();
    }
}

Конструкторы в TS
// Обратите внимание, что в конструктор тут добавляется модификатор доступа перед аргументом
// Это просто синтаксический сахар для такого кода:
export class Engine2 {
    public cylinders
    constructor(cylinders: number) {
        this.cylinders = cylinders
    }
}

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


Перепишем код так, чтобы экземпляры зависимостей Car передавались извне:


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

Уже получше. Код самого сервиса сократился, а сам сервис стал более гибким. Его легче тестировать и конфигурировать:


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());

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


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


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.


На пути к внедрению


Как можно улучшить код? Каждый потребитель знает о том, какие сервисы-зависимости ему нужны. Но чтобы уменьшить связность системы, потребитель не должен создавать их сам. Можно создать класс-синглтон, в котором бы создавались и хранились инстансы всех наших сервисов. В таком классе мы определяем, как нужно создавать необходимые сервисы, а получать их можно, например, по некому ключу. Тогда в сервисах достаточно будет только как-то получить экземпляр такого синглтона, а из него уже получать готовые инстансы зависимостей. Такой паттерн называется ServiceLocator. Это одна из разновидности инверсии контроля. Тогда код выглядел бы примерно так:


import {ServiceLocator} from 'service-locator.ts';
// ...
let computer = ServiceLocator.instance.getService(Car) // получаем сервис по его типу

С одной стороны, мы избавились от жёсткой связи потребителя и его зависимостей: все сервисы создаются вне потребителей. Но с другой, теперь потребители жёстко связаны с сервис-локатором: каждый потребитель должен знать, где находится инстанс сервис-локатора. Ну и создание сервисов происходит по-прежнему вручную.


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


Этим и занимаются DI-фреймворки. Они управляют жизненным циклом внедряемых зависимостей, отслеживают места, где эти зависимости требуются и внедряют их, т.е. передают в потребителя созданный инстанс зависимости, которую потребитель запросил. Из потребителей исчезает жёсткая зависимость от сервис-локатора: теперь с локатором работает DI-фреймворк.


Суть работы примерно такая:


  • Сперва каким-то образом надо зарегистрировать все внедряемые зависимости в "инжекторе": для каждого сервиса нужно описать, как его создать и как его потом найти в сервис-локаторе.
  • В потребителе указываются необходимые ему зависимости.
  • DI-контейнер сканирует программу на наличия точек внедрения.
  • Когда кому-то понадобится зависимость, контейнер найдёт нужный экземпляр сервиса в своём сервис-локаторе и внедрит этот экземпляр куда нужно.

И в зависимости от DI-фреймворка, эти пункты будут выглядеть в коде по-разному.


Ангуляр №1


Для более лучшего понимания устройства второй версии этого фреймворка, в частности, DI, я хотел бы немного описать, как устроена первая его часть.


Жизненный цикл приложения состоит из нескольких этапов. Я хотел бы выделить 2 этапа:


  • Сonfig. На этом этапе происходит настройка модулей, входящих в приложение. Доступно лишь ограниченное внедрение зависимостей. Настройка модулей обычно подразумевает настройку сервисов, которые будут использованы в дальнейшем. Сервисы можно конфигурировать через их провайдеры — особые объекты, которые можно внедрить на этом этапе и которые в дальнейшем вернут настроенный инстанс сервиса.
  • Run. Тут сконфигурированное приложение запущено и работает. Доступны все зависимости, сервисы уже созданы (на самом деле там ленивая инициализация, но не суть) и настроены.

На верхнем уровне находятся модули. Модуль, по-сути, — просто объект, в котором могут регистрироваться и храниться различные части приложения: сервисы, контроллеры, директивы, фильтры. Так же, у модуля могут быть config- и run-колбеки, которые запустятся на соответствующих этапах приложения.


Итак, как же выглядит внедрение зависимостей в первой версии:


Код
 // функция-фабрика
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)); // используем внедрённый сервис
})

Да, тут есть куча всяких нюансов. Например, в первом ангуляре есть аж 5 разных видов сервисов, так что зарегистрировать сервис можно разными способами. А при минификации кода аргументы функции могут измениться, так что лучше использовать другой синтаксис для объявления зависимостей...


Но я не хочу углубляться в дебри первого ангуляра, напишу лишь основные моменты:


  • Все сервисы являются синглтонами.
  • Регистрация чего-либо (сервисов, котнтоллеров, и т.д.) происходит в модуле путём вызова соответствующих функций, т.о. модуль является неким хранилищем различных частей приложения.
  • Внедрение зависимости происходит по строке, т.е. сервис-локатор хранит и ищет всё в объекте, где ключ — имя внедряемого сервиса, а значение — инстанс. На самом деле, ещё можно внедрять контроллеры. Для них хранится не инстанс, а функция-конструктор.
  • В приложении есть особый сервис $injector. Это и есть сервис-локатор, через который можно получить зависимость вручную. Обычно, он один на всё приложение.
  • Зависимости ищутся среди зарегистрированных в главном модуле. Главный модуль тот, который непосредственно загружается на HTML-страницу.
  • Если при создании модуля указать зависимости, то $injector будет искать зависимость не только в текущем модуле, но и в зависимых.

Angular 2: новый путь


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


Первый ангуляр был, по-сути, просто набором полезных приёмов, техник и паттернов, склееных с помощью DI вместе. Но отдельные его части были как-то сами по себе, были слегка разрозненны. Не было единой концепции.


  • Контроллер можно было вставить в DOM кучей разных способов: через атрибут, через роутер (которых уже 3 версии: простой оригинальный, навороченный сторонний, и бэкпорт из 2-й части), через директиву.
  • Параллельно DOM-дереву, было дерево с иерархией объектов-скоупов, в которых хранились данные, доступные через контроллеры. Сами скоупы могли наследоваться, почти как прототипы, а могли быть изолированными. Так же были крайне сложные для понимания transclude-скоупы.
  • Была своя событийная система, связанная с иерархией скоупов, по которой можно было передавать данные в разные стороны

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


Компонентный подход


Что такое компонент в Angular 2? Это просто класс с определёнными метаданными и связанный с ним слой представления (шаблон). Чтобы сделать из класса компонент, нужно добавить в него эти самые определённые метаданные. Самый простой способ — обернуть его в декоратор @Component, который и связывает представление с его ViewModel (т.е. самим классом). А с точки зрения иерархии типов, компонент — частный случай директивы (которая определяется с помощью декоратора @Directive), у которой есть шаблон:


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

В декоратор нужно передать объект, который должен содержать минимум 2 обязательных поля: selector и template.


Поле selector содержит строку, которая будет использоваться в качестве css-селектора для поиска компонента в DOM. Можно передать любой валидный селектор, но чаще всего используют селектор-тэг, не входящий в стандартный набор HTML-тэгов. Таким образом, создаются кастомные тэги.


Поле template содержит строку-шаблон, которым заменится содержимое DOM-элемента, найденного по селектору. Вместо строки с шаблоном можно передать строку с путём до файла-шаблона (только поле будет называться templateUrl). Подробнее про синтаксис шаблонов можно почитать страницу доков или её русский перевод.


Иерархия компонентов


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


Ещё были проблемы с роутерами. Оригинальный был довольно скуден, не позволял создавать нормальной иерархии. UI-router был более богат на фичи, позволял использовать несколько view, умел строить иерархию состояний.
Но основная проблема обоих роутеров заключалась в том, что вся эта иерархия путей была абсолютно никак не связана с иерархией скоупов и была крайне не гибкой.


Как же поступили во второй версии? В основе второго ангуляра, как я уже говорил, лежат компоненты. Всё приложение состоит только из компонентов, которые образуют древовидную иерархическую структуру. Корневой компонент загружается с помощью функции bootstrap на HTML-страницу (если используется браузер как целевая платформа). Все остальные компоненты помещаются внутрь корневого и образуют дерево компонентов.


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


Это и отличает DI в Angular2 от других DI-фреймворков: в ангуляре у приложения нет одного инжектора, у каждого компонента может быть свой инжектор


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


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


Сервисы


Сервис в Angular 2 — это простой класс.


interface User {
    username: string;
    email: string;
}

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

Регистрация сервисов


Чтобы сервис можно было внедрить, сперва нужно зарегистрировать его. Нам не нужно вручную создавать инжектор, ангуляр сам создаёт глобальный инжектор, когда вызывается функция bootstrap:


bootstrap(AppComponent);

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


bootstrap(AppComponent, [UserService]);

Этот код сделает наш сервис доступным для всего приложения. Однако так делать не всегда хорошо. Разработчики фреймворка советуют регистрировать в этом месте только системные провайдеры, и только если они нужны во всей системе. Например, провайдеры роутера, форм и Http-сервисов.


Второй способ зарегистрировать сервис — добавить его в метаданные компонента в поле providers:


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);

Внедрение сервисов в компонент


Самый простой способ внедрить сервис — через конструктор. Так как TypeScript поддерживает типы, то достаточно написать так:


@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);

И всё! Если UserService был зарегистрирован, то ангуляр внедрит нужный инстанс в аргумент конструктора.


Внедрение сервисов в сервисы


Чтобы сервис мог сам внедрять зависимости, нужно обернуть его декоратором @Injectable. Разработчики же рекомендуют добавлять этот декоратор вообще для любых сервисов, так как никогда не знаешь, понадобится ли когда-нибудь зависимости внутри сервиса. Так что последуем их совету.


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' };
    }
}

Теперь нужно не забыть зарегистрировать и сервис Logger, иначе ангуляр выдаст ошибку:


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

Так что добавляем Logger в список провайдеров компонента:


providers: [UserService, Logger],

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


Если внедряемый сервис не обязателен, то нужно добавить аннотацию @Optional:


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' };
    }
}

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


Провайдеры


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


Когда мы добавляем класс сервиса в список провайдеров (компонента или в функцию bootstrap), на деле это означает следующее:


[Logger],
// Это просто укороченная запись для такого выражения
[new Provider(Logger, {useClass: Logger})],
// Тоже самое, используя функцию provide
[provide(Logger, {useClass: Logger})],

И конструктор класса Provider, и функция provide принимают 2 аргумента:


  • Токен, который выступает в качестве ключа, по которому сервис-локатор будет искать зависимость
  • Объект, который содержит рецепт, как именно нужно создать внедряемое значение

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


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


Допустим, мы хотим вместо класса Logger использовать экземпляр класса BetterLogger в качестве сервиса. Нет необходимости искать и менять по всему приложению зависимость Logger на BetterLogger, достаточно зарегистрировать провайдер для Logger с опцией useClass:


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

Даже если альтернативный класс имеет какую-то зависимость, которой нет у оригинального сервиса:


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

Мы всё равно сможем так же просто использовать его, нужно лишь зарегистрировать нужные зависимости:


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

Алиасы провайдеров


Предположим, у нас есть некоторый старый компонент, который зависит от старого сервиса логгера OldLogger. Этот сервис имеет такой же интерфейс, что и новый логгер NewLogger. Но по какой-то причине, мы не можем менять тот старый компонент. Так что мы хотим, чтобы вместо старого логгера использовался новый. Если мы попробуем сделать так:


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

То получится не то, что мы хотели: создадутся 2 экземпляра нового логгера. Один будет использоваться там, где внедряется старый, другой — где внедряется новый логгер. Чтобы создался только 1 инстанс нового логгера, который бы использовался везде, регистрируем провайдер с опцией useExisting:


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

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


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


// Просто создадим объект, который будет реализовывать нужный интерфейс, в данном случае Logger
let silentLogger = {
    logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
    log: () => {}
}

Чтобы использовать уже готовый объект, регистрируем провайдер с опцией useValue:


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

Провайдер-фабрика / фабричный провайдер


Иногда нужно зарегистрировать провайдер динамически, используя информацию, недоступную с самого начала. Например, эта информация может быть получена из сессии и быть разной от раза к разу. Также предположим, что внедряемый сервис не имеет независимого доступа к этой информации.
В таких случаях используют провайдер-фабрику / фабричный провайдер.


Пускай у нас есть некий сервис BookService, который так же как и EvenBetterLogger нуждается в информации из другого сервиса. Допустим, мы хотим проверить, авторизирован ли пользователь, используя данные из AuthService. Но, в отличие от EvenBetterLogger мы не можем внедрить напрямую сервис, т.е. в данном случае, BookService не имеет доступа к AuthService. Сервисы выглядят вот так:


@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, и существует он только на этапе компиляции. В рантайме нет никаких интерфейсов, так что внедрить интерфейс тайпскрипта мы не сможем.


Решение проблемы


Мы можем использовать специальный класс 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, которое содержит ссылку на родительский инжектор. Если компоненту требуется зависимость, инжектор компонента пытается найти нужную у себя. Если не находит, пытается найти в родительских инжекторах вплоть до корневого.


Вот пример того, как работают инжекторы с иерархией:


Код
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-го инстанса сервиса. Но так как самих инжекторов может быть несколько, то и разных инстансов одного и того же сервиса во всём приложении может быть больше одного.


Выводы


  • Внедрять зависимости в Angular 2 можно по типу, OpaqueToken'у, строке и др. По-умолчанию используется внедрение с токеном-типом.
  • Интерфейсы внедрить не получится, используйте OpaqueToken.
  • В отличие от многих других фреймворков, в Angular2-приложении могут быть несколько инжекторов.
  • Каждый компонент имеет свой инжектор. Он может быть общим с другими компонентами или быть уникальным.
  • Все инжекторы образуют иерархию, повторяющую иерархию компонентов.
  • Сервисы ищутся по иерархии от дочерних к родительским.

Ну и несколько советов:


  • Постарайтесь избегать регистрации в глобальном инжекторе. Это делает ваш код менее гибким. В функции bootstrap регистрируйте только провайдеры самого ангуляра.
  • Также не стоит регистрировать абсолютно все провайдеры в корневом компоненте.
  • Для конкретного сервиса определите в вашей иерархии компонентов самый верхний компонент, использующий его и регистрируйте провайдер там.
  • Постарайтесь вынести настройки сервиса (если он подразумевает настройку) в объект-конфиг. Создайте для него OpaqueToken и экспортируйте его вместе с сервисом.
  • Если какой-то компонент требует отдельного экземпляра сервиса, достаточно зарегистрировать его в этом компоненте. Учтите, что каждый созданный компонент будет иметь свой экземпляр зависимости.
Tags:
Hubs:
+22
Comments 53
Comments Comments 53

Articles