Как повторно запускать все чистые трубы на все дерево компонентов в Angular 2

У меня есть чистая труба TranslatePipe, которая переводит фразы с использованием LocaleService, которая имеет locale$: Observable<string> текущую локаль. У меня также есть ChangeDetectionStrategy.OnPush для всех моих компонентов, включая AppComponent. Теперь, как я могу перезагрузить целое приложение, когда кто-то меняет язык? (испускает новое значение в locale$ наблюдаемом).

В настоящее время я использую location.reload() после того, как пользователь переключится между языками. И это раздражает, потому что вся страница перезагружается. Как я могу сделать это angular -way с помощью стратегии чистого и стратегии OnPush?

Ответ 1

Благодаря Günter Zöchbauer answer (см. комментарии), я получил его работу.

Как я понимаю, детектор изменений Angular работает следующим образом:

cd.detectChanges(); // Detects changes but doesn't update view.
cd.markForCheck();  // Marks view for check but doesn't detect changes.

Итак, вам нужно использовать оба способа, чтобы быстро перестроить все дерево компонентов.

1. Изменения шаблона

Чтобы перезагрузить все приложение, нам нужно скрыть и показать все дерево компонентов, поэтому нам нужно обернуть все в app.component.html в ng-container:

<ng-container *ngIf="!reloading">
  <header></header>
  <main>
    <router-outlet></router-outlet>
  </main>
  <footer></footer>
</ng-container>

ng-container лучше, чем div, потому что он не отображает никаких элементов.

Для поддержки async мы можем сделать что-то вроде этого:

<ng-container *ngIf="!(reloading$ | async)"> ... </ng-container>

reloading: boolean и reloading$: Observable<boolean> здесь указывает, что компонент в настоящее время перезагружается.

В компоненте я есть LocaleService, который имеет language$ наблюдаемый. Я буду слушать измененное языковое событие и выполнить перезагрузку приложения.

2. Пример синхронизации

export class AppComponent implements OnInit {
    reloading: boolean;

    constructor(
        private cd: ChangeDetectorRef,
        private locale: LocaleService) {

        this.reloading = false;
    }

    ngOnInit() {
        this.locale.language$.subscribe(_ => {
            this.reloading = true;
            this.cd.detectChanges();
            this.reloading = false;
            this.cd.detectChanges();
            this.cd.markForCheck();
        });
    }
}

3. Пример Aync

export class AppComponent implements OnInit {
    reloading: BehaviorSubject<boolean>;

    get reloading$(): Observable<boolean> {
        return this.reloading.asObservable();
    }

    constructor(
        private cd: ChangeDetectorRef, // We still have to use it.
        private locale: LocaleService) {

        this.reloading = new BehaviorSubject<boolean>(false);
    }

    ngOnInit() {
        this.locale.language$.subscribe(_ => {
            this.reloading.next(true);
            this.cd.detectChanges();
            this.reloading.next(false);
            this.cd.detectChanges();
        });
    }
}

Теперь нам не нужно cd.markForChanges(), но мы все равно должны сообщить детектору обнаружить изменения.

4. Маршрутизатор

Маршрутизатор не работает, как ожидалось. При перезагрузке приложения таким образом содержимое router-outlet станет пустым. Я еще не разрешил эту проблему, и переход на тот же маршрут может быть болезненным, потому что это означает, что любые изменения, внесенные пользователем в формы, например, будут изменены и потеряны.

5. OnInit

Вы должны использовать крючок OnInit. Если вы попытаетесь вызвать cd.detectChanges() внутри конструктора, вы получите сообщение об ошибке, потому что Angular еще не будет создавать компонент, но вы попытаетесь обнаружить изменения на нем.

Теперь вы можете подумать, что я подписываюсь на другую службу в конструкторе, и моя подписка будет срабатывать только после полной инициализации компонента. Но дело в том, что вы не знаете, как работает сервис! Если, например, он просто испускает значение Observable.of('en') - вы получите сообщение об ошибке, потому что, как только вы подписались - первый элемент испустит немедленно, пока компонент все еще не инициализирован.

Моя LocaleService имеет ту же самую проблему: субъект за наблюдаемым BehaviorSubject. BehaviorSubject является субъектом rxjs, который сразу же после подписания выдает значение по умолчанию. Поэтому, как только вы пишете this.locale.language$.subscribe(...) - подписка сразу же срабатывает хотя бы один раз, и только тогда вы будете ждать изменения языка.

