如何将 HttpClient 请求的结果正确地多播到 Angular 中的多个组件?

How to properly multicast result of HttpClient request to several components in Angular?

我正在尝试在我的 Angular 应用程序中发送 HttpClient post 请求多个组件的结果。我正在使用 Subject 并在成功执行新的 post 请求时调用其 next() 方法。每个组件订阅服务的 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 由用户点击地图触发。

从服务器检索数据并将其传递给组件是可行的:我可以将有效负载输出到每个组件的控制台。但是,组件的模板无法更新。

在谷歌搜索和摆弄今天的大部分时间后,我发现此实现不会触发 Angular 的更改检测。解决方法是

对于这样一个微不足道的用例,这两种解决方案都感觉像是肮脏的黑客。必须有更好的方法来实现这一点。

因此我的问题是:一次获取数据并将其发送到多个组件的正确方法是什么?

我还想了解为什么我的实现逃脱了 Angular 的变更检测系统。

EDIT 事实证明我是在 Angular 区域之外发起对 HttpClient 的调用,因此它无法检测到我的更改,请参阅我的回答更多详情。

编辑:

由于您的变化检测似乎无法处理对象中的任何细节,因此我有另一个建议。因为我也更喜欢 BehaviorSubject 而不是简单的 Subject 我什至调整了这部分代码。

1st 定义一个包装器对象。有趣的是,我们添加了第二个值,与您生产的任何其他同类包装器相比,它绝对必须是唯一的。

我通常将那些 类 放在 models.ts 中,然后在我使用该模型的任何地方导入它。

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

2nd 在您的服务中按如下方式使用此包装器。

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() 方法。

这一定行得通。使用这个包装器+独特的令牌技术,我最终总是解决了这样的变化检测问题。

一种方法是获取 SubjectObservable 并使用 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);
  }
}

sabith的代码是错误的(但是思路是正确的)

  //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);
    });
  }

必须工作

另一个想法是在您的组件中使用 getter

//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
}

在 angular 应用程序中执行此操作的标准方法是在 class 服务中 getter。

get data()
{
     return data;
}

你为什么要把事情复杂化?有什么好处,有好处就克服坏处?

我想我找到了问题所在。我简要地提到 fetchBuildingData 是由用户点击地图触发的;该地图是由 ngx-leaflet 模块提供的 Leaflet。我绑定到它的 click 事件如下

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

我现在意识到,回调是在 Angular 的区域之外触发的。 Angular 因此无法检测到对 this.building 的更改。解决方案是通过 NgZone 将 Angular 区域中的回调作为

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