Angular 18: как обеспечить срабатывание ngAfterViewInit без Zone.js в компонентах OnPush

Тело:

Недавно я удалил zone.js из своего проекта Angular 18, чтобы оптимизировать производительность, и теперь столкнулся с проблемой, связанной с тем, что крючок жизненного цикла ngAfterViewInit не срабатывает последовательно в компоненте с ChangeDetectionStrategy.OnPush. Мой текущий обходной путь включает использование ApplicationRef.tick(), но я ищу более подходящее решение, которое соответствует реактивной парадигме и не полагается на триггеры обнаружения изменений вручную.

Контекст:

  • Угловая версия: 18.0.6
  • Я успешно удалил zone.js, но это привело к тому, что перехватчики жизненного цикла не сработали должным образом.
  • Структура приложения включает в себя административный компонент, который использует MatSidenav Angular Material и контролируется с помощью реактивных сигналов от @angular/core.

admin.comComponent.ts:


@Component({
  selector: 'app-admin',
  templateUrl: './admin.component.html',
  styleUrls: ['./admin.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export default class AdminComponent implements OnDestroy, AfterViewInit {
  sidenav = viewChild.required<MatSidenav>(MatSidenav);

  private destroy$ = new Subject<void>();
  private observer = inject(BreakpointObserver);
  private router = inject(Router);
  private accountService = inject(AccountService);
  private cdr = inject(ChangeDetectorRef);
  private appRef = inject(ApplicationRef);
  constructor() {
    this.router.events.pipe(
      filter(e => e instanceof NavigationEnd), takeUntil(this.destroy$)
    ).subscribe(() => {
      this.appRef.tick();
    });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  ngAfterViewInit() {
    this.observer
      .observe(['(max-width: 800px)'])
      .pipe(takeUntil(this.destroy$), delay(1))
      .subscribe((res: { matches: boolean }) => {
        this.setSidenav(res.matches);
        this.cdr.detectChanges();
      });

    this.router.events
      .pipe(
        takeUntil(this.destroy$),
        filter((e) => e instanceof NavigationEnd)
      )
      .subscribe(() => {
        if (this.sidenav().mode === 'over') {
          this.setSidenav(false);
        }
      });
  }

  logout() {
    this.accountService.logout();
  }

  private setSidenav(matches: boolean) {
    if (matches) {
      this.sidenav().mode = 'over';
      this.sidenav().close();
    } else {
      this.sidenav().mode = 'side';
      this.sidenav().open();
    }
  }
}

admin.comComponent.html:

<mat-toolbar color = "primary" class = "mat-elevation-z8">
  <button mat-icon-button *ngIf = "sidenav.mode === 'over'" (click) = "sidenav.toggle()">
    <mat-icon *ngIf = "!sidenav.opened">menu</mat-icon>
    <mat-icon *ngIf = "sidenav.opened">close</mat-icon>
  </button>
  <span class = "admin-panel-title">Admin Panel</span>
</mat-toolbar>

<mat-sidenav-container>
  <mat-sidenav #sidenav = "matSidenav" class = "mat-elevation-z8">
    <div class = "logo-container">
      <img routerLink = "/" alt = "logo" class = "avatar mat-elevation-z8 logo-admin" src = "../../../../assets/img/logo-s.png" />
    </div>
    <mat-divider></mat-divider>

    <ul class = "nav-list">
      <li class = "nav-item">
        <a routerLink = "/admin" routerLinkActive = "active" [routerLinkActiveOptions] = "{exact:true}">
          <mat-icon class = "nav-icon">home</mat-icon>
          <span>Dashboard</span>
        </a>
      </li>
      <li class = "nav-item">
        <a routerLink = "/admin/products" routerLinkActive = "active">
          <mat-icon class = "nav-icon">library_books</mat-icon>
          <span>Products</span>
        </a>
      </li>
      <li class = "nav-item">
        <a routerLink = "/admin/brands" routerLinkActive = "active">
          <mat-icon class = "nav-icon">branding_watermark</mat-icon>
          <span>Brands</span>
        </a>
      </li>
      <li class = "nav-item">
        <a routerLink = "/admin/product-types" routerLinkActive = "active">
          <mat-icon class = "nav-icon">branding_watermark</mat-icon>
          <span>Types</span>
        </a>
      </li>
      <li class = "nav-item">
        <a routerLink = "/admin/users" routerLinkActive = "active">
          <mat-icon class = "nav-icon">supervisor_account</mat-icon>
          <span>Users</span>
        </a>
      </li>
      <li class = "nav-item">
        <a (click) = "logout()" class = "nav-item-logout" routerLinkActive = "active">
          <i class = "fa fa-sign-out fa-2x nav-icon-logout"></i>
          <span>Logout</span>
        </a>
      </li>
    </ul>

    <mat-divider></mat-divider>
  </mat-sidenav>
  <mat-sidenav-content>
    <div class = "content mat-elevation-z8">
      <router-outlet></router-outlet>
    </div>
  </mat-sidenav-content>
</mat-sidenav-container>

ngAfterViewInit() в моем AdminComponent регистрируется только тогда, когда я явно вызываю this.appRef.tick(). Однако я бы предпочел не использовать ApplicationRef для этой цели и ищу решение, которое позволит Angular более естественно обрабатывать обновления, не возвращаясь к zone.js.

Проблема:

  • Метод ngAfterViewInit() не срабатывает должным образом без ручного вмешательства через ApplicationRef.tick().
  • Эта проблема возникает только после удаления zone.js, что указывает на проблему с механизмом обнаружения изменений Angular в среде без зон.

Вопрос: Как я могу гарантировать, что перехватчики жизненного цикла, такие как ngAfterViewInit, активируются соответствующим образом в приложении Angular с помощью ChangeDetectionStrategy.OnPush, когда zone.js удаляется? Существуют ли какие-либо рекомендуемые методы или шаблоны для управления обнаружением изменений вручную в таких случаях?

Что я пробовал:

  1. Использование ApplicationRef.tick() для запуска обнаружения изменений вручную.
  2. Подписка на события роутера и звонки ChangeDetectorRef.detectChanges() внутри подписок.
  3. Использование NgZone.run() для выполнения кода, обновляющего представление.

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

пожалуйста, поделитесь репозиторием stackblitz или github с проблемой, воспроизведенной для отладки, а также объясните шаги по репликации сценария.

Naren Murali 11.07.2024 20:19
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Angular и React для вашего проекта веб-разработки?
Angular и React для вашего проекта веб-разработки?
Когда дело доходит до веб-разработки, выбор правильного front-end фреймворка имеет решающее значение. Angular и React - два самых популярных...
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Эпизод 23/17: Twitter Space о будущем Angular, Tiny Conf
Мы провели Twitter Space, обсудив несколько проблем, связанных с последними дополнениями в Angular. Также прошла Angular Tiny Conf с 25 докладами.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
Мое недавнее углубление в Angular
Мое недавнее углубление в Angular
Недавно я провел некоторое время, изучая фреймворк Angular, и я хотел поделиться своим опытом со всеми вами. Как человек, который любит глубоко...
Освоение Observables и Subjects в Rxjs:
Освоение Observables и Subjects в Rxjs:
Давайте начнем с основ и постепенно перейдем к более продвинутым концепциям в RxJS в Angular
0
1
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я нашел решение своей проблемы. Проблема была связана с запуском моего приложения Angular без zone.js и использованием компонентов Angular Material, в частности MatSidenav.

Решение

Чтобы решить эту проблему, мне нужно было использовать функцию provideExperimentalZonelessChangeDetection() в массиве поставщиков моего AppModule. Эта настройка гарантирует, что Angular сможет правильно обрабатывать обнаружение изменений в бесзональной конфигурации.

AppModule:

app.module.ts

import { NgModule, provideExperimentalZonelessChangeDetection } from 

@NgModule({
  declarations: [
    AppComponent,
  ],
  bootstrap: [AppComponent],
  imports: [

  ],
  providers: [
    provideExperimentalZonelessChangeDetection(),  // Add this line
  ]
})
export class AppModule { }

Объяснение

  • Добавляя provideExperimentalZonelessChangeDetection() в массив поставщиков, Angular настраивается на правильную обработку обнаружения изменений без zone.js.
  • Это особенно важно при использовании компонентов Angular Material, таких как MatSidenav, которые для правильной работы полагаются на обнаружение изменений.

После внесения этого изменения мое приложение отлично работает без zone.js.

Другие вопросы по теме