Лучшие практики фреймворка Angular
Каждое руководство описывает либо хорошую, либо плохую практику, и все они имеют примеры. Все приведенные ниже правила разделены на несколько категорий: от самых важных до незначимых.
"Do" - это то, что стоит делать всегда.
"Consider" несет рекомендательный характер.
"Avoid" указывает на то, чего стоит избегать.
"Why?" объясняет, почему стоит следовать (или нет) той или иной рекомендации.
Соглашение о названиях
Некоторые примеры кода отображают пример, который имеет один или несколько схожих по названию сопутствующих файлов. Например, hero.component.ts
и hero.component.html
.
Руководство использует сокращение hero.component.ts|html|css|spec
для представления этих различных файлов. Использование этого сокращения делает структуру файлов этого руководства более удобочитаемой и краткой.
Единая ответственность
Применение принципа единственной ответственности (SRP) ко всем компонентам, сервисам и другим символам помогает сделать приложение более чистым, легким для чтения и поддержки, а также более тестируемым.
01-01 Правило одного компонента
Do: В одном файле - одна сущность. Это может быть компонент, сервис или директива. Но только один на файл.
Consider: Рассмотрите ограничение файлов до 400 строк кода.
Why: Один компонент в файле значительно упрощает его чтение, поддержку и избегание конфликтов с командами в системе контроля версий.
Why: Один компонент в файле избавляет от скрытых ошибок, которые часто возникают при объединении компонентов в файле, где они могут использовать общие переменные, создавать нежелательные замыкания или нежелательную связь с зависимостями.
Why: Один компонент может быть экспортирован как основной для своего файла, что упрощает ленивую загрузку с помощью маршрутизатора.
Ключевое здесь - сделать код переиспользуемым, легким для чтения и менее подверженным ошибкам.
Вот пример как делать НЕ СТОИТ:
/* avoid */ import { Component, NgModule, OnInit } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; interface Hero { id: number; name: string; } @Component({ selector: 'app-root', template: ` <h1>{{title}}</h1> <pre>{{heroes | json}}</pre> `, styleUrls: ['app/app.component.css'] }) class AppComponent implements OnInit { title = 'Tour of Heroes'; heroes: Hero[] = []; ngOnInit() { getHeroes().then(heroes => (this.heroes = heroes)); } } @NgModule({ imports: [BrowserModule], declarations: [AppComponent], exports: [AppComponent], bootstrap: [AppComponent] }) export class AppModule {} platformBrowserDynamic().bootstrapModule(AppModule); const HEROES: Hero[] = [ { id: 1, name: 'Bombasto' }, { id: 2, name: 'Tornado' }, { id: 3, name: 'Magneta' } ]; function getHeroes(): Promise<Hero[]> { return Promise.resolve(HEROES); // TODO: get hero data from the server; }
Хорошей практикой считается перераспределение компонента и его вспомогательных классов в их собственные, отдельные файлы. По мере роста приложения это правило становится приоритетным.
01-02 Небольшие функции
Следует определять небольшие функции, не более 75 строк.
Why: Небольшие функции легче тестировать, особенно когда они делают одну вещь и служат одной цели.
Why: Небольшие функции способствуют повторному использованию.
Why: Небольшие функции легче читать.
Why: Небольшие функции легче поддерживать.
Why: Небольшие функции помогают избежать ошибок, которые возникают с большими функциями, которые используют переменные из внешней области видимости, создают нежелательные замыкания или нежелательную связь с зависимостями.
Наименование
Соглашения об именовании имеют огромное значение для поддержки и читаемости кода. В этом руководстве рекомендуются соглашения об именовании для имен файлов.
02-01 Общие рекомендации по именованию
Do: Следует использовать одинаковые имена для всех файлов
Do: Следует придерживаться шаблона, который описывает функциональность файла, а затем его тип. Рекомендуемый шаблон: feature.type.ts.
Why:Соглашения об именовании помогают обеспечить единообразный способ быстрого поиска содержимого. Единообразие в проекте является важным. Единообразие в команде важно. Единообразие во всей компании обеспечивает огромную эффективность.
Why: Соглашения об именовании должны помогать быстрее находить нужный код и легче его понимать.
Why: Имена папок и файлов должны четко передавать своё назначение. Например, app/heroes/hero-list.component.ts может содержать компонент, управляющий списком героев.
02-02 Разделяйте имена файлов точками и тире
Do: Следует использовать тире для разделения слов в описательном имени.
Do: Следует использовать точки для разделения описательного имени от типа.
Do: Следует использовать однотипные имена типов для всех компонентов, следуя шаблону, который описывает функциональность компонента, а затем его тип. Рекомендуемый шаблон - feature.type.ts.
Do: Следует использовать обычные имена типов, включая .service, .component, .pipe, .module и .directive. При необходимости можно придумать дополнительные имена типов, но следует избегать их излишества.
Why: Имена типов обеспечивают единообразный способ быстро определить содержимое файла.
Why: Имена типов упрощают поиск конкретного типа файла с использованием техник нечеткого поиска редактора или среды разработки.
Why: Не сокращенные имена типов, такие как .service, описательны и однозначны. Сокращения, такие как .srv, .svc и .serv, могут вызвать путаницу.
Why: Имена типов обеспечивают сопоставление шаблонов для любых автоматизированных задач.
Do: Следует использовать однотипные имена для всех ресурсов, названных в соответствии с их представлением.
Do: Следует использовать CamelCase
для имен классов.
Do: Имя компонента должно совпадать с именем файла.
Do: Следует добавлять к имени компонента суффикс типа (например, Component, Directive, Module, Pipe или Service) для объекта этого типа.
Do: Следует давать файлу согласованный суффикс (например, .component.ts, .directive.ts, .module.ts, .pipe.ts или .service.ts) для файла этого типа.
Why: Однотипные соглашения облегчают быструю идентификацию и ссылку на ресурсы различных типов.
// app.component.ts @Component({ … }) export class AppComponent { } // heroes.component.ts @Component({ … }) export class HeroesComponent { } // hero-list.component.ts @Component({ … }) export class HeroListComponent { } // user-profile.service.ts @Injectable() export class UserProfileService { } // init-caps.pipe.ts @Pipe({ name: 'initCaps' }) export class InitCapsPipe implements PipeTransform { }
Do: Следует использовать однотипные имена для всех сервисов, названных по их особенности.
Do: Класс сервиса следует снабдить суффиксом Service
. Например, что-то, что получает данные или героев, следует называть DataService
или HeroService
.
Why: Обеспечивает однотипный способ быстрого идентификации ссылки на сервисы.
Why: Ясные названия сервисов, такие как Logger, не требуют суффикса.
Why: Названия сервисов, такие как Credit, являются существительными и требуют суффикса и должны быть названы с суффиксом, когда не очевидно, что это сервис или что-то другое.
// hero-data.service.ts @Injectable() export class HeroDataService { } // credit.service.ts @Injectable() export class CreditService { }
Do: Следует помещать логику загрузки и платформы для приложения в файл с именем main.ts.
Do: Следует включать обработку ошибок в логику загрузки.
Avoid: Избегайте помещения логики приложения в main.ts
. Вместо этого рассмотрите возможность размещения ее в компоненте или сервисе.
Avoid: Почему? Следует удерживать согласованную конвенцию для запуска приложения.
Why: Почему? Следует использовать знакомую конвенцию из других технологических платформ.
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule) .then(success => console.log(`Bootstrap success`)) .catch(err => console.error(err));
Следует использовать dashed-case
или kebab-case
для именования селекторов элементов компонентов.
Why: Это сохраняет согласованность имен элементов согласно спецификации для Пользовательских Элементов.
/* avoid */ @Component({ selector: 'tohHeroButton', templateUrl: './hero-button.component.html' }) export class HeroButtonComponent {}
@Component({ selector: 'toh-hero-button', templateUrl: './hero-button.component.html' }) export class HeroButtonComponent {}
02-07 Пользовательский префикс компонента
Do: Следует использовать значение селектора элемента через дефисы и в нижнем регистре; например, admin-users
.
Do: Следует использовать пользовательский префикс для селектора компонента. Например, префикс toh представляет собой Tour of Heroes, а префикс admin представляет собой область административной функции.
Do: Следует использовать префикс, который идентифицирует область функционала или само приложение.
Why: Предотвращает коллизии имен элементов с компонентами в других приложениях и с элементами HTML.
Why: Упрощает продвижение и обмен компонентами с другими приложениями.
Why: Компоненты легко идентифицировать в DOM.
/* avoid */ // HeroComponent is in the Tour of Heroes feature @Component({ selector: 'hero' }) export class HeroComponent {}
@Component({ selector: 'toh-hero' }) export class HeroComponent {}
Do: Следует использовать lowerCamelCase
для названия селекторов директив.
Why: Сохраняет согласованность имен свойств, определенных в директивах, которые связаны с видом, с именами атрибутов.
Why: Парсер HTML в Angular чувствителен к регистру и распознает нижний регистр с верблюжьими заглавными буквами.
Do: Следует использовать пользовательский префикс для селектора директив (например, префикс toh из "Tour of Heroes").
Do: Необходимо записывать селекторы в lowerCamelCase
, если только селектор не предназначен для сопоставления с DOM HTML-атрибутом.
Не следует добавлять префикс ng
к имени директивы, потому что этот префикс зарезервирован для Angular и его использование может вызвать трудноуловимые ошибки.
Why: Это предотвращает коллизии имен.
Why: Директивы легко идентифицировать.
/* avoid */ @Directive({ selector: '[validate]' }) export class ValidateDirective {}
@Directive({ selector: '[tohValidate]' }) export class ValidateDirective {}
Do: Следует использовать последовательные имена для всех пайпов, названные по их функциональности. Имя класса канала должно использовать UpperCamelCase
(общее соглашение для имен классов), а соответствующая строка имени должна использовать lowerCamelCase
. Строка имени не может содержать дефисы ("dash-case" или "kebab-case").
Why: Обеспечивает последовательный способ быстрого определения и ссылки на пайпы.
// ellipsis.pipe.ts @Pipe({ name: 'ellipsis' }) export class EllipsisPipe implements PipeTransform { } // init-caps.pipe.ts @Pipe({ name: 'initCaps' }) export class InitCapsPipe implements PipeTransform { }
02-10 Имена для юнит-тестов
Do: Следует называть файлы спецификаций тестов так же, как компонент, который они тестируют.
Do: Следует называть файлы спецификаций тестов с суффиксом .spec.
Why: Обеспечивает последовательный способ быстрого определения тестов.
Why: Обеспечивает шаблонное сопоставление для Karma или других сред для запуска тестов.
// components heroes.component.spec.ts hero-list.component.spec.ts hero-detail.component.spec.ts // services logger.service.spec.ts hero.service.spec.ts filter-text.service.spec.ts // pipes ellipsis.pipe.spec.ts init-caps.pipe.spec.ts
02-11 Имена для End-to-End (E2E) тестов
Do: Следует называть файлы спецификаций тестов End-to-End тестирования по имени функции, которую они тестируют, с суффиксом .e2e-spec.
Why: Обеспечивает последовательный способ быстрого определения тестов конечного-конечного тестирования.
Why: Обеспечивает шаблонное сопоставление для сред для запуска тестов и автоматизации сборки.
02-12 Имена для ngModule
Do: Следует добавлять к имени символа суффикс Module.
Do: Следует давать файлу расширение .module.ts
.
Do: Следует называть модуль по имени функции и папке, в которой он находится.
Why: Обеспечивает единообразие
Why: Верхний регистр первой буквы слова принят для идентификации объектов, которые можно создавать с использованием конструктора.
Why: Легко идентифицирует модуль как корень с тем же именем.
Do: Следует добавлять к имени класса отвечающего за роутинг суффикс RoutingModule
.
Do: Следует заканчивать имя файла RoutingModule
на -routing.module.ts
.
Why:RoutingModule
- это модуль, полностью ответственный за навигацию в приложен. Согласованное соглашение о классе и имени файла делает эти модули легко обнаруживаемыми и проверяемыми.
// app.module.ts @NgModule({ … }) export class AppModule { } // heroes.module.ts @NgModule({ … }) export class HeroesModule { } // villains.module.ts @NgModule({ … }) export class VillainsModule { }
Структура приложения и NgModules
При создании приложения важно иметь представление о долгосрочном развитии приложения.
Весь код приложения помещается в папку с именем src
. Все зависимости находятся в своих папках, с их собственным NgModule
. Весь контент представлен в папке assets
. Каждый компонент
, сервис
и pipe
находится в отдельных файлах. Все сторонние сценарии поставщиков хранятся в других папках.
Do: Следует структурировать приложение таким образом, чтобы можно было быстро находить код, легко определять его с первого взгляда, сохранять как можно более плоскую структуру и стараться избегать дублирования.
Do: Следует определять структуру в соответствии с этими четырьмя основными рекомендациями, перечисленными в порядке важности.
Why: Почему? LIFT обеспечивает согласованную структуру, которая хорошо масштабируется, разбита на модули и упрощает повышение эффективности разработчика путем быстрого нахождения кода. Чтобы подтвердить свою интуицию относительно определенной структуры, задайте себе вопрос: могу ли я быстро начать работу во всех связанных файлах для этой функции?
Do: Следует делать поиск кода интуитивно понятным и быстрым.
Why: Почему? Для эффективной работы необходимо быстро находить файлы, особенно когда вы не знаете (или не помните) названия файлов. Хранение связанных файлов рядом друг с другом в интуитивно понятном месте экономит время. Организованная структура поможет избежать путаницы.
Do: Следует называть файл так, чтобы сразу становилось понятно, что он содержит и представляет.
Do: Нужно быть описательным с названиями файлов и держать содержимое файла исключительно для одного компонента.
Avoid: Избегайте файлов с несколькими компонентами, несколькими сервисами или их смеси.
Why: Экономьте время, ища и выбирая код, и становитесь более эффективным. Длинные названия файлов намного лучше, чем короткие, но неясные сокращенные названия.
Иногда бывает выгодно отклониться от правила "одна сущность - один файл", когда у вас есть набор маленьких, тесно связанных функций, которые лучше обнаруживать и понимать в одном файле, чем в нескольких файлах.
04-04 Плоская структура
Do: Следует сохранять плоскую структуру папок как можно дольше. Рассмотрите создание подпапок, когда в папке набирается семь и более файлов. Рассмотрите настройку среды разработки для скрытия отвлекающих, несущественных файлов, таких как сгенерированные файлы .js
и .js.map
.
Why: Никто не хочет искать файл через семь уровней папок. Плоская структура легко просматривается.
04-05 Try DRY (Try to Don't Repeat Yourself)
Do: Стремитесь к соблюдению принципа DRY
Avoid: Если отказ от DRY
в конкретных ситуациях приведет к повышению удобства в работе с кодом - возможно им пожертвовать
Why: Быть DRY
важно, но не критично, если это жертвует другими элементами LIFT
. Вот почему это называется T-DRY
. Например, избыточно называть шаблон hero-view.component.html
, потому что с расширением .html
ясно, что это представление. Но если что-то не очевидно или отличается от конвенции, то лучше явно это указать.
04-06 Общие рекомендации по структуре
Do: Начинайте с малого, но имейте в виду, куда движется приложение в долгосрочной перспективе.
Do: Имейте представление о ближайших шагах в разработке и долгосрочной стратегии.
Do: Поместите весь код приложения в папку с названием src
.
Consider: Рассмотрите создание отдельной папки для компонента, когда он имеет несколько сопутствующих файлов (.ts, .html, .css и .spec).
Why: Это помогает сохранить структуру приложения компактной и легкой для обслуживания на начальных этапах, а также легко изменяемой по мере роста приложения.
Why: Компоненты часто состоят из четырех файлов (например, *.html, *.css, *.ts и *.spec.ts
) и могут быстро заполнить папку.
Пример корректной структуры файлов:
Хотя компоненты в отдельных папках широко предпочтительны, еще один вариант для небольших приложений - это хранить компоненты плоско (не в отдельных папках). Это добавляет до четырех файлов к существующей папке, но также уменьшает уровень вложенности папок.
04-07 Структура папок зависит от функционала
Создавайте папки с названиями, соответствующими областям функциональности, которые они представляют.
Do: Создавайте NgModule для разных модулей приложения Why: Разработчик может быстро найти код и определить, что представляет собой каждый файл с первого взгляда. Структура максимально плоская, и нет повторяющихся или избыточных названий.
Why: Помогает избежать захламления приложения за счет организации содержимого и его соответствия указаниям LIFT.
Why: Когда файлов много, например, 10 и более, их легче найти с помощью последовательной структуры папок, а в плоской структуре это затруднительно.
Why: NgModule позволяют лениво загружать маршрутизируемые функции.
Why: NgModule упрощают изоляцию, тестирование и повторное использование функций.
Why: Для получения дополнительной информации обратитесь к этому примеру структуры папок и файлов.
04-08 Корневой модуль приложения
Do: Создайте NgModule в корневой папке приложения, например, в /src/app
.
Why: Каждое приложение требует как минимум одного корневого NgModule
.
Consider: Рассмотрите возможность назвать корневой модуль app.module.ts
.
Why: Это упрощает поиск и идентификацию корневого модуля.
import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { AppComponent } from './app.component'; import { HeroesComponent } from './heroes/heroes.component'; @NgModule({ imports: [ BrowserModule, ], declarations: [ AppComponent, HeroesComponent ], exports: [ AppComponent ] }) export class AppModule {}
Do: NgModule создается для каждого независимого модуля приложения; например, модуль для раздела "Герои".
Do: Разместите модуль в папке с названием класса; например, в app/heroes
. Назовите файл модуля, отражающий название функциональной области и папки; например, app/heroes/heroes.module.ts
.
Do: Назовите файл модуля, отражающий название класса, папки и файла; например, app/heroes/heroes.module.ts
определяет HeroesModule.
Why: Модуль может скрывать или открывать свою реализацию для других модулей.
Why: Модуль отвечает за необходимые для работы компонентов.
Why: Благодаря использованию модулей можно реализовать LazyLoading.
Why: Модуль определяет четкие границы между функционалом приложения.
Why: Модули помогают распределить обязанности по разработке между разными командами.
Why: Модуль можно легко изолировать для тестирования.
04-10 Общий модуль
Do: Создайте модуль с именем SharedModule
в папке shared
; например, app/shared/shared.module.ts
определяет SharedModule
.
Do: Объявляйте компоненты, директивы и пайпы в общем модуле, когда эти элементы будут повторно использоваться и ссылаться в компонентах, объявленных в других модулях.
Consider: Рассмотрите использование имени SharedModule
, когда содержимое общего модуля ссылается на всё приложение.
Consider: Рассмотрите возможность не объявлять сервисы в общих модулях. Сервисы обычно являются синглтонами, которые предоставляются один раз для всего приложения или для определенного модуля функциональности. Однако существуют исключения. Например, в приведенном ниже примере обратите внимание, что SharedModule
предоставляет FilterTextService
. В данном случае это применимо, поскольку что сервис не хранит состояние; то есть, потребители сервиса не зависят от новых экземпляров.
Do: Импортируйте все модули, требуемые для активов в SharedModule; например, CommonModule
и FormsModule
.
Why: SharedModule
объявит переиспользуемые модули приложения; например, ngFor
в CommonModule
. Объявляйте все переиспользуемые компоненты, директивы и каналы в SharedModule
. Экспортируйте все переиспользуемые зависимости из SharedModule
.
Why: SharedModule
существует для того, чтобы сделать общие компоненты, директивы и пайпы доступными для использования в компонентах, а так же во многих других модулях.
Do: Избегайте объявления сервисов на уровне SharedModule. Это может привести к созданию нескольких экземпляров.
Why: Лениво загружаемый модуль, который импортирует SharedModule
, создаст свою собственную копию сервиса и, вероятно, получит нежелательные результаты.
04-11 Объединяйте LazyLoading функционал по папкам
Отдельный компонент или модуль могут быть загружены по требованию. Размещайте содержимое в папке которая подгружается через LazyLoading.
Why: Папка облегчает идентификацию и изоляцию содержимого функции.
04-12 Не импортируйте модули напрямую
Do: Избегайте непосредственного импорта модулей которые должны быть прогружены через LazyLoading
Why: Прямой импорт и использование модуля приведут к его немедленной загрузке, в то время как целью является загрузка его по требованию.
04-13 Не добавляйте логику фильтрации и сортировки в кастомные пайпы
Avoid: Избегайте добавления фильтраций или сортировок в пайпах.
Do: Предварительно вычисляйте логику фильтрации и сортировки в компонентах или сервисах перед привязкой модели в шаблонах.
Why: Фильтрация и особенно сортировка — это дорогостоящие операции. Поскольку Angular может вызывать методы пайпов многократно за секунду, операции сортировки и фильтрации могут серьезно ухудшить пользовательский опыт даже для списков умеренного размера.
Компоненты
Consider: Рассмотрите возможность давать компонентам селектор элемента, в отличие от атрибутных или селекторов классов.
Why: Компоненты имеют шаблоны, содержащие HTML и необязательный синтаксис шаблонов Angular. Они отображают контент. Разработчики помещают компоненты на страницу, как они бы сделали с нативными HTML-элементами и веб-компонентами.
Why: Глядя на HTML шаблон легче понять что является компонентом.
/* avoid */ @Component({ selector: '[tohHeroButton]', templateUrl: './hero-button.component.html' }) export class HeroButtonComponent {}
<!-- avoid --> <div tohHeroButton></div>
<toh-hero-button></toh-hero-button>
05-04 Извлекайте шаблоны и стили в отдельные файлы
Do: Разделяйте шаблоны и стили в отдельные файлы, когда их объем превышает 3 строки.
Do: Имя файла шаблона должно соответствовать формату [имя-компонента].component.html
, где [имя-компонента]
- название компонента.
Do: Имя файла стилей должно быть [имя-компонента].component.css
, где [имя-компонента]
- название компонента.
Do: Используйте относительные URL-адреса, начинающиеся с ./
, чтобы ссылаться на файлы шаблонов и стилей компонента.
Why: Большие встроенные шаблоны и стили усложняют понимание реализации компонента, снижая его читаемость и поддерживаемость.
Why: В большинстве редакторов отсутствуют подсказки синтаксиса и фрагменты кода при работе с встроенными шаблонами и стилями. Angular TypeScript Language Service
(в будущем) обещает устранить этот недостаток для HTML-шаблонов
в редакторах, которые его поддерживают, но это не касается CSS-стилей
.
Why: Использование относительных URL-адресов позволяет избежать изменений при перемещении файлов компонента в другие директории.
Why: Префикс ./
- стандартный способ указания относительных путей к файлам в Angular
и необходим для правильной работы компонента при перемещении или переименовании файлов.
/* avoid */ @Component({ selector: 'toh-heroes', template: ` <div> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes | async" (click)="selectedHero=hero"> <span class="badge">{{hero.id}}</span> {{hero.name}} </li> </ul> <div *ngIf="selectedHero"> <h2>{{selectedHero.name | uppercase}} is my hero</h2> </div> </div> `, styles: [` .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: .3em 0; height: 1.6em; border-radius: 4px; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #607D8B; line-height: 1em; position: relative; left: -1px; top: -4px; height: 1.8em; margin-right: .8em; border-radius: 4px 0 0 4px; } `] }) export class HeroesComponent { heroes: Observable<Hero[]>; selectedHero!: Hero; constructor(private heroService: HeroService) { this.heroes = this.heroService.getHeroes(); } }
// toh-heroes.component.ts @Component({ selector: 'toh-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent { heroes: Observable<Hero[]>; selectedHero!: Hero; constructor(private heroService: HeroService) { this.heroes = this.heroService.getHeroes(); } } // toh-heroes.component.scss .heroes { margin: 0 0 2em 0; list-style-type: none; padding: 0; width: 15em; } .heroes li { display: flex; } .heroes button { flex: 1; cursor: pointer; position: relative; left: 0; background-color: #EEE; margin: .5em; padding: 0; border-radius: 4px; display: flex; align-items: stretch; height: 1.8em; } .heroes button:hover { color: #2c3a41; background-color: #e6e6e6; left: .1em; } .heroes button:active { background-color: #525252; color: #fafafa; } .heroes button.selected { background-color: black; color: white; } .heroes button.selected:hover { background-color: #505050; color: white; } .heroes button.selected:active { background-color: black; color: white; } .heroes .badge { display: inline-block; font-size: small; color: white; padding: 0.8em 0.7em 0 0.7em; background-color: #405061; line-height: 1em; margin-right: .8em; border-radius: 4px 0 0 4px; } .heroes .name { align-self: center; } // toh-heroes.component.html <div> <h2>My Heroes</h2> <ul class="heroes"> <li *ngFor="let hero of heroes | async"> <button type="button" (click)="selectedHero=hero"> <span class="badge">{{hero.id}}</span> <span class="name">{{hero.name}}</span> </button> </li> </ul> <div *ngIf="selectedHero"> <h2>{{selectedHero.name | uppercase}} is my hero</h2/div> </div>
05-12 Декорируйте входные и выходные свойства с помощью декораторов
Do: Декорируйте входные и выходные свойства с помощью декораторов класса @Input()
и @Output()
вместо использования свойств inputs
и outputs
в метаданных @Directive
и @Component
:
Consider: Рассмотрите возможность размещения декораторов @Input()
или @Output()
на той же строке, что и декорируемое свойство.
Why: Так легче определить, какие свойства в классе являются входными или выходными.
Why: Если вам когда-либо потребуется изменить имя свойства или события, связанного с @Input()
или @Output()
, вы сможете сделать это в одном месте.
Why: Объявление метаданных, прикрепленных к директиве, короче и, следовательно, более читабельно.
Why: Размещение декоратора на той же строке обычно делает код более коротким и все еще легко идентифицирует свойство как входное или выходное.
/* avoid */ @Component({ selector: 'toh-hero-button', template: `<button type="button"></button>`, inputs: [ 'label' ], outputs: [ 'heroChange' ] }) export class HeroButtonComponent { heroChange = new EventEmitter<any>(); label: string; }
@Component({ selector: 'toh-hero-button', template: `<button type="button">{{label}}</button>` }) export class HeroButtonComponent { @Output() heroChange = new EventEmitter<any>(); @Input() label = ''; }
05-13 Избегайте переопределения имен входных и выходных параметров
Avoid: Избегайте псевдонимов для входных и выходных свойств, за исключением случаев, когда это имеет важное значение.
Why: Два имени для одного свойства (одно приватное, другое публичное)
Why: Вы должны использовать псевдоним, когда имя директивы также является входным свойством, и имя директивы не описывает свойство.
/* avoid pointless aliasing */ @Component({ selector: 'toh-hero-button', template: `<button type="button">{{label}}</button>` }) export class HeroButtonComponent { // Pointless aliases @Output('heroChangeEvent') heroChange = new EventEmitter<any>(); @Input('labelAttribute') label: string; }
@Component({ selector: 'toh-hero-button', template: `<button type="button" >{{label}}</button>` }) export class HeroButtonComponent { // No aliases @Output() heroChange = new EventEmitter<any>(); @Input() label = ''; }
05-14 Последовательность членов класса
Do: Размещайте свойства вверху, за которыми следуют методы.
Do: Размещайте приватные члены после публичных членов, в алфавитном порядке.
Why: Размещение членов в одинаковой последовательности упрощает чтение и мгновенно помогает определить, какие члены компонента выполняют какую функцию.
/* avoid */ export class ToastComponent implements OnInit { private defaults = { title: '', message: 'May the Force be with you' }; message: string; title: string; private toastElement: any; ngOnInit() { this.toastElement = document.getElementById('toh-toast'); } // private methods private hide() { this.toastElement.style.opacity = 0; setTimeout(() => this.toastElement.style.zIndex = 0, 400); } activate(message = this.defaults.message, title = this.defaults.title) { this.title = title; this.message = message; this.show(); } private show() { console.log(this.message); this.toastElement.style.opacity = 1; this.toastElement.style.zIndex = 9999; setTimeout(() => this.hide(), 2500); } }
export class ToastComponent implements OnInit { // public properties message = ''; title = ''; // private fields private defaults = { title: '', message: 'May the Force be with you' }; private toastElement: any; // public methods activate(message = this.defaults.message, title = this.defaults.title) { this.title = title; this.message = message; this.show(); } ngOnInit() { this.toastElement = document.getElementById('toh-toast'); } // private methods private hide() { this.toastElement.style.opacity = 0; setTimeout(() => this.toastElement.style.zIndex = 0, 400); } private show() { console.log(this.message); this.toastElement.style.opacity = 1; this.toastElement.style.zIndex = 9999; setTimeout(() => this.hide(), 2500); } }
05-15 Делегируйте сложную логику компонента сервисам
Do: Ограничьте логику в компоненте только тем, что необходимо для представления. Вся остальная логика должна быть делегирована сервисам.
Do: Перемещайте повторно используемую логику в сервисы и держите компоненты простыми, сосредоточенными на своей намеченной цели.
Why: Логика может быть использована несколькими компонентами, когда она размещена в сервисе и представлена в виде функции.
Why: Логику в сервисе легче изолировать в модульном тесте
, а вызывающая логика в компоненте может быть легко замокирована
.
Why: Убирает зависимости и скрывает детали реализации от компонента.
Why: Улучшает читаемость кода компонента.
/* avoid */ import { OnInit } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { catchError, finalize } from 'rxjs/operators'; import { Hero } from '../shared/hero.model'; const heroesUrl = 'http://angular.io'; export class HeroListComponent implements OnInit { heroes: Hero[]; constructor(private http: HttpClient) {} getHeroes() { this.heroes = []; this.http.get(heroesUrl).pipe( catchError(this.catchBadResponse), finalize(() => this.hideSpinner()) ).subscribe((heroes: Hero[]) => this.heroes = heroes); } ngOnInit() { this.getHeroes(); } private catchBadResponse(err: any, source: Observable<any>) { // log and handle the exception return new Observable(); } private hideSpinner() { // hide the spinner } }
import { Component, OnInit } from '@angular/core'; import { Hero, HeroService } from '../shared'; @Component({ selector: 'toh-hero-list', template: `...` }) export class HeroListComponent implements OnInit { heroes: Hero[] = []; constructor(private heroService: HeroService) {} getHeroes() { this.heroes = []; this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } ngOnInit() { this.getHeroes(); } }
05-16 Не добавляйте префикс для @Output
Do: Называйте события без префикса on.
Do: Называйте методы обработчиков событий с префиксом on
, за которым следует название события.
Why: Нейминг получается слишком схожий с DOM-событиями, такими как нажатие кнопки.
Why: Angular позволяет использовать альтернативный синтаксис on-*. Если само событие было бы с префиксом on, это привело бы к вызову привязки on-onEvent.
/* avoid */ // app/heroes/hero.component.ts @Component({ selector: 'toh-hero', template: `...` }) export class HeroComponent { @Output() onSavedTheDay = new EventEmitter<boolean>(); } // app/app.component.html <!-- avoid --> <toh-hero (onSavedTheDay)="onSavedTheDay($event)"></toh-hero>
// app/heroes/hero.component.ts export class HeroComponent { @Output() savedTheDay = new EventEmitter<boolean>(); } // app/app.component.html <toh-hero (savedTheDay)="onSavedTheDay($event)"></toh-hero>
05-17 Логика вычислений должна быть в ts-файлах
Do: Лучше всего размещать вычислений в классе компонента, а не в самом шаблоне.
Why: Это позволяет сосредоточить логику в одном месте (в классе компонента), вместо ее разброса по двум местам.
Why: Хранение логики в классе компонента, а не в шаблоне, улучшает возможность проведения тестирования, облегчает поддержку и повторное использование.
/* avoid */ @Component({ selector: 'toh-hero-list', template: ` <section> Our list of heroes: <toh-hero *ngFor="let hero of heroes" [hero]="hero"> </toh-hero> Total powers: {{totalPowers}}<br> Average power: {{totalPowers / heroes.length}} </section> ` }) export class HeroListComponent { heroes: Hero[]; totalPowers: number; }
@Component({ selector: 'toh-hero-list', template: ` <section> Our list of heroes: <toh-hero *ngFor="let hero of heroes" [hero]="hero"> </toh-hero> Total powers: {{totalPowers}}<br> Average power: {{avgPower}} </section> ` }) export class HeroListComponent { heroes: Hero[]; totalPowers = 0; get avgPower() { return this.totalPowers / this.heroes.length; } }
05-18 Инициализация входных параметров
Компиляторная опция --strictPropertyInitialization
в TypeScript гарантирует, что класс инициализирует свои свойства на этапе конструктора. При включении этой опции компилятор TypeScript
сообщает об ошибке, если класс не устанавливает значение для любого свойства, которое не является явно помеченным как необязательное.
По умолчанию Angular рассматривает все свойства @Input
как необязательные. Когда это возможно, рекомендуется включить проверку -strictPropertyInitialization
, предоставив значение по умолчанию для всех входных значений.
@Component({ selector: 'toh-hero', template: `...` }) export class HeroComponent { @Input() id = 'default_id'; }
Если сложно определить значение по умолчанию, используйте символ "?" для явного обозначения свойства как необязательного.
@Component({ selector: 'toh-hero', template: `...` }) export class HeroComponent { @Input() id?: string; process() { if (this.id) { // ... } } }
Вам может потребоваться обязательное поле @Input
, что означает, что все пользователи вашего компонента обязаны передавать этот атрибут. В таких случаях используйте значение по умолчанию. Простое подавление ошибки TypeScript с помощью ! недостаточно и его следует избегать, потому что это предотвращает проверку типа, чтобы гарантировать ввод.
@Component({ selector: 'toh-hero', template: `...` }) export class HeroComponent { // The exclamation mark suppresses errors that a property is // not initialized. // Ignoring this enforcement can prevent the type checker // from finding potential issues. @Input() id!: string; }
Директивы
06-01 Используйте директивы для улучшения элемента
Do: Используйте атрибутные директивы, когда у вас есть логика представления без шаблона.
Why: Атрибутные директивы не имеют ассоциированного шаблона.
Why: К элементу можно применить более одной атрибутной директивы.
// app/shared/highlight.directive.ts @Directive({ selector: '[tohHighlight]' }) export class HighlightDirective { @HostListener('mouseover') onMouseEnter() { // do highlight work } } // app/app.component.html <div tohHighlight>Bombasta</div>
06-03 @HostListener и @HostBinding
Do: Рассмотрите использование декораторов @HostListener
и @HostBinding
метаданным в директивах @Directive
и @Component
.
Why: Свойство, связанное с @HostBinding
, или метод, связанный с @HostListener
, можно изменить только в одном месте - в классе директивы
. Если вы используете метаданные, вам придется изменить как объявление свойства/метода в классе директивы, так и метаданные в декораторе, связанном с директивой.
import { Directive, HostBinding, HostListener } from '@angular/core'; @Directive({ selector: '[tohValidator]' }) export class ValidatorDirective { @HostBinding('attr.role') role = 'button'; @HostListener('mouseenter') onMouseEnter() { // do work } }
Сравним с реализацией через метаданные host
import { Directive } from '@angular/core'; @Directive({ selector: '[tohValidator2]', host: { '[attr.role]': 'role', '(mouseenter)': 'onMouseEnter()' } }) export class Validator2Directive { role = 'button'; onMouseEnter() { // do work } }
Сервисы
Do: Используйте сервисы как синглтоны в пределах одного инжектора. Используйте их для обмена данными.
Why: Сервисы идеально подходят для обмена методами в пределах модуля или всего приложения.
Why: Сервисы идеально подходят для обмена состоянием в памяти.
export class HeroService { constructor(private http: HttpClient) { } getHeroes() { return this.http.get<Hero[]>('api/heroes'); } }
07-02 Принцип единой ответственности
Do: Создавайте сервисы которые отвечают за конкретный функционал.
Do: Создавайте новый сервис, когда текущий начинает выходить за рамки своей основной цели.
Why: Когда у сервиса есть несколько обязанностей, его становится сложно тестировать.
Why: Когда у сервиса есть несколько обязанностей, каждый компонент или сервис, который его инжектирует, несет на себе всю их нагрузку.
07-03 Глобальное использование сервиса
Do: Подключайте сервис на уровне всего приложения, используя { providedIn: 'root' }
Why: Инжектор Angular работает по иерархическому принципу.
Why: Когда сервис предоставлен корневому инжектору, экземпляр этого сервиса общий и доступен в каждом классе, который его использует. Это особенно полезно, когда сервис предоставляет методы или управляет состоянием приложения.
Why: Регистрируя сервис в декораторе @Injectable
, оптимизационные инструменты, такие как те, что использует Angular CLI
для сборки продакшн версии, могут производить оптимизацию кода и удалять сервисы, которые не используются в приложении.
Если различным компонентам требуются разные экземпляры сервиса, то лучше предоставить сервис на уровне компонента, чтобы каждый получал свой отдельный экземпляр.
// src/app/treeshaking/service.ts @Injectable({ providedIn: 'root', }) export class Service { }
07-04 Используйте декоратор @Injectable()
Do: Предпочтительнее использовать декоратор класса @Injectable()
вместо декоратора параметра @Inject
, когда в сервисе используются DI сервисы.
Why: Механизм внедрения зависимостей (DI) Angular разрешает зависимости сервиса на основе объявленных типов параметров его конструктора.
Why: При использовании зависимостей, связанных с типовыми токенами, синтаксис @Injectable()
гораздо менее громоздок по сравнению с использованием @Inject()
для каждого отдельного параметра конструктора.
// app/heroes/shared/hero-arena.service.ts /* avoid */ export class HeroArena { constructor( @Inject(HeroService) private heroService: HeroService, @Inject(HttpClient) private http: HttpClient) {} }
// app/heroes/shared/hero-arena.service.ts @Injectable() export class HeroArena { constructor( private heroService: HeroService, private http: HttpClient) {} }
Сервисы данных
08-01 Общение с бекендом через сервис
Do: Переносите логику работы с данными и общение с сервером в отдельные сервисы.
Do: Делайте сервисы данных ответственными за выполнение запросов к серверу, сохранение в локальное хранилище, хранение в памяти или любые другие операции с данными.
Why: Задача компонента - представление и сбор данных для отображения. Ему не следует беспокоиться о том, как получить данные, ему важно только знать, куда обратиться за ними. Разделение логики получения данных в сервисы помогает упростить компонент и сделать его более сфокусированным на представлении.
Why: Такой подход упрощает тестирование запросов к данным при проверке компонента, который использует сервис данных.
Why: Детали управления данными, такие как заголовки, методы HTTP, кэширование, обработка ошибок и логика повтора, не должны заботить компоненты и другие части приложения.
Сервис данных берет на себя эти задачи, что позволяет проще разрабатывать и тестировать код, который использует данные.
Жизненный цикл компонентов
09-01 Реализация интерфейсов методов жизненного цикла
Реализуйте интерфейсы хуков жизненного цикла.
Why: Интерфейсы жизненного цикла содержат типизированные сигнатуры методов. Их использование поможет обнаружить ошибки разного рода.
// app/heroes/shared/hero-button/hero-button.component.ts /* avoid */ @Component({ selector: 'toh-hero-button', template: `<button type="button">OK</button>` }) export class HeroButtonComponent { onInit() { // misspelled console.log('The component is initialized'); } }
@Component({ selector: 'toh-hero-button', template: `<button type="button">OK</button>` }) export class HeroButtonComponent implements OnInit { ngOnInit() { console.log('The component is initialized'); } }
Это вольный перевод оригинального материала