Angular Data Transfer: Observables, ngLet и все, что между ними

Feb 15 2023
Эффективная передача данных важна для успешной разработки приложений Angular. Использование наблюдаемых является ключевой функцией, которая значительно улучшает разработку.
Фото Джойс Адамс на Unsplash

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

Например, мы хотим создать компонент меню, который извлекает данные о меню из сервиса и предоставляет возможность управлять его состоянием (открыть/закрыть) с помощью кнопки или на основе изменений в маршрутизации.

Реализация сервиса будет примерно такой:

// menu.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject} from 'rxjs';

@Injectable({ providedIn: 'root' })
export class MenuService {
  // declare a BehaviorSubject to store the menu data
  private menuSubject = new BehaviorSubject({
    isOpen: false,
    items: [
      { name: 'Item 1', price: 10 },
      { name: 'Item 2', price: 20 },
      { name: 'Item 3', price: 30 },
    ],
  });
  // expose the menuSubject as an observable to other components
  public menu$ = this.menuSubject.asObservable();

  constructor() {}

  // helper method to close menu when routing changes
  public closeMenuWhenRoutingChange() {
    if (this.menuSubject.value.isOpen) {
      this.toggle();
    }
  }

  // method to toggle the menu state
  public toggle() {
    this.menuSubject.next({
      ...this.menuSubject.value,
      isOpen: !this.menuSubject.value.isOpen,
    });
  }
}

В нашем компоненте мы можем подписаться на menu$наблюдаемые и использовать значение из подписки для использования данных из menuService:

// menu.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { filter, Subject, takeUntil } from 'rxjs';
import { MenuService } from './menu.service';

@Component({
  /* ... */
  template: `
    <div class="container">
      <button (click)="toggle()">
        {{ menuData.isOpen ? 'Close' : 'Open' }} Menu
      </button>
      <ul *ngIf="menuData.isOpen">
          <li *ngFor="let item of menuData.items">
            {{ item.name }} - {{ item.price }}
          </li>
      </ul>
    </div>
  `
})
export class MenuComponent implements OnInit, OnDestroy {
  private readonly destroy$ = new Subject<void>();
  public menuData = { isOpen: false, items: [] };

  constructor(private menuService: MenuService, private router: Router) {}

  ngOnInit() {
    // listen to the menu$ observable and setting the menuData
    this.menuService.menu$
      .pipe(takeUntil(this.destroy$))
      .subscribe((menuData) => {
        this.menuData = menuData;
      });

    // listen to the Router events and close the menu when a navigation starts
    this.router.events
      .pipe(
        filter(e => e instanceof NavigationStart),
        takeUntil(this.destroy$),
      )
      .subscribe(() => {
        this.menuService.closeMenuWhenRoutingChange();
      });
  }

  toggle() {
    this.menuService.toggle();
  }

  // clean up the component subscriptions by completing the destroy$ subject
  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

Метод II — использование асинхронной трубы:

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

Мы можем немного изменить наш код, чтобы сделать это:

// menu.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { filter, Subject, takeUntil } from 'rxjs';
import { MenuService } from './menu.service';

@Component({
  /* ... */
  template: `
    <div class="container">
      <button (click)="toggle()">
        {{ (menuService.menu$ | async).isOpen ? 'Close' : 'Open' }} Menu
      </button>
      <ul *ngIf="(menuService.menu$ | async).isOpen">
          <li *ngFor="let item of (menuService.menu$ | async).items">
            {{ item.name }} - {{ item.price }}
          </li>
      </ul>
    </div>
  `
})
export class MenuComponent implements OnInit, OnDestroy {
  private readonly destroy$ = new Subject<void>();
  
  constructor(public menuService: MenuService, private router: Router) {}
  
  ngOnInit() {
    // now we can remove the first subscription from here

    this.router.events
      .pipe(
        filter(e => e instanceof NavigationStart),
        takeUntil(this.destroy$),
      )
      .subscribe(() => {
        this.menuService.closeMenuWhenRoutingChange();
      });
  }

  /* ... */
}

Однако использование нескольких asyncканалов в сложном шаблоне может привести к проблемам с производительностью, поскольку каждый канал создает новую подписку на наблюдаемый объект. Чтобы преодолеть это, простым и эффективным решением является использование нашей собственной директивы.

Метод III — использование директивы ngLet:

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

// ng-let.directive.ts
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

interface ContextModel<T> {
  ngLet: T;
}

@Directive({ selector: '[ngLet]' })
export class LetDirective<T> {
  // context that will hold the value passed to directive
  private context: ContextModel<T> = { ngLet: undefined };

  constructor(viewContainer: ViewContainerRef, templateRef: TemplateRef<ContextModel<T>>) {
    // render the TemplateRef using the context object, creating an embedded view within the ViewContainerRef
    // this allows us to bind the value of "ngLet" directive to the template and access it within the view
    viewContainer.createEmbeddedView(templateRef, this.context);
  }

  @Input() set ngLet(value: T) {
    this.context.ngLet = value;
  }
}

// menu.component.ts
import { Component, OnDestroy, OnInit } from '@angular/core';
import { NavigationStart, Router } from '@angular/router';
import { filter, Subject, takeUntil } from 'rxjs';
import { MenuService } from './menu.service';

@Component({
  /* ... */
  template: `
    <div class="container" *ngLet="menuService.menu$ | async as menu">
      <button (click)="toggle()">
        {{ menu.isOpen ? 'Close' : 'Open' }} Menu
      </button>
      <ul *ngIf="menu.isOpen">
        <li *ngFor="let item of menu.items">
          {{ item.name }} - {{ item.price }}
        </li>
      </ul>
    </div>
  `
})
export class MenuComponent implements OnInit, OnDestroy {
  /* ... */
}

Мы можем использовать операторы shareи shareReply(в зависимости от варианта использования) вместо использования этой директивы. Эти операторы позволяют нескольким подписчикам совместно использовать одно и то же выполнение наблюдаемого объекта.

Заключение

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

Рекомендации

  • библиотека ngrx-utils ngLet directive implementation.