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

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

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


person EwanCoder    schedule 26.01.2017    source источник


Ответы (6)


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

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

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

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

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

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

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

person Günter Zöchbauer    schedule 26.01.2017
comment
Спасибо, это один из способов решить эту проблему, но у меня много строк в моих шаблонах и много кода, например {{'Это какой-то текст' | translate}}, так что наличие дополнительного параметра, который мне пришлось бы помещать везде, повредит... Нет ли способа перезагрузить все приложение без перезагрузки всей страницы? - person EwanCoder; 26.01.2017
comment
Вы можете поместить ngIf="expr" в самый внешний элемент вашего AppComponent, изменить expr на false, запустить ChangeDetectorRef.detectChanges(), снова установить его на true и снова запустить detectChanges(). Я думаю, что это самый простой способ. Таким образом, все будет удалено и прочитано. Возможно, вам придется снова перейти к текущему маршруту (хотя я не уверен, как маршрутизатор вообще реагирует на это, но я думаю, что это достаточно просто, чтобы просто попробовать) - person Günter Zöchbauer; 26.01.2017
comment
Работал..! Спасибо - person Renil Babu; 20.03.2018

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

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

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

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

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

person Judson Terrell    schedule 14.02.2018
comment
Вы, сэр, только что спасли мой день. Благодарю вас! :) - person Florent Arlandis; 12.09.2019

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

Насколько я понимаю, детектор изменений 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. Пример Айнк

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. Если вы попытаетесь вызвать cd.detectChanges() внутри конструктора, вы получите ошибку, потому что angular еще не соберет компонент, но вы попытаетесь обнаружить в нем изменения.

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

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

person EwanCoder    schedule 26.01.2017
comment
Вы можете использовать <ng-container *ngIf="!reloading">. Он был добавлен, чтобы иметь возможность использовать более распространенный синтаксис, но с тем же поведением, что и элемент <template>. - person Günter Zöchbauer; 26.01.2017
comment
Обновлено с помощью <ng-container>. - person EwanCoder; 26.01.2017
comment
AFAIK cd.detectChanges(); запускает обнаружение изменений немедленно и синхронно, а cd.markForCheck(); запускает обнаружение изменений для этого компонента в следующем цикле обнаружения изменений (несколько позже). - person Günter Zöchbauer; 26.01.2017
comment
Когда я попытался использовать cd.detectChanges() без cd.markForCheck() в конце - это просто не сработало. Он также не работает только с cd.markForCheck(). - person EwanCoder; 26.01.2017
comment
Возможно связано с роутером. Возможно, ApplicationRef.tick() в этом случае является лучшим способом вызвать обнаружение изменений для всего приложения. Это может даже исправить 4. - person Günter Zöchbauer; 26.01.2017
comment
Ваш 5. немного странный. Если этот код находится в constructor, this.reloading.next() и следующий код будет вызываться одновременно с перемещением его в ngOnInit. Он будет вызван, когда this.locale.language$.subscribe(...) создаст событие. - person Günter Zöchbauer; 26.01.2017
comment
ApplicationRef.tick() тоже ничего не сделал) Это первое, что я попробовал, когда возникла эта проблема. Так или иначе, я выложил то, что у меня работает, теперь пытаюсь бороться с Роутером. Если я приду к более красивому решению, я обновлю ответ. Может быть, angular должен включать какой-то метод, например ApplicationRef.updateWholeComponentTreeWithAllRouters()? - person EwanCoder; 26.01.2017
comment
В порядке. В любом случае спасибо за запись :) - person Günter Zöchbauer; 26.01.2017
comment
Кроме того, я не могу заставить маршрутизатор-выход обновляться. Если я перехожу к тому же маршруту или событию на другой маршрут, он все еще пуст (после того, как я обнаружил изменения). - person EwanCoder; 26.01.2017
comment
Сложно сказать. Не могли бы вы попробовать воспроизвести в плункере? (как можно меньше кода, столько, чтобы продемонстрировать проблему) Plunker предоставляет готовый к использованию шаблон TS Angular2. - person Günter Zöchbauer; 26.01.2017

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

@Pipe({
  name: 'callback',
  pure: false
})
person lohiarahul    schedule 09.02.2018
comment
Проверка перевода @StinkyCat — это просто чтение из хэш-карты. может ли это так сильно повлиять на производительность? - person skyboyer; 11.07.2018
comment
Это зависит от веб-сайта, который вы создаете, но если это огромный веб-сайт с множеством привязок... да, это может быть еще одним фактором, замедляющим работу. (мое мнение, по опыту, реально не измерял!) - person StinkyCat; 12.07.2018
comment
Идеальное решение, оно сработало со мной как шарм!! - person El7or; 30.10.2019

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

}
person Harry Burns    schedule 11.07.2018
comment
Он специально просил чистую трубку. - person Bryan Rayner; 20.11.2018
comment
Брайан, я знаю. Но решение Async Pipe достаточно хорошо для создания каналов трансляции. По крайней мере, на моих проектах это работает очень хорошо. Проблем с производительностью замечено не было. - person Harry Burns; 21.11.2018
comment
Разве markForCheck не имеет смысла в нечистой трубе? Или вообще любую трубу? - person cambunctious; 26.03.2019
comment
@cambunctious, если ваш канал зависит от чего-то, что не является параметром этого канала (например, локаль приложения из службы локализации), то, если этот параметр изменится, вам нужно заставить канал снова вызвать метод transform(). Даже если ваш ChangeDetectionStrategy это Default. ChangeDetectorRef.markForCheck() сделает это. Он пометит текущий вид для повторной проверки изменений, и это вызовет функцию transform(). - person Harry Burns; 28.03.2019
comment
Хорошо, я вижу. Это хорошее решение, поскольку оно не позволяет компоненту использовать ChangeDetectorRef. AsyncPipe — хороший связанный пример, так как он также реализован с использованием ChangeDetectorRef, но вам, конечно же, не нужно его использовать. Также просто отметим, что нечистый канал абсолютно необходим, если вы хотите, чтобы канал менял свой вывод без изменения ввода. Пока метод преобразования недорог, все в порядке, так же как можно использовать обнаружение изменений по умолчанию и часто иметь угловой доступ к вашему компоненту. - person cambunctious; 29.03.2019
comment
Гарри, старая статья про аллигаторов действительно рекомендовала использовать markForCheck в конце subscribe Блок, как вы указали. Однако это может быть внутри ngOnInit вместо конструктора. - person CPHPython; 01.08.2019

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

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

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

person Mohammad Hassani    schedule 13.07.2019