×

Як написати власний декоратор Angular та де застосувати цей підхід

Усі статті, обговорення, новини про Front-end — в одному місці. Підписуйтеся на телеграм-канал!

Привіт! Мене звати Артур, я — Front-end-розробник у компанії ISsoft Ukraine. Я працюю у даній сфері протягом останніх чотирьох років. Більшу частину цього часу я використовував Angular — один з найпопулярніших на сьогодні фреймворків для front-end розробки від Google, що дозволяє створювати динамічні single-page додатки.

Проблематика

Кожен застосунок, створений за допомогою Angular, по суті, складається з набору класів, що взаємодіють між собою за принципом DI (Dependency Injection). А от концептуально у всіх цих класів різне призначення та доволі чітка ієрархія: модулі, компоненти, директиви, пайпи та ще багато різних рішень для того, щоб покрити будь-які потреби процесу розробки.

Та як Angular має розрізняти всі ці складові, щоб правильно побудувати структуру застосунку? Саме тут починається магія TypeScript’у. Кожен з таких класів має власний декоратор.

Тож, що це таке — ці ваші декоратори? Це функція, яка додає певні метадані до класу, методу, властивості або параметру. Так, найбільш розповсюдженим, на мою думку, є декоратор @Component — яскравий приклад того, які саме дані ми можемо зв’язати з класом (стилі, розмітка тощо).

Тому приклад з класами, зазначений вище, — лише частина функціоналу, що може бути запроваджений за допомогою декораторів. Angular має доволі широкий набір вбудованих декораторів, як то @NgModule, @Component, @Input, @Output, @Injectable тощо, що покривають велику кількість потреб під час розробки.

Але як уникнути ситуацій, коли необхідно запровадити подібний функціонал в багатьох місцях проєкту, але не просто копіювати конкретний шматок логіки, а зробити це у логічніший спосіб?

Angular дозволяє створити власний декоратор, який задає компілятору алгоритм обробки тих самих класів, методів, властивостей та параметрів.

Створення власного Angular-декоратору

Розглянемо створення власного декоратора на конкретній ситуації. Під час підписки на Observable створюється об’єкт Subscription, що дозволяє керувати цією підпискою. Але цей об’єкт існує, допоки ми від нього не відпишемося або потік не завершиться.

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

Ідея полягає в тому, щоб відписка від відповідних Observables відбувалася тоді, коли викличеться метод ngOnDestroy. Цей функціонал може бути реалізований за допомогою коду нижче:

import { Observable, Subject } from 'rxjs';

export function DestroySubscriptions(constructor: Function): void {
  const defaultDestroy = constructor.prototype.ngOnDestroy;

  constructor.prototype.destroyed = function(): Observable<unknown> {
    this._unsub = this._unsub || new Subject();
    return this._unsub.asObservable();
  };

  constructor.prototype.ngOnDestroy = function () {
    if (defaultDestroy) {
      defaultDestroy.call(this);
    }

    if (this._unsub) {
      this._unsub.next();
      this._unsub.complete();
    }
  };
}

Бачимо, що в нас є декоратор, аргументом якого є конструктор класу, до якого він застосований. Спочатку ми створюємо властивість destroyed, яка дає змогу компоненту відслідковувати — чи викликався метод ngOnDestroy.

Оскільки функція, яку ми записуємо у цю властивість, не має власного контексту, код у цій функції створить у нашому компоненті, до якого застосовується декоратор, Subject, що завершиться, як тільки компонент «знищиться».

А далі ми вже перезаписуємо стандартний метод ngOnDestroy, додавши у нього логіку, яка буде емітити новостворену властивість. Тобто, загалом, ми просто оновили стандартний lifecycle hook компонента.

Тепер справа за використанням у компоненті.

@Component({
  selector: 'hello',
  template: `<h1>Hello {{name}}!</h1>`,
  styles: [`h1 { font-family: Lato; }`],
})
@DestroySubscriptions
export class HelloComponent implements OnInit, OnDestroy {
  @Input() name: string;

  private interval$: Observable<number> = interval(1000);

  ngOnInit(): void {
    this.interval$.pipe(takeUntil(this.destroyed())).subscribe(console.log);
  }

  ngOnDestroy(): void {}
}

Як використовувати новостворений декоратор