Ответ 2

Чистые трубы запускаются только при изменении входного значения.

Вы можете добавить искусственное дополнительное значение параметра, которое вы изменяете

@Pipe({name: 'translate'})
export class TranslatePipe {
  transform(value:any, trigger:number) {
    ...
  }
}

а затем используйте его как

<div>{{label | translate:dummyCounter}}</div>

Всякий раз, когда dummyCounter обновляется, выполняется труба.

Вы также можете передать локаль в качестве дополнительного параметра вместо счетчика. Я не думаю, что использование |async для одного параметра канала будет работать, поэтому это может быть немного громоздким (ему нужно назначить поле, которое можно использовать в качестве параметра трубы)

Ответ 3

ЛУЧШЕЕ РЕШЕНИЕ ПРОИЗВОДИТЕЛЬНОСТИ:

Я понял решение для этого. Мне не нравится называть это решением, но оно работает.

У меня была такая же проблема с и pipeByBy. Я пробовал все решения здесь, но влияние производительности было ужасным.

Я просто добавил дополнительный аргумент моей трубе

let i of someArray | groupBy:'someField':updated" 
<!--updated is updated after performing some function-->

то в любое время, когда я выполняю обновление массива, я просто

updateArray(){
    //this can be a service call or add, update or delete item in the array
      .then.....put this is in the callback:

    this.updated = new Date(); //this will update the pipe forcing it to re-render.
}

Это заставляет мою трубку orderBy снова преобразовать. И производительность намного лучше.

Ответ 4

Просто установите свойство pure в false

@Pipe({
  name: 'callback',
  pure: false
})

Ответ 5

Вы также можете создать собственную нечистую трубу для отслеживания внешних изменений. Проверьте исходные тексты Async Pipe, чтобы понять основную идею.

Все, что вам нужно, это вызвать ChangeDetectorRef.markForCheck(); внутри вашей нечистой трубы каждый раз, когда ваш Observable возвращает новую строку локали. Мое решение:

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private subscription: Subscription;
  private lastInput: string;
  private lastOutput: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.subscription = this.globalizationService.currentLocale // <- Observable of your current locale
      .subscribe(() => {
        this.lastOutput = this.globalizationService.translateSync(this.lastInput); // sync translate function, will return string
        this.changeDetectorRef.markForCheck();
      });
  }

  ngOnDestroy(): void {
    if (this.subscription) {
      this.subscription.unsubscribe();
    }
    this.subscription = void 0;
    this.lastInput = void 0;
    this.lastOutput = void 0;
  }

  transform(id: string): string { // this function will be called VERY VERY often for unpure pipe. Be careful.
    if (this.lastInput !== id) {
      this.lastOutput = this.globalizationService.translateSync(id);
    }
    this.lastInput = id;
    return this.lastOutput;
  }
}

Или вы даже можете инкапсулировать AsyncPipe внутри своей трубы (не очень хорошее решение, например):

@Pipe({
  name: 'translate',
  pure: false
})
export class TranslatePipe implements OnDestroy, PipeTransform {

  private asyncPipe: AsyncPipe;
  private observable: Observable<string>;
  private lastValue: string;

  constructor(private readonly globalizationService: GlobalizationService,
              private readonly changeDetectorRef: ChangeDetectorRef) {
    this.asyncPipe = new AsyncPipe(changeDetectorRef);
  }

  ngOnDestroy(): void {
    this.asyncPipe.ngOnDestroy();
    this.lastValue = void 0;
    if (this.observable) {
      this.observable.unsubscribe();
    }
    this.observable = void 0;
    this.asyncPipe = void 0;
  }

  transform(id: string): string {
    if (this.lastValue !== id || !this.observable) {
      this.observable = this.globalizationService.translateObservable(id); // this function returns Observable
    }
    this.lastValue = id;

    return this.asyncPipe.transform(this.observable);
  }

}

Ответ 6

Прочитайте эту угловую документацию для обнаружения изменений в массиве, который будет использовать pipe.

Чтобы это исправить, создайте новый массив (с новыми изменениями) и назначьте ему массив, который вы хотите использовать в конвейере. На этот раз Angular обнаруживает, что ссылка на массив изменилась. Он выполняет конвейер и обновляет отображение новым массивом с новыми изменениями.

введите описание ссылки здесь