Как правильно передать многоадресный результат запроса HttpClient нескольким компонентам в Angular?

Я пытаюсь отправить результат HttpClient post запросов нескольких компонентов в моем приложении Angular. Я использую Subject и вызываю его метод next() всякий раз, когда новый запрос post успешно выполняется. Каждый компонент подписывается на Subject службы.

Неисправные службы определяются как

@Injectable()
export class BuildingDataService {

  public response: Subject<object> = new Subject<object>();

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    ...

    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.response.next(resp);
    });
}

Компоненты подписываются на BuildingService.response следующим образом

@Component({
  template: "<h1>{{buildingName}}</h1>"
  ...
})

export class SidepanelComponent implements OnInit {
  buildingName: string;

  constructor(private buildingDataService: BuildingDataService) { }

  ngOnInit() {
    this.buildingDataService.response.subscribe(resp => {
        this.buildingName = resp['buildingName'];
      });
  }

  updateBuildingInfo(location) {
    this.buildingDataService.fetchBuildingData(location);
  }
}

updateBuildingInfo запускается пользователями, нажимающими на карту.

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

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

  • обернуть мой звонок next() в службу NgZone.run(() => { this.response.next(resp); }
  • вызовите ApplicationRef.tick() после this.title = resp['title'] в компоненте.

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

Поэтому мой вопрос: как правильно получить данные один раз и отправить их нескольким компонентам?

Кроме того, я хотел бы понять, почему моя реализация ускользает от системы обнаружения изменений Angular.

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


person Simeon Nedkov    schedule 25.04.2018    source источник


Ответы (5)


Один из способов — получить Observable из Subject и использовать его в своем шаблоне с помощью канала async:

(building | async)?.buildingName

Кроме того, если разные компоненты подписываются на службу в разное время, возможно, вам придется использовать BehaviorSubject вместо Subject.


@Injectable()
export class BuildingDataService {
  private responseSource = new Subject<object>();
  public response = this.responseSource.asObservable()

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.responseSource.next(resp);
    });
  }
}

@Component({
  template: "<h1>{{(building | async)?.buildingName}}</h1>"
  ...
})

export class SidepanelComponent implements OnInit {
  building: Observable<any>;

  constructor(private buildingDataService: DataService) {
      this.building = this.buildingDataService.response;
  }

  ngOnInit() {
  }

