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 подход, который повышает производительность без ущерба для реактивности и удобства сопровождения.

🤔 А знаете ли вы, что...
Angular предлагает мощную систему инъекции зависимостей для управления зависимостями и компонентами приложения.


60
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.