Статьи

Руководство по обеспечению качества компонентов Angular 1.5

Эта статья была рецензирована Марком Брауном и Юргеном Ван де Муром . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!

Ученый с различными колбами и банками


2017.01.10 : статья была обновлена, чтобы прояснить раздел об односторонней привязке и добавить информацию об одноразовых привязках.


В Angular 1 компоненты — это механизм, который позволяет вам создавать свои собственные элементы HTML. Это было возможно с директивами Angular в прошлом, но компоненты основаны на различных улучшениях, внесенных в Angular, и применяют лучшие методы их построения и проектирования.

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

Следует также отметить, что многие из лучших практик Angular 2 внедрены в Angular 1 с помощью нового API компонентов, что позволяет создавать приложения, которые впоследствии легче реорганизуются. Angular 2 повлиял на то, как мы думаем и спроектируем компоненты Angular 1, но есть еще ряд отличительных отличий. Angular 1 по-прежнему является очень мощным инструментом для создания приложений, поэтому я считаю, что стоит инвестировать в улучшение ваших приложений с помощью компонентов, даже если вы не планируете или не готовы перейти на Angular 2.

Что делает хороший компонент?

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

  • Изолированный — логика компонента должна быть инкапсулирована, чтобы оставаться внутренней и частной. Это помогает создать меньшую связь между компонентами.
  • Сосредоточенный — компоненты должны действовать как единое целое для одной основной задачи, что облегчает их рассуждение и часто более пригодно для повторного использования.
  • Одностороннее связывание — когда это возможно, компоненты должны использовать одностороннее связывание, чтобы уменьшить нагрузку на цикл дайджеста.
  • Использовать события жизненного цикла — жизненный цикл компонента начинается с экземпляра и заканчивается удалением со страницы. Лучше всего подключиться к этим событиям, чтобы поддерживать компонент с течением времени.
  • Четко определенный API — компоненты должны согласованно принимать конфигурацию в качестве атрибутов, поэтому их легко узнать, как их использовать.
  • Излучать события — для связи с другими компонентами они должны излучать события с соответствующими именами и данными.

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

Компоненты должны быть изолированы

Эволюция возможностей Angular 1 заключалась в том, чтобы включать изолированные и инкапсулированные компоненты, и на то есть веские причины. Некоторые из ранних приложений были тесно связаны с использованием $scope и вложенных контроллеров. Изначально Angular не давал решения, но теперь это делает.

Хорошие компоненты не раскрывают свою внутреннюю логику. Благодаря тому, как они разработаны, это довольно легко сделать. Однако не поддавайтесь искушению злоупотреблять компонентами, используя $scope если в этом нет крайней необходимости, например, при отправке / трансляции событий.

Компоненты должны быть сфокусированы

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

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

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

Компоненты приложения

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

 <body> <app></app> </body> 

Это рекомендуется, в первую очередь, для контроля четности Angular 2, поэтому при желании будет легче выполнить миграцию. Это также помогает в тестировании, перемещая весь корневой контент вашего приложения в один компонент, вместо того, чтобы помещать его в файл index.html . Компонент приложения также дает вам место для создания экземпляров приложения, так что вам не нужно делать это в методе run приложения, повышая тестируемость и уменьшая зависимость от $rootScope .

Этот компонент должен быть максимально простым. Вероятно, он будет содержать только шаблон и не будет содержать привязок или контроллера, если это возможно. Однако он не заменяет ng-app и не требует загрузки вашего приложения.

Компоненты маршрутизации

В прошлом мы связывали контроллеры и шаблоны в состоянии ui-router (или маршрута ngRoute ). Теперь можно связать маршрут непосредственно с компонентом, поэтому компонент по-прежнему является местом, в котором контроллер и шаблон сопряжены, но при этом он также является маршрутизируемым.

Например, с помощью ui-router мы должны связать шаблон и контроллер.

 $stateProvider.state('mystate', { url: '/', templateUrl: 'views/mystate.html', controller: MyStateController }); 

Теперь вы можете вместо этого связать URL-адрес непосредственно с компонентом.

 $stateProvider.state('mystate', { url: '/', component: 'mystate' }); 

Эти компоненты могут связывать данные из параметров маршрута (например, идентификатор элемента), и их роль заключается в том, чтобы сосредоточиться на настройке маршрута для загрузки других необходимых компонентов. Это, казалось бы, незначительное изменение в определении маршрутов на самом деле очень важно для возможности миграции на Angular 2, но также важно в Angular 1.5 для лучшей инкапсуляции шаблона и контроллера на уровне компонентов.