  updateBuildingInfo(location) {
    this.buildingDataService.fetchBuildingData(location);
  }
}
person sabithpocker    schedule 25.04.2018
comment
Спасибо за быстрый ответ! К сожалению, канал async производит тот же эффект: он работает только тогда, когда я запускаю обнаружение изменений вручную, предпочтительно через NgZone, иначе он отстает на один тик, т. е. отображает предыдущее значение вместо текущего. Я изучу BehaviourSubject, спасибо, что указали на это. - person Simeon Nedkov; 26.04.2018
comment
Также убедитесь, что вы отписываетесь от подписок, сделанных вручную. Я действительно не вижу необходимости в ручном обнаружении изменений в этом сценарии. - person sabithpocker; 26.04.2018
comment
Кроме того, вы вводите DataService вместо BuildingDataService, это опечатка? - person sabithpocker; 26.04.2018
comment
Это опечатка; исправлено сейчас. Я стремлюсь к ручному обнаружению изменений, поскольку шаблон не обновляется при получении нового значения через Subject (или BehaviourSubject, если на то пошло, см. мои комментарии в ответе DiabolicWord. - person Simeon Nedkov; 26.04.2018
comment
Я обнаружил проблему: я запускаю fetchBuildingData из обратного вызова за пределами зоны Angular. Большое спасибо за разные подходы; они помогли мне сосредоточиться на проблеме. Ваше замечание о том, что вы на самом деле не видите необходимости в ручном обнаружении изменений в этом сценарии, почему-то напомнило мне посмотреть, как я вызываю fetchBuildingData. - person Simeon Nedkov; 26.04.2018
comment
Я принял ваш ответ, поскольку он наиболее близок к искомому решению. Однако в четвертой строке первого блока кода отсутствует this. Можете добавить? - person Simeon Nedkov; 30.04.2018
comment
Этот ответ не имеет ничего общего с вашим вопросом. Вы должны принять свой собственный ответ, чтобы не смущать будущих читателей. - person Roberto Zvjerković; 30.04.2018
comment
Я не был уверен, что делать, поскольку другие ответы напрямую касаются вопроса о многоадресной рассылке, тогда как мое решение исправляет что-то совершенно не связанное. Я бы не хотел, чтобы кто-то погуглил за многоадресную рассылку и оказался на странице, описывающей, как что-то исправить с помощью Leaflet. Все еще не знаю, что делать, поэтому, пожалуйста, посоветуйте. - person Simeon Nedkov; 30.04.2018

Стандартный способ сделать это в угловом приложении — геттер в классе обслуживания.

get data()
{
     return data;
}

Почему вы пытаетесь усложнить ситуацию? Каковы преимущества, и если они есть, они преодолеют недостатки?

person Giona Granata    schedule 26.04.2018
comment
Я хочу отправить данные в компонент A, который извлекается из-за взаимодействия пользователя с компонентом B. Я не понимаю, как это можно сделать с помощью геттеров, поскольку мне придется вызывать их явно из компонента A, верно? - person Simeon Nedkov; 26.04.2018
comment
Нет, вы не вызываете их непосредственно в компоненте A. Вы внедряете одну и ту же службу Singleton в оба. Вы начинаете извлекать данные, вызывая метод для синглтона (ferchBuildingData). Этот метод сохраняет данные в члене синглтона. Член синглтона является общим для любого компонента с геттером. Это стандартная практика в angular, вы можете увидеть это в руководстве по герою в официальной документации. Другой элегантный способ решить эту проблему — использовать библиотеку redux для angular, поэтому реализовать управление состоянием (мне это очень нравится). - person Giona Granata; 26.04.2018
comment
Да, вы правы, это работает как шарм. Я не знал о том, что геттеры привязаны к значениям! Это действительно намного проще, чем подход Субъект/Наблюдаемый. - person Simeon Nedkov; 26.04.2018
comment
Можете ли вы указать мне документы, где упоминается get? Я не могу найти это... - person Simeon Nedkov; 26.04.2018

Я думаю, что нашел проблему. Я кратко упомяну, что fetchBuildingData запускается пользователями, нажимающими на карту; эта карта представляет собой Leaflet, предоставленную модулем ngx-leaflet. Я привязываюсь к его событию click следующим образом

map.on('click', (event: LeafletMouseEvent) => {
  this.buildingDataService.fetchBuildingData(event.latlng);
});

Теперь я понимаю, что обратный вызов запускается за пределами зоны Angular. Поэтому Angular не может обнаружить изменение на this.building. Решение состоит в том, чтобы вызвать обратный вызов в зоне Angular через NgZone как

map.on('click', (event: LeafletMouseEvent) => {
  ngZone.run(() => {
    this.buildingDataService.fetchBuildingData(event.latlng);
  });
});

Это решает проблему, и каждое предлагаемое решение работает как шарм.

Большое спасибо за быстрые и полезные ответы. Они сыграли важную роль в том, чтобы помочь мне сузить проблему!

Справедливости ради: эта проблема упоминается в документации ngx-leaflet [1]. Мне не удалось сразу понять последствия, так как я все еще изучаю Angular, и мне нужно многое понять.

[1] https://github.com/Asymmetrik/ngx-leaflet#a-note-about-change-detection

person Simeon Nedkov    schedule 26.04.2018
comment
Я не был уверен, что делать, поскольку другие ответы напрямую касаются вопроса о многоадресной рассылке, тогда как мое решение исправляет что-то совершенно не связанное. Я бы не хотел, чтобы кто-то погуглил за многоадресную рассылку и оказался на странице, описывающей, как что-то исправить с помощью Leaflet. Все еще не знаю, что делать, поэтому, пожалуйста, посоветуйте. - person Simeon Nedkov; 30.04.2018
comment
Ну, я тоже думаю, что это имеет смысл. Я думаю, вы можете оставить это как есть. - person sabithpocker; 30.04.2018

ИЗМЕНИТЬ:

Поскольку ваше обнаружение изменений, кажется, борется с любой деталью вашего объекта, у меня есть еще одно предложение. И поскольку я также предпочитаю BehaviorSubject простому Subject, я скорректировал даже эту часть кода.

1st определяет объект-оболочку. Интересно то, что мы добавляем второе значение, которое определенно должно быть уникальным по сравнению с любой другой оберткой того же типа, которую вы производите.

Обычно я помещаю эти классы в файл models.ts, который затем импортирую везде, где использую эту модель.

export class Wrapper {
   constructor(
       public object: Object,
       public uniqueToken: number
   ){}
}

Второй в вашем сервисе используйте эту оболочку следующим образом.

import { Observable } from 'rxjs/Observable'; 
import { BehaviorSubject } from 'rxjs/BehaviorSubject'; 
import { Wrapper } from './models.ts';

@Injectable()
export class BuildingDataService {
   private response: BehaviorSubject<Wrapper> = new BehaviorSubject<Wrapper>(new Wrapper(null, 0));

   constructor (private http: HttpClient) { }

   public getResponse(): Observable<Wrapper> {
        return this.response.asObservable();
   }

   fetchBuildingData(location) {
      ...
      this.http.post(url, location, httpOptions).subscribe(resp => {

         // generate and fill the wrapper
         token: number = (new Date()).getTime();
         wrapper: Wrapper = new Wrapper(resp, token);

         // provide the wrapper
         this.response.next(wrapper);
      });
   }

Импортируйте model.ts во все подписные классы, чтобы иметь возможность правильно и легко с ним обращаться. Позвольте вашим подписчикам подписаться на метод getResponse().

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

person Lynx 242    schedule 25.04.2018
comment
Спасибо за быстрый ответ! Проблема, к сожалению, сохраняется. - person Simeon Nedkov; 26.04.2018
comment
Привет, я понимаю. Я обновил свой пост. Я надеюсь, что это в конечном итоге работает для вас. - person Lynx 242; 26.04.2018
comment
Спасибо за это творческое решение! К сожалению, это тоже не работает. Я получаю начальное значение BehaviourSubject, но потом ничего, если только я не оберну его в NgZone.run(). Видя, что ваше решение очень похоже на решение @sabithpocker, я начинаю думать, что с моей установкой Angular (5.2.10) что-то не так. В качестве альтернативы, Angular не выполняет обнаружение изменений в обратных вызовах: я пытался вызывать next() в сервисе каждую секунду через setInterval, но он также работает только при ручном запуске обнаружения изменений. - person Simeon Nedkov; 26.04.2018
comment
Я обнаружил проблему: я запускаю fetchBuildingData из обратного вызова за пределами зоны Angular. Смотрите мое решение. Миллион благодарностей за помощь! Предоставив ряд различных решений, вы помогли мне убедиться, что часть Subject/Observable верна. - person Simeon Nedkov; 26.04.2018

Код сабита неверный (но идея правильная)

  //declare a source as Subject
  private responseSource = new Subject<any>(); //<--change object by any

  public response = this.responseSource.asObservable() //<--use"this"

  constructor (private http: HttpClient) { }

  fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.responseSource.next(resp);
    });
  }

Должно быть работа

Другая идея - просто использовать геттер в вашем компоненте.

//In your service you make a variable "data"
data:any
fetchBuildingData(location) {
    this.http.post(url, location, httpOptions).subscribe(resp => {
      this.data=resp
    });
  }

//in yours components

get data()
{
     return BuildingDataService.data
}
person Eliseo    schedule 26.04.2018
comment
Да, геттер прекрасно работает. Я не знал, что они привязаны к переменным и автоматически получают новые значения. - person Simeon Nedkov; 26.04.2018
comment
Нет, это не совсем так. Angular — это не волшебство, он должен учитывать изменения в приложении (щелчок, изменение одного ввода...). Когда происходит изменение, он ищет все, что может измениться, и показывает новые значения. Использование геттера для Angular для поиска нового значения. - person Eliseo; 26.04.2018
comment
Благодарю за разъяснение. Как работает принуждение? Это что-то, что сделано в Angular, или это свойство самого JavaScript? - person Simeon Nedkov; 26.04.2018
comment
Я не знаю, но если вы напишете console.log(***) в геттере, вы увидите, что он вызывается несколько раз. (возможно, принуждение — не то слово, извините) - person Eliseo; 26.04.2018