Добрый день. Меня зовут Юрик и я angular-разработчик. Остальные в комнате у психиатра: - здравствуй Юрик, мы рады тебя видеть.
Итак, для чего я начну эту статью, а по результатам голосования продолжу? Для этого есть 2 причины. Первая - достаточно много людей не пользуются всеми возможностями того фрэймворка, на котором работают, а делают как умеют. Вторая - написано много статей про SOLID, DRY, чистый код, но это больше теория, чтобы пройти собеседование или набить карму, чтобы потом самому пройти собеседование. В первом случае код будет работать и даже пройдет код-ревью, но о гибкости, правильном выборе архитектурных паттернов чаще всего можно забыть (не кидайтесь матом, я потом объясню). Во-втором случае, начинающий разработчик просто даже не всегда представляет то, что на самом деле эти принципы и паттерны означают. Как правило, люди пользуются чужим опытом, пока не наработают свой.
Так вот о чем будет разговор. Я соберу несколько примеров и дам несколько решений одной задачи. Расскажу о том, как пишут многие, как надо или не надо писать, преимущества и недостатки. Статья не рассчитана на совсем новичков. Если вы читаете эту статью - вы уже должны знать, что такое Angular, как создать компонент, что такое послойная архитектура, что такое паттерны проектирования. Ну, что-же. Преамбула закончена, давайте начинать.
Для начала, я буду показывать код, возможно диаграммы, графики. Но я не буду переписывать сюда весь Angular. На GitHub имеется проект, где будут все материалы.
Используемые фрэймворки и библиотеки: Angular 17, Angular Material, NGRX
Binding не то, чем кажется
Разработчики часто используют binding не то, что бы не правильно - скорее не эффективно. Часто Binding можно заменить более эффективным способом.
Binding - это работа JS/TS. Но веб-приложение работает не только на JS/TS. У нас имеется и HTML и стили CSS. Правило производительности гласит: производительность системы считается по производительности самого слабого звена. JS - это слабое звено. Производительность CSS выше, чем JS.
Так в чем же его не эффективность? Сейчас покажу. У нас имеется какой-то UI компонент. Сейчас он выглядит достаточно просто, примерно так:
В репозитории по ссылке выше - это компонент 'app‑test‑filter-1'
Итак, задача. т.к. компонент будет использоваться везде, то TITLE компонента можно будет изменять. Это простейшая задача, которая решается обычным @Input(). Большинство разработчиков сделают именно так. К примеру - Primeng. Вот код из их документации. А вот и как это выглядит:
Листинг нашего же компонента будет таким:
@Component({
selector: 'app-test-filter-1',
standalone: true,
imports: [
MatIconButton
],
templateUrl: './test-filter-1.component.html',
styleUrl: './test-filter-1.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestFilter1Component {
@Input()
public title: string = '';
}
А вот так это будет выглядеть в компоненте app.component.html
<div class="d-block pt-3 pb-3">
Самый обычный компонент
</div>
<div class="d-block w-100 h-100">
<app-test-filter-1 title="Title"></app-test-filter-1>
</div>
Задача решена? Да. Работает? Да. Если копнуть дальше, то работает правильно? Опять же да. Мы видим стратегию ChangeDetectionStrategy.OnPush
и зададим вопрос: будет ли триггерить ChangeDetection при смене title
? Ответ - да. Наше property - это строка, а как мы помним из JS, результатом любых действий над строкой будет новая строка, значит сменится reference, значит @Input выступит триггером и компонент пройдет этап рендеринга. Но что тут тогда не так? Это решение вполне можно использовать в работе, да тысячи разработчиков используют. А не так тут - Binding. Давайте копнем глубже. Что такое Binding с точки зрения Angular?
Выше я писал о том, что JS/TS - это слабое звено и вот тут Binding это подтверждает. Что происходит под капотом: Property title - это по сути переменная. Указывая в шаблоне точку биндинга {{ title }}
мы говорим ядру Angular, что вот тут вот будет изменение значения (сами не знаем когда) и это изменение надо будет проверять каждый раз когда проходит цикл рендеринга. т.е. мы создали область в памяти, которая не будет изменяться во всем жизненном цикле приложения, но сам Angular об этом даже не догадывается, потому что создали мы не теми средствами. Какой принцип разработки из SOLID нарушен? Правильно - S. Принцип единой ответственности. Биндинг предназначен для отслеживания изменений данных и их рендеринга. Я не буду затрагивать вопросы по NgZone потому-что не об этом статья. Но еще раз повторюсь, данный способ решения все-равно актуален и никто не будет бить по рукам за его использование. Как решить данную проблему? Оказывается очень просто.
В Angular компоненты разделены на шаблоны HTML и застраничные файлы TS. Учитывая, что шаблоны все-равно проходят через JS, они все-равно работают быстрее Binding. При любой возможности желательно статический контент определять в шаблонах.
Теперь давайте усложним задачу. Наш потенциальный фильтр должен не только показывать текст, но и к примеру, кнопки. Эта задача все-еще решаема через Binding. Можно определить еще одно property для настроек, поставить @Output для триггера событий. Это все можно сделать. Вы уже заметили, что мы начинаем усложнять бизнес-логику компонента путем добавления кода TS? Я в качестве примера могу даже написать это все. А можно проще? Да. Используем ng-content
. Темплейты работают быстрее кода, что нам мешает показать статичный шаблон вместо переменной? Ничего. Вот именно так и делаем. Рассмотрим компонент 'app-test-filter-2'
Посмотрим в листинг app.component.html
<div class="d-block pt-5 pb-3">
Компонент ng-content
</div>
<div class="d-block w-100 h-100">
<app-test-filter-2>
<mat-label>
<div class="d-block text-muted display-6">
Вот тут просто текст
</div>
</mat-label>
</app-test-filter-2>
</div>
<div class="d-block w-100 h-100 pt-5">
<app-test-filter-2>
<mat-label>
<button mat-raised-button color="primary"><mat-icon>add</mat-icon> А вот тут кнопка</button>
</mat-label>
</app-test-filter-2>
</div>
Внутри нашего фильтра находится верстка HTML, которая и рендерится внутри самого компонента. При этом, прошу заметить, работает инкапсуляция стилей. Стили, определенные в root компоненте нельзя перебить в child. Как это выглядит в самом компоненте:
<div class="filter-title">
<ng-container ngProjectAs="'div'">
<ng-content select="mat-label"></ng-content>
</ng-container>
<div class="mr-auto"></div>
<button mat-icon-button><img src="assets/img/icon/filter-gray-ico.svg"></button>
</div>
<div class="accordion">
<div class="d-flex d-flex-row justify-start align-center h-100">
<div class="d-block subtitle">
Фильтр по параметрам
</div>
</div>
</div>
Обращу внимание на строки 2-4. Мы имеем возможность выбирать то, что показывать в children компонента. Я выбрал <mat-label>
из библиотеки Angular Material. Но тут можно выбирать и x-path и названия классов. Как это выглядит в живую:
Итак, задача выполнена? Да. Ангуляр знает, что изменяемый контент статичен? Несомненно. Расширяемость компонента присутствует? Определенно. Принцип единой ответственности нарушен? Да мы даже ни строчки кода не написали. Такое решение лучше, чем через Binding? Да.
В дальнейшем я расскажу что такое плоский шаблон, как работает Injector, как работает странсфер данных по CQRS, как отделить бизнес-логику от инфраструктуры и многое многое другое. Принимаю конструктивную критику, всяческие пожелания и просто пальцы вверх.