Розглянемо детальніше процес використання новоствореного декоратора. Спочатку нам треба вказати, для чого він буде використаний. Декоратор застосовується у форматі @functionName, де functionName — ім’я нашого декоратора. Оскільки він не має власних аргументів, дужки після нього можна не писати.

Також варто зазначити, що рекомендується додати метод ngOnDestroy та відповідний інтерфейс до реалізації компонента, бо можуть виникнути проблеми з AOT компіляцією.

Перейдімо до головної частини — підписки на Observable. В методі ngOnInit створено новий потік, на який ми підписуємося. За допомогою метода pipe обробляємо цей потік, вказуючи умову закінчення підписки. І за умову, очевидно, вказуємо додану нами властивість this.destroyed().

Ось ми і отримали реалізацію відписки або завершення потоку, на який ми підписалися.

Розглянемо іншу ситуацію. Припустимо, що у компонента є обов’язкова @Input властивість і нам потрібно вивести помилку в консоль, якщо ця властивість не була передана. Тож створимо спеціальний декоратор, який буде перевіряти її наявність.

export function Required(target: unknown, propertyName: string): void {
  Object.defineProperty(target, propertyName, {
    get() {
      throw new Error(
        `Property ${propertyName} is required for ${target.constructor.name} component`
      );
    },
    set(value) {
      Object.defineProperty(target, propertyName, {
        value,
        writable: true,
        configurable: true,
      });
    },
    configurable: true,
  });
}

Як ми можемо бачити, коли викликається даний декоратор, він визначає нову властивість для класу з того атрибуту, до якого він був застосований. І допоки значення не буде перезаписане зовні, getter цієї властивості буде повертати помилку.

Тепер розглянемо застосування даного декоратора.

export class HelloComponent {
  @Required
  @Input() name: string;
}

Ми застосовуємо наш декоратор таким чином, що вказаний вище (порядок декораторів неважливий). Тож результатом є декоратор, що виводить помилку у консолі, якщо якась з необхідних властивостей не передана.

Примітка: якщо дане рішення не працює з видимих на те причин (я зіткнувся з проблемою, що властивість ніяк не реагувала на декоратор), першочергово необхідно перевірити поле useDefineForClassFields (за замовчуванням може бути false, має бути true).

Висновок

Декоратори дають змогу додавати складний функціонал з мінімальними витратами робочого ресурсу, допомагають підвищити читабельність коду та зменшити кількість дублікацій однієї і тієї ж логіки в усьому застосунку.

Спектр застосування даного підходу величезний: кешування даних, запровадження конфігурацій для певних користувачів, дебагінг тощо. Я навів приклади найпростіших декораторів, що можуть серйозно допомогти уникнути доволі розповсюджених проблем. Тож і застосування у декораторів широке, та ще й велику частину функціоналу Angular надає з коробки.

Невід’ємним плюсом декораторів є те, що ми можемо застосувати, змінити або прибрати певний функціонал видаленням або додаванням всього одного «виразу».

Тож, сподіваюсь, стаття стане у пригоді і як знайомство з декораторами, і як поштовх до створення абсолютно нового підходу для ваших застосунків. Ви можете знайти проєкт з повним кодом тут.

Корисні посилання

👍ПодобаєтьсяСподобалось7
До обраногоВ обраному2
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

в angular 16 нарешті є обидва рішення з коробки, більше нічого непотрібно робити:
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@Input({required: true}) data: any;
data$.pipe(takeUntilDestroyed()).subscribe()

Треба було аж 15 версій чекати на

takeUntilDestroyed

Можна було весь цей час використовувати пакет ngneat/until-destroy
www.npmjs.com/...​age/@ngneat/until-destroy Ангуляр нічого свого в 16 версії практично не додає. Скопіював takeUntilDestroyed з пакета вище і signals з SolidJS.

Ще вставлю свої 5 копійок, якщо у вас 10 версія Ангуляру на проекті(як мінімум на старті таке 100% було, при чому декілька проміжних версій 10-ки так точно), то у вас все рівно не будуть працювати наведені в прикладі декоратори по причині того, що або у них(команди Ангуляру) дійсно як вони сказали баг був з інстансом класу і хуками, або просто невдала фіча, а саме: неможливо було перевизначити і викликати хуки компонент самостійно.

Так, це критичне issue було в декількох проміжних версіях десятки

Підписатись на коментарі