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

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


    Одна из тем, вызывающая много вопросов — внедрение зависимостей. Некоторые люди не сталкивались с подобной технологией. Другие не до конца понимают, как она работает в рамках 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 и экспортируйте его вместе с сервисом.
    • Если какой-то компонент требует отдельного экземпляра сервиса, достаточно зарегистрировать его в этом компоненте. Учтите, что каждый созданный компонент будет иметь свой экземпляр зависимости.
    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 53

      +1
      Мощно. Спасибо за материал.
      А был ли у вас опыт написание production level проекта на Angular 2? Было бы интересно прочитать про грабли и методы их обхода, если таковые имеются.
        0
        Мы потихоньку начинаем внедрять… Основная грабля — это IE. Корпоративная политика велит использовать IE, приходится использовать.
        Пришел к выводу, что для меня лучше, когда транспиляция идет на клиенте, гораздо удобней отлаживать. Первоначальное отлаживание делаю в хроме, потом перепроверяю всё в IE (в режиме 10 версии, потом в 11 версии).
        например вот так не работает в IE10, но работает в 11:
        <div [hidden]="!showThisBlock">
           содержимое
        </div>
        

        Пришлось переделать на *ngIf и покрутить стили, чтобы при показе блок был «правильной» ширины…
          +1
          Можно было просто добавить css класс
          [hidden] {display:none !important;}
          
            0
            Ну так все правильно — неподдерживаемый атрибут
            К тому же полифилится одной строчкой, как заметили ниже, кроме уж совсем извращенных случаев, когда блок с аттрибутом hidden и каким-то классом всё же нужно отобразить. Ангуляр здесь вообще ни при чём.
            Кстати "*ngIf" и «display: none» — это не одно и тоже. В случае «display: none» блок не отображается, но он есть и происходит его инициализация, он занимает место в DOM. В случае "*ngIf===false" блока нет и не было.
            +3

            По работе сейчас пишу 2 проекта на нём. Пока они не сильно большие, но развиваются и растут.
            Из граблей:


            • При обновлении с beta.9 до beta.10 были проблемы. Но не у ангуляра, а у одной из зависимости (zone.js). В сети можно было найти пару костылей, как это обойти. Я просто подождал, пока починят и обновился без проблем. На данный момент актуальная версия beta.14. В целом, нужно просто обновляться вручную и не ставить в зависимостях ^.
            • Сейчас не так много библиотек для 2-го ангуляра. Например, Material в стадии альфы, бутстрап-компоненты тоже бывают с багами. Но в целом, нет особых проблем, просто что-то приходится писать руками и я не вижу тут ничего плохого.

            Если брать конкретно сам фреймворк, то я сталкивался с такими проблемами:


            1) биндинг напрямую к имени класса не работает. Я имею ввиду такую штуку:


            <div class="form-group" [class.has-error]="!form.valid"></div>  

            Решение:


            <div class="form-group" [ngClass]="{ 'has-error': !form.valid }"></div>  

            2) Этот баг может всплывать в различных местах, например если мы обрабатываем событие формы submit и в нём переходим на другой адрес. Вот пример, откройте консоль и нажмите на кнопку.
            Решение: забить, или делать переход не по событию submit, а, например, через <a> и его click.


            3) Не баг, просто ещё не реализованная фича. Иногда некоторые маршруты должны быть с проверкой прав доступа. Во втором ангуляре есть декоратор @CanActivate. Он принимает функцию-коллбек, которая может вернуть либо булевское значение, либо промис. В зависимости от значения результата, роутер либо переходит на нужную локацию, либо нет. Так вот, тут нельзя использовать внедрение зависимостей. А проверку прав или авторизированности пользователя обычно делают в сервисах. Ок, можно самому получить инжектор, передать ему список сервисов и брать нужный сервис оттуда. Но инжектор создаст новые инстансы сервисов. А иногда хочется взять уже готовые и настроенные.
            Решение: пока что можно использовать такой хак: сохраняем инстанс корневого компонента или его инжектор в переменной, которая доступна глобально. Когда нужно, берём сервисы из этого инжектора. Вот пример реализации. Вот более объёмный пример с разными кейсами.


            Тут дело в том, что авторы просто не решили, как сделать внедрение зависимостей правильно. Просто внедрять зависимости напрямую в декоратор не очень хорошо. Авторы думают и советуются с обществом, как бы это получше сделать. Обсуждение тут или тут.


            В общем, ничего критичного нет.

              0
              Ваш комментарий содержит полезной информации больше, чем некоторые статьи, и не только на хабре. Спасибо за столь раскрытый ответ.
              А вы, часом, не думали написать некоторое подобие туториала? Что-то на подобии React.js для начинающих (статья на habrahabr) — было бы очень полезно и информативно, учитывая ваш подход к ответам на вопросы.
                +1

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


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

                0
                А теперь обновитесь до RC :)
              +4
              Осторожно: субъективное мнение
              Вот за это я не люблю Angular. За стремление самим изобрести все велосипеды или хотя бы переписать существующие своими словами.
                0

                В целом я согласен, велосипедов много. Но в первом.
                Во втором же наоборот, от них уходят.


                Внедрение зависимостей стало выглядеть как во многих Java/.Net фреймворках. Только тут оно более продвинутое и гибкое.
                В проекте используется Rx, который почти на всех языках выглядит одинаково и многим уже известен.
                В шаблонах уменьшилось количество специфичных вещей: максимально используются стандартные атрибуты. Например, ng-show/ng-hide исчез, теперь чтобы сделать элемент невидимым используется стандартный атрибут hidden. Тоже самое для событий: вместо ng-click/ng-keypress и т.д. используются стандартные click/keypress.

                  –2
                  Внедрение зависимостей стало выглядеть как во многих Java/.Net фреймворках. Только тут оно более продвинутое и гибкое.

                  Ну да, а старый-добрый require или стандартный import из ES6 это слишком мейнстримно для Angular, ага. По-моему, одна из основных задач этого фреймворка — найти чем занять десятки программистов Google. Вот они и переписывают стандартные функции языка
                    0

                    А вы разницу между импортом и внедрением зависимостей понимаете? Почитайте введение, я же объясняю чем удобно внедрение и почему его используют. Так-то и в Java есть импорты, и что?

                      –1
                      Признаюсь честно, не понимаю. Краем глаза читал про «gorilla-banana problem». Можете вкратце рассказать?
                        +1

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

                          0
                          Не буду спорить с вами, но мне, с моей кочки, кажется, что Angular опять создает сложности на ровном месте. Меня вполне устраивает работа с зависимостями в стиле React, где минимум React'a и максимум яваскрипта. В Angular 2 по-прежнему много заумных терминов, зачастую придуманных разработчиками прямо в процессе написания мануала. Мне, трудяге-фронтенду, хотелось бы тратить свое время на изучение и совершенствование яваскрипт/ES6 функционала (область применения которого не ограничена фреймворком, который устареет через пару лет, а то еще умрет не родившись), а не болезненных фантазий ребят из команды Angular.
                            0
                            А какая в реакте может быть работа с зависимостями? из коробки никакой.
                            И вообще, ангулар это фреймворк, а реакт ui-библиотека. Они выполняют разные функции и сравнимы только частично, в области отрисовки ui.
                              0
                              Говоря React, я имею в виду экосистему, включая Redux, Reflux, Alt, React Router и прочие плюшки.
                                0
                                У нас большой проект на реакте и мы юзаем DI (очень похожую на DI angular2), это дает возможность удобно использовать модульность. Например можно объявить интерфейс модуля и уже в рантайме биндить к интерфейсу модуля конкретную реализацию. при этом сами модули ничего не знают друг о друге, только об интерфейсах.

                                Конечно можно это все реализовать и через сервис-локатор например или просто на коленке, но поддерживать и расширять код с DI имхо намного проще.

                                С сервис-локатором например 2 варианта использования, либо делать его глобальным, либо прокидывать его через все сущности. При DI инжектор управляет тем, что будет использовать клиентский код, а не клиентский код следит за местоположением сервис-локатора и своими зависимостями (IoC принцип, странно что про него упоминания я не заметил в статье).
                                  0
                                  Надо просто понимать, что есть что. Зависимости управляют связями между разным кодом, а не код управляет иерархией. Код является контентом для контейнера и пассивен по отношению к нему.Angular 2 по прежнему строит DI иерархию — сверху вниз. К сожалению, это в корне неправильно. Точнее с корня :). Но так пока работает Angular 2. Таким его, не особенно подумав, задумали разработчики. Я не могу повлиять на разработчиков. Именно поэтому я пока не буду использовать Angular 2. Постою в сторонке. Интерфейсы тут не спасут…
                                    0

                                    А как правильно?

                                      0
                                      Наоборот.
                                        0

                                        Не развернёте мысль?

                                          0
                                          Кто-кто, а ты сам можешь развернуть:) Для начала надо подумать — как сделать иерархию из массива контейнеров с контентом, которые содержат проинжектированные инстансы классов (клонированные и или по ссылке на контент). Нам же предлагается готовая иерархия из контейнеров. Что явно не айс. Явно не всем.
                                            +1

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

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

                                                Что такое "пересборка иерархии"?

                                                  0
                                                  Как из линейного массива вида [[id,parentid, content]] построить иерархию? Потом разобрать ее в линейный массив и построить заново?
                                                    0

                                                    А зачем?

                                                      0
                                                      Для создания адаптивных архитектурных решений. В игровой сфере, например. Там постоянно что-то приходится менять.
                                                        0

                                                        И чем тут может помочь "пересборка массивов"? Может приведёте пример кода?

                                                          0
                                                          Никогда не привожу код и не размещаю на гитхабе. А вы хитренький:)
                              +2

                              Заумные термины ввели разработчики лет 30 назад. Видимо всё время вы тратите только на изучение того самого "яваскрипт/ES6 функционала", раз не слышали про них и считаете, что это болезненные фантазии.
                              Паттерны проектирования не ограничены ни фреймворком, ни языками, так что их знание и умение применить всегда будет полезно. Ангуляр лишь реализует некоторые из них.

                        +2
                        Вы что-то путаете. Внедрение зависимостей совсем не то же самое что require/import.
                    0
                    Отличная статья, спасибо!
                      0
                      Тот самый момент, когда хочется переписать заново недавно законченый огромный проект на Angular 1.
                      А как в Angular 2 c производительностью при большом количестве вотчеров? В Angular 1 приходилось использовать библиотеку `bindonce` но и она не везде спасала.
                      +1
                      Как человеку, не очень знакомому с экосистемой Angular, мне не хватило какого-то введения, хотя бы в терминологию с пояснениями. Можно было просто ссылочки дать. Причём, я не могу сказать, что ничего про Angular 2 не знаю. Многое читал и многое мне было понятно… Но сейчас вроде статью прочитал, вроде даже что-то понял, в голове всё равно, в основном звенят только «Инжектор», «Провайдер», «Сервис», «Фабрика»…
                        0

                        Ну терминология не относится конкретно к ангуляру, ей уже много лет и она в целом общеизвестна (правда обычно больше в кругах java, .net и c++). Если бы я начал всё расписывать, статья бы растянулась раза в полтора-два… Ссылки постараюсь найти и вставить.

                          0
                          Вот оно что :) У меня, к сожеланию, нет общения с java, .net и c++, привык вращаться в среде жаваскрипта и эти штуки, пока, выглядят экзотично и кажутся слегка избыточными ) Хотя в целом Agunlar2 нравится. Смущает пока только размер пакета.
                        +1
                        «Для более лучшего понимания» это как «более лучше одеваться»?
                          0
                          Не подскажете, как зарегестрировать сервис как синглтон во втором ангуляре?
                            0

                            Сервисы и так являются синглтонами, я же писал об этом к конце. Если нужно, чтобы сервис был доступен всем компонентам, регистрировать его надо либо в корневом компоненте, либо в функции bootstrap.

                              0
                              Мне показалось, что вы писали это относительно к первому ангуляру и как упрек ему. Да, понял, но выглядит ненадежно. Допустим, мне надо железный сингтон на все, и тут кто-то в компоненте по незнанию заинжектит зависимость и все, у нас не синглтон, при том хрен потом отловишь это место. Как-то фиговато выглядит эта модель неявных синглтонов. Может я и не прав, но в таком случае статик юзать будет надежнее.
                                0
                                по незнанию заинжектит зависимость

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


                                Если нужен прям синглтон то… Ну можно сделать просто синглтон. Ещё можно получить родительский инжектор вручную и брать сервисы оттуда.

                                  0
                                  Допустим в проекте есть сервис с данными пользователя(залогинен/гость и всякое разное) User. Проект разбит на независимые модули(например разделы сайта/функционала) и в разных модулях нам нужны данные пользователя. Логично что инжектить User на уровне приложения как-то не очень, тем более есть множество других случаев с более редкими сервисами используемыми в разных частях приложения(в разных модулях), поэтому мы его(их) инжектим внутри каждого отдельного модуля или уже конкретно на месте, в компоненте. И тут сталкиваемся с тем, что для каждого модуля(компонента) у нас будет свой инстанс этого сервиса, а нужен один общий синглтон, инициализированный единственный раз. Как быть?
                                  В текущем проекте на ng1 гора подобных сервисов которые могут быть нужны в разных частях приложения, и офк их нельзя инжектить на уровне всего приложения.

                                  з.ы. за статью спасибо, сходу по оф докам DI был не так прозрачен
                                    0
                                    Пока читал Ваш комментарий, пришла такая мысль: что если реализовать синглтон на уровне фабрики? Ну, то есть, инжектим везде через { useFactory:… }, а в фабрике соответственно, не создаем каждый раз новый объект, а возвращаем однажды созданный. А чтобы никто не внедрил сервис в обход фабрики сделать проверку уже в рантайме, определив каким-то образом, откуда вызыван конструктор сервиса — из фабрики или как-то еще (например, опциональный параметр).
                            0
                            В одном из первых примеров у сервиса UserService определена зависимость от Logger (в конструкторе _logger: Logger). Однако при компиляции тайпскрипта в яваскрипт тип аргумента потеряется. Откуда ангуляр знает что первый аргумент в конструкторе UserService это зависимость именно типа Logger? В тайпскрипте есть рефлексия/метаданные?
                              0

                              Да, это рефлексия. Но рефлексия — фишка конкретно не Typescript, а es7. Typescript лишь обеспечивает простоту использования всего этого. Ангуляр зависит от пакета reflect-metadata, который и делает всю магию.

                                0
                                Эх жаль. Я надеялся что за рефлексию все таки тайпскрипт отвечает, тогда можно было бы ожидать что и инъекция через интерфейсы в будущем будет работать.
                            0
                            Прикольно, зашел почитать про js, а читаешь про родную Java.

                            > Это и отличает DI в Angular2 от других DI-фреймворков: в ангуляре у приложения нет одного инжектора, у каждого компонента может быть свой инжектор
                            Возможно для js мира так но для других языков нет(например http://scaldi.org/).

                            Only users with full accounts can post comments. Log in, please.