Это третья часть в серии из пяти статей, в которой рассматриваются распространенные ошибки AngularJS. Напомним, что пять основных ошибок, которые я вижу, делают люди:
- Сильная зависимость от $ scope (без использования контроллера как)
- Злоупотребляя $ watch
- Чрезмерное использование трансляции $ и $ emit
- Взлом DOM
- Неспособность проверить
Когда я опубликовал первую статью, в одном из комментариев было высказано предположение, что использование контроллера является приемлемым для небольших контроллеров, но большие сложные контроллеры с зависимостями также могут не сработать. В этой статье вы увидите некоторые зависимости и, надеюсь, новый способ думать о том, как управлять взаимозависимостями.
Чрезмерное использование трансляции $ и $ emit
Одно из преимуществ использования $ scope — это разнообразие доступных функций, например возможность наблюдать за моделью. Я объяснил, почему это не лучшая идея или подход, и в этом посте мы рассмотрим другой набор функций. По сути, концепция событий и возможность настраивать события — это хорошая идея. В AngularJS пользовательские события поддерживаются с помощью функций $ emit (всплывающее вверх) и $ broadcast (замедление) для публикации события.
В Angular области видимости являются иерархическими. Следовательно, с помощью этих функций можно общаться с детьми или слушать детей. Простая функция $ on используется для регистрации или подписки на событие. Давайте возьмем простой пример. Опять же, я использую что-то придуманное, чтобы показать, как это работает, но, надеюсь, вы сможете экстраполировать на более сложные сценарии, такие как списки master / detail или даже страницы с большим количеством информации, для которой требуется какой-то механизм для автоматического обновления при обновлении одной области.
Для этого примера у меня есть три отдельных контроллера. Один отвечает за выбор пола. Отдельный контроллер обрабатывает отображение связанной метки и, следовательно, должен знать об изменениях, а еще один контроллер считает, сколько раз пол изменился.
HTML5 выглядит так:
<div ng-app="myApp">
<div ng-controller="genderCtrl">
<select ng-options="g as g for g in genders"
ng-model="selectedGender">
</select>
</div>
<div ng-controller="babyCtrl">
It's a {{genderText}}!
</div>
<div ng-controller="watchCtrl">
Changed {{watches}} times.
</div>
</div>
Контроллер пола выставляет список и обрабатывает текущий выбор. Поскольку другие контроллеры зависят от этого выбора, он следит за изменением и транслирует событие всякий раз, когда оно изменяется. Некоторые из вас могут узнать это старинное устройство, которое мы использовали в прошлом для трансляции музыки.
Обычный шаблон, который я вижу реализованным, — это широковещательная передача из корневой области, поскольку она гарантированно охватывает все дочерние области.
function GenderController($scope, $rootScope) {
$scope.genders = genders;
$scope.selectedGender = genders[0];
$scope.$watch('selectedGender', function () {
$rootScope.$broadcast('genderChanged',
$scope.selectedGender);
});
}
Контроллер, который выставляет метку, прослушивает событие и соответственно обновляет текст.
function BabyController($scope) {
$scope.genderText = labels[0];
$scope.$on('genderChanged',
function (evt, newGender) {
$scope.genderText =
newGender ===
genders[0] ? labels[0] : labels[1];
});
}
Наконец, третий контроллер прослушивает событие и обновляет счетчик при каждом его срабатывании.
function WatchController($scope) {
$scope.watches = 0;
$scope.$on('genderChanged', function () {
$scope.watches += 1;
});
}
Это приложение работает ( проверьте это здесь ), но я думаю, что мы можем сделать лучше. Почему мне не нравится этот подход? Вот несколько причин:
- Это создает зависимость от $ scope, в которой я не уверен, что это необходимо.
- Кроме того, он создает зависимость от $ rootScope.
- Если я решу не использовать $ rootScope, мои контроллеры должны понимать иерархию $ scope и иметь возможность соответственно $ emit или $ broadcast. Попробуйте это проверить!
- Чтобы отреагировать на изменение, также требуется зависимость от $ scope.
- Теперь я должен понять, как работают пользовательские события, и правильно использовать их соглашение (обратите внимание, например, что новое значение находится во втором параметре, а не в первом).
- Сейчас я делаю что-то «угловым путем», что я мог бы сделать с чистым JavaScript. Я уверен, что чем ближе к JavaScript я останусь, тем проще будет обновить и перенести этот код позже.
Хорошо, так каков ответ? Один из способов взглянуть на это не в виде последовательности событий (т.е. пользователь меняет пол, который вызывает изменение, которое запускает событие, которое вызывает ответ), а вместо этого смотрит на результат. Что на самом деле происходит? Смена пола действительно меняет состояние модели. Состояние метки просто чередуется в зависимости от состояния выбора пола, а счетчик повторяется. Таким образом, изменение состояния приводит к изменению модели. Мне действительно нужно сообщение, чтобы сообщить об этом?
Позвольте мне разобрать это по-другому. Во-первых, когда я думаю о чем-то общем между контроллерами, я сразу думаю о сервисе . Определяется ли это через фабрику или службу, здесь не главное (если вы не знаете или не понимаете разницы, прочитайте раздел «Общие сведения о поставщиках, службах и фабриках в Angular» ), а скорее о том, что существует общее состояние, общее для контроллеров. Итак, я создаю гендерный сервис:
function GenderService() { }
angular.extend(GenderService.prototype, {
getGenders: function () {
return genders.slice(0);
},
getLabelForGender: function () {
return this.selectedGender === genders[0] ?
labels[0] : labels[1];
},
selectedGender: genders[0]
});
Служба предоставляет мне копию списка полов, позволяет мне получить или установить выбранный пол и возвращает метку для пола. Все полезные, общие функции упакованы в один компонент. Помимо способа, которым я подключил его с помощью функции расширения Angular, это чистый объект POJO, который я могу тестировать без зависимости от Angular.
Теперь я могу создать контроллер, который также является POJO. Он полагается на гендерный сервис, поэтому будет использовать инжектор конструктора. Хотя Angular будет обрабатывать это для меня в приложении, я могу легко создать свой собственный экземпляр службы гендерной проверки или смоделировать его и передать его в конструктор для тестирования, и все равно он не будет зависеть от Angular вообще… или $ scope… или $ emit… или $ трансляция.
function GenderController(genderService) {
this.genders = genderService.getGenders();
this.genderService = genderService;
}
Object.defineProperty(GenderController.prototype,
'selectedGender', {
enumerable: true,
configurable: false,
get: function () {
return this.genderService.selectedGender;
},
set: function (val) {
this.genderService.selectedGender = val;
}
});
Обратите внимание, что наш «контроллер» — это на самом деле просто объект, который предоставляет список полов и обладает свойством, которое передает выбранный пол через гендерную службу. На самом деле я мог бы просто разоблачить сервис и связать его напрямую, но это, на мой взгляд, слишком много информации. Мой пользовательский интерфейс должен касаться только списка и выбора, а не базовой реализации того, как им управляют. Конечным результатом теперь является то, что вы можете выбрать пол, и служба будет удерживать выбор. Заметьте, что он не отправляет никаких сообщений, так как меняется контроллер меток? Взглянуть:
function BabyController(genderService) {
this.genderService = genderService;
}
Object.defineProperty(BabyController.prototype,
'genderText', {
enumerable: true,
configurable: false,
get: function () {
return this.genderService.getLabelForGender();
}
});
Видите, как это просто? Я могу написать тест, который создает три POJO (сервис и контроллеры), имитируя обновление выбора на контроллере выбора и проверяя изменения метки пола на контроллере меток. Мой пользовательский интерфейс просто привязывается к интересующему его свойству (свойству гендерного текста) и не путается с какой-либо презентацией или бизнес-логикой, происходящей «за кадром». При обращении к свойству label он запрашивает у службы, какой должна быть метка, и всегда возвращает правильное значение на основе текущего выбора.
К настоящему времени вы, наверное, уже догадались, как выглядит контроллер наблюдателя …
function WatchController(genderService) {
this.genderService = genderService;
this._watches = 0;
this._lastSelection = genderService.selectedGender;
}
Object.defineProperty(
WatchController.prototype,
'watches', {
enumerable: true,
configurable: false,
get: function () {
if (this.genderService.selectedGender !==
this._lastSelection) {
this._watches += 1;
this._lastSelection =
this.genderService.selectedGender;
}
return this._watches;
}
});
Он просто отслеживает часы внутри, и всякий раз, когда к ним обращаются извне, проверяется, изменился ли выбор. Если вам не нравится этот тип мутации в свойстве getter, вы можете вместо этого выставить его как метод и привязать данные к результату вызова метода.
Теперь у меня есть четыре POJO без угловых зависимостей. Я могу проверить их по своему усмотрению, мне не нужно увеличивать объем $, и мне даже не важно, как реализованы $ emit или $ broadcast. Посмотрите на рабочий пример без $ emit или $ broadcast .
Есть одна вещь, которую нужно вызвать. Некоторые из вас могут признать , что этот подход ( в частности , для контроллера часов) это зависит от углового косвенно . Реализация наблюдателя основана на доступе к количеству часов. Независимо от Angular, вы можете манипулировать выделением несколько раз, и наблюдатель пропустит его, если вы не будете опрашивать его каждый раз. Я знаю, что в Angular он всегда будет обновляться из-за цикла дайджеста, поэтому в контексте Angular он будет работать нормально, но если бы я хотел что-то более «независимое», мне нужно было бы выставить событие из службы пола и зарегистрироваться от наблюдателя. сервис для обновления счетчика, даже если он не опрошен. В этом случае я бы, вероятно, реализовал свой собственный агрегатор событий это не зависит от иерархии $ scope или $ scope для работы.
Кстати, причина, по которой это работает без часов или сообщений, заключается в том, как работает цикл Angular $ digest. Когда вы изменяете модель путем изменения пола, Angular автоматически проверяет значения других свойств, связанных с данными. Это, конечно, вызывает получатель, который был реализован, и приводит к тому, что правильное значение возвращается и обновляется в пользовательском интерфейсе. Еще раз, мы избегаем лишних часов.
Поскольку часы и обработчики событий могут изменять модель, Angular на самом деле должен возвращаться и переоценивать выражения каждый раз, когда вызывается функция наблюдения или обработчик событий, чтобы убедиться в отсутствии дальнейших обновлений. В этой модели сама модель уже обновлена, поэтому требуется только один проход (вы увидите несколько проходов, потому что есть несколько привязок, но никаких дополнительных проходов из-за часов).
Такой подход уменьшает зависимости и, следовательно, сложность кода, облегчает тестирование и обеспечивает повышение производительности. Это на самом деле просто включает в себя размышление о вещах с точки зрения отношений и состояния, а не идею «процесса», который «толкает» вещи. Пусть Angular справится со всем этим подъемом и сделает ваш дизайн простым и понятным. В следующем посте я расскажу о более сложном проекте, в котором показано полностью работающее приложение, использующее этот принцип без каких-либо зависимостей от $ scope внутри контроллеров.
Хотя я рассмотрел некоторые распространенные сценарии, связанные с привязкой данных, вы всегда будете сталкиваться с ситуациями, требующими манипулирования DOM. Это может быть что-то простое, например, обновление заголовка страницы на основе изменения контекста, или что-то более сложное, например рендеринг SVG, или установка фокуса на элемент управления на основе ошибки проверки. Это часто приводит к четвертой ошибке, которую, как я вижу, делают разработчики, которая заключается в прямом взломе DOM, часто с самого контроллера!
В следующем посте я поделюсь с вами, как справиться с этим с помощью AngularJS. Пожалуйста, уделите некоторое время, чтобы прокомментировать и поделиться своими мыслями со мной, и если вы считаете, что я могу предоставить дополнительную ценность, пожалуйста, не стесняйтесь — контактная форма на этой странице отправит мне электронное письмо напрямую, так что дайте мне знать, как я могу помочь !
Спасибо,