Угловые компоненты 101 — обзор

Опубликовано: 2022-03-11

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

В этой статье я укажу правильные и неправильные способы использования компонентов Angular с примерами. Этот пост должен дать вам четкое представление о:

  • Определение компонентов Angular;
  • Когда вам следует создавать отдельные компоненты Angular; а также
  • Когда не стоит создавать отдельные компоненты Angular.

Изображение обложки для компонентов Angular

Прежде чем мы сможем перейти к правильному использованию компонентов Angular, я хочу кратко коснуться темы компонентов в целом. Вообще говоря, каждое приложение Angular по умолчанию имеет как минимум один компонент — root component . Оттуда, это зависит от нас, как разработать наше приложение. Обычно вы создаете компонент для отдельных страниц, а затем каждая страница содержит список отдельных компонентов. Как правило, компонент должен соответствовать следующим критериям:

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

Давайте представим сценарий, в котором у нас есть приложение с двумя страницами: « Предстоящие задачи » и « Выполненные задачи ». На странице « Предстоящие задачи » мы можем просматривать предстоящие задачи, помечать их как «выполненные» и, наконец, добавлять новые задачи. Точно так же на странице « Выполненные задачи » мы можем просмотреть выполненные задачи и пометить их как «отмененные». Наконец, у нас есть навигационные ссылки, которые позволяют нам перемещаться между страницами. При этом мы можем разделить следующую страницу на три раздела: корневой компонент, страницы, повторно используемые компоненты.

Диаграмма предстоящих задач и выполненных задач

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

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

 ── myTasksApplication ├── components │ ├── header-menu.component.ts │ ├── header-menu.component.html │ ├── task-list.component.ts │ └── task-list.component.html ├── pages │ ├── upcoming-tasks.component.ts │ ├── upcoming-tasks.component.html │ ├── completed-tasks.component.ts │ └── completed-tasks.component.html ├── app.component.ts └── app.component.html

Итак, давайте свяжем файлы компонентов с фактическими элементами на каркасе выше:

  • header-menu.component и task-list.component — повторно используемые компоненты, которые отображаются с зеленой рамкой на скриншоте каркаса;
  • upcoming-tasks.component и completed-tasks.component — страницы, которые отображаются с желтой рамкой на скриншоте каркаса выше; а также
  • Наконец, app.component — это корневой компонент, который отображается с красной рамкой на скриншоте каркаса.

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

Входные переменные

Входные переменные используются внутри компонентов для передачи некоторых данных из родительского компонента. В приведенном выше примере у нас может быть два входных параметра для task-list.componenttasks и listType . Соответственно, tasks будут представлять собой список строк, в котором каждая строка будет отображаться в отдельной строке, тогда как listType будет либо предстоящим , либо завершенным , что укажет, установлен ли флажок. Ниже вы можете найти небольшой фрагмент кода того, как может выглядеть фактический компонент.

 // task-list.component.ts import { Component, Input } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. constructor() { } }

Выходные переменные

Подобно входным переменным, выходные переменные также могут использоваться для передачи некоторой информации между компонентами, но на этот раз в родительский компонент. Например, для task-list.component у нас может быть выходная переменная itemChecked . Он будет информировать родительский компонент, если элемент был отмечен или не отмечен. Выходные переменные должны быть эмиттерами событий. Ниже вы можете найти небольшой фрагмент кода того, как компонент может выглядеть с выходной переменной.

 // task-list.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-task-list', templateUrl: 'task-list.component.html' }) export class TaskListComponent { @Input() tasks: string[] = []; // List of tasks which should be displayed. @Input() listType: 'upcoming' | 'completed' = 'upcoming'; // Type of the task list. @Output() itemChecked: EventEmitter<boolean> = new EventEmitter(); @Output() tasksChange: EventEmitter<string[]> = new EventEmitter(); constructor() { } /** * Is called when an item from the list is checked. * @param selected---Value which indicates if the item is selected or deselected. */ onItemCheck(selected: boolean) { this.itemChecked.emit(selected); } /** * Is called when task list is changed. * @param changedTasks---Changed task list value, which should be sent to the parent component. */ onTasksChanged(changedTasks: string[]) { this.taskChange.emit(changedTasks); } }
Возможное содержимое task-list.component.ts после добавления выходных переменных

Использование дочерних компонентов и связывание переменных

Давайте посмотрим, как использовать этот компонент в родительском компоненте и как выполнять различные типы привязок переменных. В Angular существует два способа привязки входных переменных — односторонняя привязка, что означает, что свойство должно быть заключено в квадратные скобки [] , и двусторонняя привязка, что означает, что свойство должно быть заключено в квадратные и круглые скобки. [()] . Взгляните на приведенный ниже пример и узнайте о различных способах передачи данных между компонентами.

 <h1>Upcoming Tasks</h1> <app-task-list [(tasks)]="upcomingTasks" [listType]="'upcoming'" (itemChecked)="onItemChecked($event)"></app-task-list>
Возможный контент upcoming-tasks.component.html

