В этой статье я покажу, как распространенные сценарии использования внедрения зависимостей в Angular 1 могут быть реализованы в Angular 2.
Примеры кода в TypeScript
Я написал примеры кода на TypeScript, потому что я большой поклонник языка. Это не означает, что вы должны использовать его при создании приложений в Angular 2. Фреймворк прекрасно работает с ES5 и ES6.
Начнем с простого компонента
Давайте начнем с реализации простого компонента входа в систему.
// Angular 1: Basic Implementation of Login Component
class Login {
formValue: { login: string, password: string } = {login:'', password:''};
onSubmit() {
const service = new LoginService();
service.login(this.formValue);
}
}
module.directive("login", () => {
return {
restrict: 'E',
scope: {},
controller: Login,
controllerAs: 'ctrl',
template: `
<form ng-submit="ctrl.onSubmit()">
Text <input type="text" ng-model="ctrl.formValue.login">
Password <input type="password" ng-model="ctrl.formValue.password"><button>Submit</button>
</form>
`
};
});
Теперь тот же компонент реализован в Angular 2.
// Angular 2: Basic Implementation of Login Component
@Component({ selector: 'login' })
@View({
template: `
<form>
Text <input type="text" ng-control="login">
Password <input type="password" ng-control="password"><button>Submit</button>
</form>
`
})
class Login {
onSubmit(formValue: { login: string, password: string }) {
const service = new LoginService();
service.login(formValue);
}
}
Опытные разработчики знают, что подключение компонента входа в систему к службе входа в систему проблематично. Трудно протестировать этот компонент изолированно. И это также делает его менее пригодным для повторного использования. Если у нас есть два приложения с двумя службами входа, мы не сможем повторно использовать наш компонент входа.
Мы можем исправить это, исправив патч системного загрузчика, чтобы мы могли заменить сервис входа в систему, но это не правильное решение. У нас есть фундаментальная проблема с нашим дизайном, которую DI может помочь нам исправить.
Использование зависимости зависимости
Мы можем решить нашу проблему, внедрив экземпляр LoginService в конструктор вместо того, чтобы создавать его напрямую.
// Angular 1: Login Component Using DI
class Login {
formValue: { login: string, password: string } = { login: '', password: '' };
constructor(public service: LoginService) { }
onSubmit() {
this.service.login(this.formValue);
}
}
module.directive("login", () => {
return {
restrict: 'E',
scope: {},
controller: ["login", Login],
controllerAs: 'ctrl',
template: `
<form ng-submit="ctrl.onSubmit()">
Text <input type="text" ng-model="ctrl.formValue.login">
Password <input type="password" ng-model="ctrl.formValue.password"><button>Submit</button>
</form>
`
};
});
Теперь нам нужно рассказать фреймворку, как создать экземпляр сервиса.
module.service("login", LoginService);
Хорошо, давайте перенесем этот пример на Angular 2.
// Angular 2: Login Component Using DI
@Component({ selector: 'login' })
@View({
template: `
<form>
Text <input type="text" ng-control="login">
Password <input type="password" ng-control="password"><button>Submit</button>
</form>
`
})
class Login {
constructor(public service: LoginService) {}
onSubmit(formValue: { login: string, password: string }) {
this.service.login(formValue);
}
}
Как и в случае с Angular 1, нам нужно сообщить фреймворку, как создать сервис. В Angular 2 директивы и декораторы компонентов — это место, где вы конфигурируете привязки внедрения зависимостей. В этом примере компонент App делает LoginService доступным для себя и всех его потомков, включая компонент login. Экземпляр службы входа будет создан рядом с компонентом приложения. Так что, если от этого зависят несколько детей, все они получат один и тот же экземпляр.
@Component({
selector: 'app',
bindings: [LoginService]
})
@View({template: `<login></login>`, directives: [Login]})
class App {}
Мы разделили две проблемы: компонент входа в систему теперь зависит от некоторой абстрактной службы входа в систему, а компонент приложения создает конкретную реализацию службы. В результате компонент входа больше не заботится о том, какую реализацию службы входа он получит. Это означает, что мы можем тестировать наш компонент изолированно. И мы можем использовать его в нескольких приложениях.
Обратите внимание, что Angular 1 использует строки для настройки внедрения зависимостей. В Angular 2 по умолчанию используются аннотации типов, но есть способ использовать строки, когда требуется большая гибкость.
Использование другого сервиса входа
Мы можем настроить наше приложение для использования другой реализации службы входа в систему.
// Angular 2: Using SomeOtherLoginService
@Component({
selector: 'app',
bindings: [bind(LoginService).toClass(SomeOtherLoginService)]
})
@View({template: `<login></login>`, directives: [Login]})
class SomeOtherApp {}
Настройка службы входа
Одна из замечательных особенностей внедрения зависимостей заключается в том, что нам не нужно беспокоиться о зависимостях наших зависимостей. Компонент входа в систему зависит от службы входа в систему, но ему не нужно знать, от чего зависит сама служба.
Допустим, сервис требует некоторого объекта конфигурации. В Angular 1 это можно сделать следующим образом:
// Angular 1: Configuring LoginService
class LoginService {
constructor(public config: {url: string}) {}
//...
}
module.value("LoginServiceConfig", { url: LOGIN_URL });
module.service("login", ["LoginServiceConfig", LoginService]);
Теперь версия Angular 2:
// Angular 2: Configuring LoginService
@Injectable() class LoginService {
constructor(@Inject("LoginServiceConfig") public config: {url: string}) {}
//...
}
@Component({
selector: 'app',
bindings: [
LoginService,
bind("LoginServiceConfig").toValue({url: 'myurl')
]
})
@View({template: `<login></login>`, directives: [Login]})
class App {}
Внедрение элемента компонента
Часто необходимо, чтобы компонент взаимодействовал со своим элементом DOM. Вот как это можно сделать в Angular 1:
// Angular 1: Injecting Element
class NeedsElement {
element;
}
module.directive("needsElement", () => {
return {
restrict: 'E',
scope: {},
controller: NeedsElement,
controllerAs: 'ctrl',
template: `some template`,
link: (scope, el, attrs, controllers) => {
scope.ctrl.element = el;
}
};
});
Angular 2 работает намного лучше. Он использует тот же механизм внедрения зависимостей, чтобы внедрить элемент в конструктор компонента.
// Angular 2: Injecting Element
@Component({selector: 'needs-element'})
@View({template: 'some template'})
class NeedsElement {
constructor(el: ElementRef) {
el.nativeElement // the DOM element
}
}
Внедрение других директив
Также довольно часто, когда несколько директив работают вместе. Например, если у вас есть реализация вкладок и панелей, компонент вкладок должен знать о компонентах панелей.
Вот как это можно сделать в Angular 1.
// Angular 1: Injecting Directives
class Tab {
addPane(pane) { }
//...
}
class Pane {
tab: Tab;
setTab(tab: Tab) {
this.tab = tab;
tab.addPane(this);
}
//...
}
module.directive('tab', function() {
return {
restrict: 'E',
scope: {},
controller: Tab,
templateUrl: 'tab.html'
};
});
module.directive('pane', function() {
return {
require: '^tab',
restrict: 'E',
controller: Pane,
controllerAs: 'ctrl',
link: function(scope, element, attrs, tabCtrl) {
scope.ctrl.setTab(tabCtrl);
},
templateUrl: 'pane.html'
};
});
Мы используем require
свойство, чтобы получить доступ к tab
контроллеру. Затем мы используем, scope
чтобы получить доступ к pane
контроллеру. И, наконец, мы используем функцию связи для соединения двух. Это довольно много работы для такого простого сценария.
И, опять же, Angular 2 делает здесь намного лучше.
// Angular 2: Injecting Directives
@Component({selector: 'tab'})
@View({template: 'tab.html'})
class Tab {
addPane(pane) { }
//...
}
@Component({selector: 'pane'})
@View({template: 'pane.html'})
class Pane {
constructor(public tab: Tab) {
tab.addPane(this);
}
//...
}
Но мы можем сделать даже лучше, чем это! Вместо регистрации панелей на ближайшей вкладке компонент вкладки может запрашивать панели.
// Angular 2: Injecting Directives (Using Query)
@Component({selector: 'tab'})
@View({template: 'tab.html'})
class Tab {
constructor(@Query(Pane) panes: QueryList<Pane>) {}
//...
}
@Component({selector: 'pane'})
@View({template: 'pane.html'})
class Pane {
}
Query заботится о многих проблемах, с которыми сталкиваются разработчики при реализации этого в Angular 1:
- Стекла всегда в порядке.
- Запрос уведомит компонент вкладки об изменениях.
- Пану не нужно знать о Tab. Компонент Pane легче тестировать и использовать повторно.
Единый API
В Angular 1 есть несколько API для введения зависимостей в директивы. Кто не смущает разница между factory
, service
, provider
, constant
и value
? Некоторые объекты вводятся по позиции (например, элемент), некоторые по имени (например, LoginService). Некоторые зависимости всегда предоставляются (например, элемент в ссылке), некоторые должны быть настроены с использованием require
, а некоторые настроены с использованием имен параметров.
Angular 2 предоставляет единый API для внедрения сервисов, директив и элементов. Все они вставляются в конструктор компонента. В результате API-интерфейсы для изучения намного меньше. И ваши компоненты гораздо проще тестировать.
Но как это работает? Как он узнает, какой элемент добавить, когда компонент запрашивает его? Как это работает так:
Каркас создает дерево инжекторов, соответствующее DOM.
<tab><pane title="one"></pane><pane title="two"></pane></tab>
Соответствующее дерево инжекторов:
Injector matching <tab>
|
|__Injector matching <pane title="one">
|
|__Injector matching <pane title="two">
Поскольку для каждого элемента DOM есть инжектор, среда может предоставлять контекстную или локальную информацию, такую как элемент, атрибуты или близлежащие директивы.
Вот как работает алгоритм разрешения зависимостей.
// this is pseudocode.
var inj = this;
while (inj) {
if (inj.has(requestedDependency)) {
return inj.get(requestedDependency);
} else {
inj = inj.parent;
}
}
throw new NoBindingError(requestedDependency);
Так что, если Pane
зависит от Tab
, Angular начнет с проверки, имеет ли элемент панели экземпляр Tab
. Если это не так, он проверит родительский элемент. Процесс будет повторяться до тех пор, пока он не найдет экземпляр Tab
или не достигнет корневого инжектора.
Вы можете указать на любой элемент на странице, и с помощью ngProbe
get его инжектора. Вы также можете увидеть инжектор элемента, когда выдается исключение.
Я знаю, что это может показаться немного сложным, но правда в том, что Angular 1 уже имеет подобный механизм. Вы можете ввести соседние директивы, используя require
. Но этот механизм не разработан в Angular 1, и поэтому мы не можем в полной мере использовать его.
Angular 2 доводит этот механизм до логического завершения. И оказывается, нам больше не нужны другие механизмы.
Расширенные примеры
До сих пор мы рассматривали примеры, которые работали как в Angular 1, так и в Angular 2. Теперь я хочу показать вам несколько продвинутых примеров, которые просто невозможно выразить в Angular 1.
Необязательные зависимости
Чтобы пометить зависимость как необязательную, используйте дополнительный декоратор.
class Login {
constructor(@Optional() service: LoginService) {}
}
Контроль видимости
Вы можете быть более конкретным, где вы хотите получить зависимости. Например, вы можете запросить другую директиву для того же элемента.
class CustomInputComponent {
constructor(@Self() f: FormatterDirective) {}
}
Или вы можете попросить директиву в том же виде.
class CustomInputComponent {
constructor(@Host() f: CustomForm) {}
}
Вы можете прочитать больше об этом здесь.
Предоставление двух реализаций одного и того же сервиса
Поскольку Angular 1 имеет только один объект-инжектор, вы не можете иметь две реализации LoginService в одном приложении. В Angular 2, где каждый элемент имеет инжектор, это не проблема.
@Component({
selector: 'sub-app',
bindings: [bind(PaymentService).toClass(CustomPaymentService1)]
})
@View({templateUrl: `subapp1.html`})
class SubApp1 {}
@Component({
selector: 'sub-app',
bindings: [bind(PaymentService).toClass(CustomPaymentService2)]
})
@View({templateUrl: `subapp2.html`})
class SubApp2 {}
@Component({
selector: 'app'
})
@View({template: `<sub-app-1></sub-app-1><sub-app-2></sub-app-2>`})
class App {}
Службы и директивы, созданные в соответствии с этим, SubApp1
будут использовать CustomPaymentService1
, а те, что созданы в соответствии с ним, SubApp2
будут использоваться CustomPaymentService2
, даже если все они объявят зависимость PaymentService
.
Посмотреть привязки
Следующее делает LoginService доступным для инъекций как в общедоступных дочерних элементах (так называемых light dom children) приложения, так и в его представлении.
@Component({
selector: 'app',
bindings: [LoginService]
})
@View({templateUrl: 'app.html'})
class App {}
Иногда вы просто не хотите делать привязку доступной в представлении компонента. Вот как вы можете это сделать:
@Component({
selector: 'app',
viewBindings: [LoginService]
})
@View({templateUrl: 'app.html'})
class App {}
Резюме
- Внедрение зависимостей является одной из основных частей Angular 2.
- Это позволяет вам зависеть от интерфейсов, а не от конкретных типов.
- Это приводит к более разъединенному коду.
- Это улучшает тестируемость.
- Angular 2 имеет один API для внедрения зависимостей в компоненты.
- Внедрение зависимостей в Angular 2 является более мощным.