Angular 1 на самом деле имеет два модуля маршрутизатора, ngRoute и ngComponentRouter. Только ngComponentRouter поддерживает компоненты, но это также не рекомендуется. Я думаю, что лучше всего пойти с UI-роутером.

Компоненты с состоянием

Большинство уникальных компонентов, которые вы создадите для своего приложения, имеют состояние. Это то место, где вы фактически разместите бизнес-логику своего приложения, сделаете HTTP-запросы, обработаете формы и другие задачи с состоянием. Эти компоненты, вероятно, уникальны для вашего приложения, и они ориентированы на поддержание данных поверх визуального представления.

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

 .controller('ProfileCtrl', function ($scope, $http) { $http.get('/api/profile').then(function (data) { $scope.profile = data; }); }) .directive('profile', function() { return { templateUrl: 'views/profile.html', controller: 'ProfileCtrl' } }) 

С компонентами вы можете создать это лучше, чем раньше. В идеале вы также должны использовать сервис вместо $http непосредственно в контроллере.

 .component('profile', { templateUrl: 'views/profile.html', controller: function($http) { var vm = this; // Called when component is ready, see below vm.$onInit = function() { $http.get('/api/profile').then(function (data) { vm.profile = data; }); }; } }) 

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

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

Компоненты без состояния

Компоненты без сохранения состояния ориентированы на рендеринг без управления бизнес-логикой и не должны быть уникальными для какого-либо конкретного приложения. Например, большинство компонентов, которые используются для элементов пользовательского интерфейса (таких как элементы управления формы, карты и т. Д.), Также не обрабатывают логику, такую ​​как загрузка данных или сохранение формы. Они должны быть модульными, многоразовыми и изолированными.

Компонент без состояния может не нуждаться в контроллере, если он просто отображает данные или контролирует все в шаблоне. Они будут принимать входные данные от компонента с состоянием. Этот пример берет значение из компонента с состоянием (пример profile выше) и отображает аватар.

 .component('avatar', { template: '<img ng-src="http://example.com/images/{{vm.username}}.png" />', bindings: { username: '<' }, controllerAs: 'vm' }) 

Чтобы использовать его, компонент с состоянием должен передать имя пользователя через атрибут, например, <avatar username="vm.profile.username"> .

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

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

Это не новая функция с компонентами, но часто разумно использовать ее с компонентами. Цель односторонних привязок состоит в том, чтобы не загружать больше работы в цикл дайджеста, что является основным фактором производительности приложения. Теперь данные поступают в компонент без необходимости смотреть за его пределы (что вызывает некоторые проблемы связывания, которые существуют сегодня), и компонент может просто визуализировать себя с учетом этого ввода. Этот дизайн также подходит для Angular 2, который помогает с будущей миграцией.

В этом примере свойство title привязывается к компоненту только один раз на основе предоставленного начального значения. Если title изменяется каким-то внешним субъектом, он не отражается в компоненте. Синтаксис для обозначения привязки как одностороннего заключается в использовании символа < .

 bindings: { title: '<' } 

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

Компоненты должны учитывать одноразовые привязки

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

Это делается путем помещения :: перед выражением привязки. Это имеет смысл, только если вы знаете, что привязка ввода не изменится в течение жизненного цикла. В этом примере, если title — односторонняя привязка, она будет продолжать обновляться внутри компонента, но привязка здесь не будет обновляться, потому что мы обозначили ее как разовую.

 <h1>{{::title}}</h1> 

Компоненты должны использовать события жизненного цикла

Вы, вероятно, заметили функцию $ onInit как новую возможность. У компонентов есть жизненный цикл с соответствующими событиями, которые вы должны использовать для управления определенными аспектами компонента.

$onInit()

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

 controller: function() { var vm = this; console.log(vm.title); // May not yet be available! vm.$onInit = function() { console.log(vm.title); // Guaranteed to be available! } } 

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

 controller: function() { var vm = this; vm.$postLink = function() { // Usually safe to do DOM manipulation } } 

$onChanges()

Пока компонент активен, он может реагировать на изменения входных значений. Односторонние привязки по-прежнему будут обновлять ваш компонент, но у нас есть новая $onChanges события $onChanges для прослушивания при изменении входных данных.

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

 bindings: { title: '<' }, controller: function() { var vm = this; vm.$onChanges = function($event) { console.log($event.title.currentValue); // Get updated value console.log($event.title.previousValue); // Get previous value } } 

$onDestroy()

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

 controller: function() { var vm = this; vm.$onDestroy = function() { // Reset or remove any event listeners or watchers } } 

Компоненты должны иметь четко определенный API

Чтобы настроить и инициализировать компонент с набором данных, компонент должен использовать привязки для принятия этих значений. Иногда это рассматривается как API-компонент, который представляет собой просто другой способ описания того, как компонент принимает входные данные.

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

 bindings: { smb: '<', symbol: '<' } 

Надеюсь, вы думали, что symbol был лучше. Иногда разработчики также предпочитают добавлять префиксы компонентов и привязок, чтобы избежать конфликтов имен. Префикс компонентов имеет смысл, например, md-toolbar — это панель инструментов Material, но префикс всех привязок становится многословным и его следует избегать.

Компоненты должны испускать события

Для взаимодействия с другими компонентами компоненты должны генерировать пользовательские события. Существует много примеров использования службы и двусторонней привязки данных для синхронизации данных между компонентами, но события являются лучшим выбором при проектировании. События гораздо более эффективны в качестве средства связи со страницей (и основополагающей частью языка JavaScript и способа его работы в Angular 2, что не является совпадением).

События в Angular могут использовать $emit (вверх по дереву областей) или $broadcast (вниз по дереву областей). Вот краткий пример событий в действии.

 controller: function($scope, $rootScope) { var vm = this; vm.$onInit = function() { // Emits an event up to parents $scope.$emit('componentOnInit'); }; vm.$onDestroy = function() { // Emits an down child tree, from root $rootScope.$broadcast('componentOnDestroy'); }; } 

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

 <my-toolbar></my-toolbar> <my-tabs> <my-tab title="Description"></my-tab> <my-tab title="Reviews"></my-tab> <my-tab title="Support"></my-tab> </my-tabs> 

В этой ситуации компоненты my-tabs и my-tab , вероятно, знают друг о друге, потому что они работают вместе, чтобы создать набор из трех разных вкладок. Однако компонент my-toolbar находится за пределами их понимания.

Всякий раз, когда выбирается другая вкладка (которая будет четной на экземпляре компонента my-tab ), компонент my-tabs должен знать об этом, чтобы он мог настроить отображение вкладок для отображения этого экземпляра. Компонент my-tab может отправлять событие до родительского компонента my-tabs . Этот тип связи похож на внутреннюю связь между двумя компонентами, которые работают вместе для создания единой возможности (интерфейс с вкладками).

Однако что, если my-toolbar хочет знать, какая вкладка выбрана в данный момент, чтобы она могла менять кнопку справки в зависимости от того, что видно? Событие my-tab никогда не достигнет my-toolbar потому что оно не является родительским. Поэтому другой вариант — использовать $rootScope для $rootScope события по всему дереву компонентов, что позволяет любому компоненту прослушивать и реагировать. Потенциальным недостатком здесь является то, что ваше событие теперь достигает каждого контроллера, и если другой компонент использует то же имя события, вы можете вызвать непреднамеренные эффекты.

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

Резюме

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

  • Изолируйте свою логику. Сохраняйте как можно большую часть логики компонента как можно дальше от других аспектов приложения, чтобы обеспечить согласованность и качество.
  • Держите компоненты простыми и ориентированными на одну роль. Они могут быть сложными компонентами, но различные задачи одного компонента должны быть логически связаны как единое целое.
  • Используйте события жизненного цикла. Подключившись к жизненному циклу компонента, вы можете убедиться, что данные готовы в нужное время и что вы можете их очистить.
  • Используйте односторонние и одноразовые привязки. По возможности односторонние привязки более эффективны и способствуют хорошему дизайну, а одноразовые привязки могут ускорить работу вашего приложения. Вы всегда можете использовать событие $onChanges жизненного цикла для просмотра изменений.
  • Используйте события для общения. Компоненты могут взаимодействовать с помощью пользовательских событий, что соответствует принципам работы Angular 2 и лучшему дизайну.
  • Иметь четко определенный API. Убедитесь, что ваши компоненты четко названы и просты для понимания.

Используете ли вы компоненты в своих приложениях Angular 1.x? Или вы собираетесь подождать, пока не совершите прыжок на Angular 2? Я хотел бы услышать о вашем опыте в комментариях ниже.