在开发环境和生产环境下构建时,Angular异步管道和路由器的组合展现出不同的行为。

3

我有一个由两个组件构成的非常简单的测试应用:

  • AppComponent
  • ListComponent

这两个组件具有相同的行为:在加载时,它们使用async管道从Observable中显示一系列项目。 AppComponent另外还有一个按钮和一个RouterOutlet,在单击按钮后,它会加载ListComponent

当这段代码被编译为开发环境时,即使用ng build命令,一切正常。但是当将相同的代码编译为生产环境时,即使用ng build --prod命令,行为就不同了。在第二种情况下,如果我单击按钮转到ListComponent,则ListComponent的Observable在页面加载时不再发出。

以下是代码(我还有一个 Stackblitz 示例,但该问题并未发生):

ListComponent

@Component({
  selector: 'app-list',
  template: `
    <input #searchField type="text">
    <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
  `,
})
export class ListComponent implements OnInit, AfterViewInit  {
  @ViewChild('searchField', {static: true}) searchField: ElementRef;
  search$: Observable<string>;
  itemsToShow$: Observable<string[]>;

  ngOnInit() {}

  ngAfterViewInit() {
    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );

    this.itemsToShow$ = this.itemsToShow();
  }

  itemsToShow() {
    let itemAsLocalVar: string[];
    return of(ITEMS).pipe(
      delay(10),
      tap(items => itemAsLocalVar = items),
      switchMap(() => combineLatest([this.search$])),
      map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
    );
  }
}

AppComponent

@Component({
  selector: 'app-root',
  template: `
    <input #searchField type="text">
    <div *ngFor="let item of itemsToShow$ | async">{{item}}</div>
    <router-outlet></router-outlet>
    <button (click)="goToList()">List</button>
  `
})
export class AppComponent implements OnInit, AfterViewInit  {
  @ViewChild('searchField', {static: true}) searchField: ElementRef;
  search$: Observable<string>;
  itemsToShow$: Observable<string[]>;

  constructor(
    private router: Router,
  ) {}

  ngOnInit() {}

  ngAfterViewInit() {
    this.search$ = merge(concat(
       of(''), 
       fromEvent(this.searchField.nativeElement, 'keyup')
     )).pipe(
       map(() => this.searchField.nativeElement.value)
     );
    this.itemsToShow$ = this.itemsToShow();
  }
  itemsToShow() {
    let itemAsLocalVar: string[];
    return of(ITEMS).pipe(
      delay(10),
      tap(items => itemAsLocalVar = items),
      switchMap(() => {
        return combineLatest([this.search$]);
      }),
      map(([searchFilter]) => itemAsLocalVar.filter(i => i.includes(searchFilter))),
      tap(i => console.log(i))
    );
  }
  goToList() {
    this.router.navigate(['list']);
  }
}

非常感谢对问题出现原因的任何想法。
任何想法

哪个版本? - Amit Baranes
Angular 7和8 - Picci
1个回答

1
一个奇怪/怪异的行为,但很好你发布了这个问题/疑问。
我认为在生产模式下出现问题是因为改变检测在生产模式和开发模式中的工作方式不同[https://blog.angularindepth.com/a-gentle-introduction-into-change-detection-in-angular-33f9ffff6f10]
在开发模式下,变更检测运行两次,以确保准确性[因此您可能会看到Expression changed.....异常https://blog.angularindepth.com/everything-you-need-to-know-about-the-expressionchangedafterithasbeencheckederror-error-e3fd9ce7dbb4]。请注意,您需要在ngAfterViewInit中设置您的observables。 由于有两个变更检测周期,在开发模式下,您会看到ListComponent正确呈现。第一次变更检测后,您已经在ngAfterViewInit中分配了observable,这会在第二次变更检测周期中检测到,并且组件按预期呈现。
在生产模式下,变更检测不会运行两次。它只运行一次,以提高性能。如果您再次点击“列表”按钮,则您的ListComponent将呈现列表,因为单击该按钮会运行Angular变更检测。
要解决此问题,您有以下选项- 1.通过注入强制进行变更检测:
constructor(private _cdref: ChangeDetectorRef) {

  }

并将您的ngAfterViewInit()更改为以下内容

ngAfterViewInit() {
    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );
    this.itemsToShow$ = this.itemsToShow();
    this._cdref.detectChanges();
  }

2. 将您的ngAfterViewInit()代码移动到ngOnInit()中,如下所示:

ngOnInit() {

    this.search$ = merge(concat(of(''), fromEvent(this.searchField.nativeElement, 'keyup'))).pipe(
      map(() => this.searchField.nativeElement.value)
    );
    this.itemsToShow$ = this.itemsToShow();

  }

我建议选择选项2。

我同意这个问题与在开发模式和生产模式下运行的变更检测有关。奇怪的是,只有与路由器结合使用时才会出现这种行为。如果您直接使用URL“myserver/list”转到ListComponent,则异步管道将订阅Observable,并且列表将立即显示在加载时间。至于将逻辑移动到“ngOnInit”,这是不可能的,因为Observable管道使用了“this.search $”,它依赖于通过“@ViewChild”设置的元素引用,其结果在“ngOnInit”中不可用,而是在“ngAfterViewInit”中可用。 - Picci
@Picci,你尝试将代码移动到“ngOnInit”中了吗?我建议你试一下。它完全正常工作。在回答之前,我已经测试过了。 - user2216584
移动到 ngOnInit 会设置 observables,直到下一个渲染周期 [通过异步管道] 才会执行。 - user2216584
您是正确的,我可以将逻辑移动到ngOnInit中。不过行为还是相当奇怪。无论如何,感谢您的帮助。 - Picci

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接