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

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

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

5 ответов

Решение

Благодаря ответу Гюнтера Цохбауэра (см. Комментарии) я получил его на работу.

Как я понял, детектор изменений 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, потому что он не отображает никаких элементов.

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

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

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

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

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

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

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

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

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

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

Я нашел решение для этого. Ненавижу называть это решением, но оно работает.

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

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

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 снова выполнять преобразование. И производительность намного лучше.

Просто установите для свойства значение false

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

Вы также можете создать собственную нечистую трубу для отслеживания внешних изменений. Проверьте исходные тексты 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);
  }

}

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

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

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

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