Пройдемся по каждому параметру:

  • Параметр tasks передается с использованием двусторонней привязки. Это означает, что в случае изменения параметра tasks в дочернем компоненте родительский компонент отразит эти изменения в переменной upcomingTasks задач. Чтобы разрешить двустороннюю привязку данных, необходимо создать выходной параметр по шаблону «[inputParameterName]Change», в данном случае tasksChange .
  • Параметр listType передается с использованием односторонней привязки. Это означает, что его можно изменить в дочернем компоненте, но это не отразится на родительском компоненте. Имейте в виду, что я мог бы присвоить параметру компонента значение 'upcoming' и вместо этого передать его — это не имело бы никакого значения.
  • Наконец, параметр itemChecked является функцией слушателя, и он будет вызываться всякий раз, когда onItemCheck выполняется для task-list.component . Если элемент отмечен, $event будет содержать значение true , но если он не отмечен, он будет содержать значение false .

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

Когда создавать отдельный компонент Angular

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

Схема компонентов Angular в действии

Разумно используйте компоненты Angular

Итак, когда вы должны создавать компоненты Angular?

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

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

Поскольку мы уже рассмотрели использование повторно используемых компонентов , давайте посмотрим на использование компонентов организации кода . Давайте представим, что у нас есть регистрационная форма, а внизу формы есть поле с Условиями . Обычно юридический язык имеет тенденцию быть очень большим и занимать много места — в данном случае в шаблоне HTML. Итак, давайте посмотрим на этот пример, а затем посмотрим, как мы потенциально можем его изменить.

Вначале у нас есть один компонент — Registration.component — который содержит все, включая registration.component форму, а также сами условия.

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div> <button (click)="onRegistrate()">Registrate</button>
Исходное состояние перед разделением </code>registration.component</code> на несколько компонентов

Шаблон теперь выглядит маленьким, но представьте, если бы мы заменили «Текст с очень длинными условиями» на реальный фрагмент текста, содержащий более 1000 слов, это сделало бы редактирование файла трудным и неудобным. У нас есть быстрое решение для этого — мы могли бы изобрести новый компонент terms-and-conditions.component , в котором будет храниться все, что связано с условиями. Итак, давайте посмотрим на файл HTML для terms-and-conditions.component .

 <div class="terms-and-conditions-box"> Text with very long terms and conditions. </div>
Недавно созданный HTML-шаблон </code>terms-and-conditions.component</code>

И теперь мы можем настроить компонент registration.component и использовать в нем terms-and-conditions.component .

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()">Registrate</button>
Обновлен шаблон </code>registration.component.ts</code> с компонентом организации кода.

Поздравляем! Мы только что уменьшили размер registration.component на сотни строк и упростили чтение кода. В приведенном выше примере мы внесли изменения в шаблон компонента, но тот же принцип можно применить и к логике компонента.

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

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

Когда следует избегать создания отдельного компонента Angular

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

Иллюстрация слишком большого количества компонентов

Слишком много компонентов замедлят работу.

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

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

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

 // log-button.component.ts import { Component, Input, Output } from '@angular/core'; @Component({ selector: 'app-log-button', templateUrl: 'log-button.component.html' }) export class LogButtonComponent { @Input() name: string; // Name of the button. @Output() buttonClicked: EventEmitter<boolean> = new EventEmitter(); constructor() { } /** * Is called when button is clicked. * @param clicked - Value which indicates if the button was clicked. */ onButtonClick(clicked: boolean) { console.log('I just clicked a button on this website'); this.buttonClicked.emit(clicked); } }
Логика некорректного компонента log-button.component.ts

И соответственно этот компонент сопровождается следующим html представлением.

 <button (click)="onButtonClick(true)">{{ name }}</button>
Шаблон неверного компонента log-button.component.html

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

 import { Directive } from '@angular/core'; @Directive({ selector: "[logButton]", hostListeners: { 'click': 'onButtonClick()', }, }) class LogButton { constructor() {} /** * Fired when element is clicked. */ onButtonClick() { console.log('I just clicked a button on this website'); } }
директива logButton , которую можно присвоить любому элементу

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

 <h2>Registration</h2> <label for="username">Username</label><br /> <input type="text" name="username" [(ngModel)]="username" /><br /> <label for="password">Password</label><br /> <input type="password" name="password" [(ngModel)]="password" /><br /> <app-terms-and-conditions></app-terms-and-conditions> <button (click)="onRegistrate()" logButton>Registrate</button>
Директива logButton используемая на кнопке формы регистрации

Теперь мы можем взглянуть на второй случай, в котором мы не должны создавать отдельные компоненты, и это противоположно компонентам оптимизации кода . Если вновь созданный компонент усложняет и увеличивает ваш код, в его создании нет необходимости. Возьмем в качестве примера наш registration.component . Одним из таких случаев было бы создание отдельного компонента для метки и поля ввода с кучей входных параметров. Давайте посмотрим на эту плохую практику.

 // form-input-with-label.component.ts import { Component, Input} from '@angular/core'; @Component({ selector: 'app-form-input-with-label', templateUrl: 'form-input-with-label.component.html' }) export class FormInputWithLabelComponent { @Input() name: string; // Name of the field @Input() id: string; // Id of the field @Input() label: string; // Label of the field @Input() type: 'text' | 'password'; // Type of the field @Input() model: any; // Model of the field constructor() { } }
Логика form-input-with-label.component

И это может быть представление для этого компонента.

 <label for="{{ id }}">{{ label }}</label><br /> <input type="{{ type }}" name="{{ name }}" [(ngModel)]="model" /><br />
Вид form-input-with-label.component

Конечно, объем кода в registration.component будет уменьшен, но сделает ли это общую логику кода более понятной и читабельной? Я думаю, мы ясно видим, что это делает код излишне более сложным, чем раньше.

Следующие шаги: угловые компоненты 102?

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

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

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