angular 中的异步调用使用事件发射器和服务进行跨组件通信

Asynchronous call in angular using event emitter and services for cross-component communication

无法将从订阅方法接收到的值存储在模板变量中。

照片细节组件

import { Component, OnInit, Input } from "@angular/core";
import { PhotoSevice } from "../photo.service";
import { Photo } from "src/app/model/photo.model";

@Component({
  selector: "app-photo-detail",
  templateUrl: "./photo-detail.component.html",
  styleUrls: ["./photo-detail.component.css"]
})
export class PhotoDetailComponent implements OnInit {

  url: string;

  constructor(private photoService: PhotoSevice) {
    this.photoService.photoSelected.subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

  ngOnInit() {

  }
}

外部 console.log 未定义,视图中未呈现任何内容,但在订阅方法中我可以看到 value.So,如何在我的视图中显示它?

照片组件

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { FnParam } from "@angular/compiler/src/output/output_ast";
import { AlbumService } from "../service/album.service";
import { Photo } from "../model/photo.model";
import { PhotoSevice } from "./photo.service";

@Component({
  selector: "app-photos",
  templateUrl: "./photos.component.html",
  styleUrls: ["./photos.component.css"]
})
export class PhotosComponent implements OnInit {
  selectedAlbumId: string;
  photoList: Photo[] = [];
  photoSelected: Photo;
  isLoading: Boolean;
  constructor(
    private rout: ActivatedRoute,
    private albumService: AlbumService,
    private router: Router,
    private photoService: PhotoSevice
  ) { }

  ngOnInit() {
    this.isLoading = true;
    this.rout.params.subscribe((params: Params) => {
      this.selectedAlbumId = params["id"];
      this.getPhotos(this.selectedAlbumId);
    });
  }

  getPhotos(id: string) {
    this.albumService.fetchPhotos(this.selectedAlbumId).subscribe(photo => {
      this.photoList = photo;
      this.isLoading = false;
    });
  }

  displayPhoto(url: string, title: string) {

    console.log(url);
    this.photoService.photoSelected.emit(url);
    this.router.navigate(["/photo-detail"]);
  }
}

请向我解释这是如何工作的以及如何解决它,以便我可以在模板视图中存储和显示从订阅和异步调用接收到的值。

这里是两个组件的观点---

photo.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>

<div class="container" *ngIf="!isLoading">
  <div class="card-columns">
    <div *ngFor="let photo of photoList" class="card">
      <img
        class="card-img-top"
        src="{{ photo.thumbnailUrl }}"
        alt="https://source.unsplash.com/random/300x200"
      />
      <div class="card-body">
        <a
          class="btn btn-primary btn-block"
          (click)="displayPhoto(photo.url, photo.title)"
          >Enlarge Image</a
        >
      </div>
    </div>
  </div>
</div>

照片-detail.component.ts

<div class="container">
  <div class="card-columns">
    <div class="card">
      <img class="card-img-top" src="{{ url }}" />
    </div>
  </div>
</div>

photo.service.ts

import { Injectable } from "@angular/core";
import { EventEmitter } from "@angular/core";

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  photoSelected = new EventEmitter();
 // urlService: string;
}

这是我的 github 存储库的 link,我将代码保留在注释中并在那里使用了不同的方法。 如果您在那里检查专辑组件,我也订阅了 http 请求并在专辑组件的模板变量中分配了值。 也有未定义的值取代订阅方法,但我可以在模板中访问它。

https://github.com/Arpan619Banerjee/angular-accelerate

这里是相册组件和服务的详细信息 请将此与事件发射器案例进行比较,并向我解释有什么区别—— albums.component.ts

import { Component, OnInit } from "@angular/core";
import { AlbumService } from "../service/album.service";
import { Album } from "../model/album.model";

@Component({
  selector: "app-albums",
  templateUrl: "./albums.component.html",
  styleUrls: ["./albums.component.css"]
})
export class AlbumsComponent implements OnInit {
  constructor(private albumService: AlbumService) {}
  listAlbums: Album[] = [];
  isLoading: Boolean;

  ngOnInit() {
    this.isLoading = true;
    this.getAlbums();
  }

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }
}

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

album.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpParams } from "@angular/common/http";
import { map, tap } from "rxjs/operators";
import { Album } from "../model/album.model";
import { Observable } from "rxjs";
import { UserName } from "../model/user.model";

@Injectable({ providedIn: "root" })
export class AlbumService {
  constructor(private http: HttpClient) {}
  albumUrl = "http://jsonplaceholder.typicode.com/albums";
  userUrl = "http://jsonplaceholder.typicode.com/users?id=";
  photoUrl = "http://jsonplaceholder.typicode.com/photos";

  //get the album title along with the user name
  fetchAlbums(): Observable<any> {
    return this.http.get<Album[]>(this.albumUrl).pipe(
      tap(albums => {
        albums.map((album: { userId: String; userName: String }) => {
          this.fetchUsers(album.userId).subscribe((user: any) => {
            album.userName = user[0].username;
          });
        });
        // console.log(albums);
      })
    );
  }

  //get the user name of the particular album with the help of userId property in albums
  fetchUsers(id: String): Observable<any> {
    //let userId = new HttpParams().set("userId", id);
    return this.http.get(this.userUrl + id);
  }

  //get the photos of a particular album using the albumId
  fetchPhotos(id: string): Observable<any> {
    let selectedId = new HttpParams().set("albumId", id);
    return this.http.get(this.photoUrl, {
      params: selectedId
    });
  }
}

我已经按照评论中的说明在偶数发射器中添加了控制台日志,这是我得到的预期行为。

我认为您正在尝试在照片详细信息组件中显示选定的图像,该组件从服务获取要显示的照片。

问题没有提到您是如何创建照片细节组件的。

  1. 组件是在用户选择要显示的照片后创建的吗?
  2. 组件是否在用户选择要显示的照片之前创建?

我认为第一个是你想要做的。 如果是这样,有两件事...

  1. 当您在构造函数内部进行订阅时,订阅 运行 中的代码会在可观察对象发出一段时间后发出。同时,订阅后的代码即 console.log(url)(外部代码)将 运行 因此它将是未定义的。

  2. 如果订阅发生在事件发出之后,即您已经使用 url 发出了事件,但到那时组件还没有订阅服务事件。所以事件丢失了,你什么也得不到。为此你可以做一些事情

    一个。将要显示详情的照片添加到url,在照片详情组件中获取。

    b。将服务中的主题/事件发射器转换为行为主题。这将确保即使您稍后订阅,您仍然会收到最后发出的事件。

    c。如果照片详细信息组件在照片组件的模板内,则将 url 作为输入参数发送(@Input() 绑定)。

希望对您有所帮助

问题有两个部分。

第 1 部分 - 照片和照片细节部分

  1. EventEmitter is used to emit variables decorated with a @Output decorator from a child-component (not a service) to parent-component. It can then be bound to by the parent component in it's template. A simple and good example can be found here。注意应用组件模板中的(notify)="receiveNotification($event)"

  2. 对于您的情况,使用 Subject or a BehaviorSubject is a better idea. Difference between them can be found in my other answer 。试试下面的代码

photo.service.ts

import { Injectable } from "@angular/core";
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  private photoSelectedSource = new BehaviorSubject<string>(undefined);

  public setPhotoSelected(url: string) {
    this.photoSelectedSource.next(url);
  }

  public getPhotoSelected() {
    return this.photoSelectedSource.asObservable();
  }
}

photos.component.ts

export class PhotosComponent implements OnInit {
  .
  .
  .
  displayPhoto(url: string, title: string) {
    this.photoService.setPhotoSelected(url);
    this.router.navigate(["/photo-detail"]);
  }
}

照片-detail.component.ts

  constructor(private photoService: PhotoSevice) {
    this.photoService.getPhotoSelected().subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

照片-detail.component.html

<ng-container *ngIf="url">
  <div class="container">
    <div class="card-columns">
      <div class="card">
        <img class="card-img-top" [src]="url"/>
      </div>
    </div>
  </div>
</ng-container>

第 2 部分 - 相册组件和服务

调用 this.albumService.fetchAlbums() returns 可观察到的 HTTP GET 响应。您正在订阅它并更新成员变量值并在模板中使用它。

来自您对另一个答案的评论:

i understand the behaviour and why the outside console.log is underfined, its beacuse the execution context is diff for async calls and it first executes the sync code and then comes the async code

恐怕同步和异步调用的区别并没有这么简单。请参阅 here 了解它们之间的区别。

albums.components.ts

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

问题是解释为什么尽管 console.log("outside subscribe method----->" + this.listAlbums); 正在打印 undefined,模板仍显示相册。简而言之,当您执行外部控制台日志时,this.listAlbums 实际上是 undefined,因为它尚未初始化。但是在模板中,有一个加载检查*ngIf="!isLoading"。从控制器代码来看,isLoading 仅在 listAlbums 赋值时设置为 false。因此,当您将 isLoading 设置为 false 时,可以确保 listAlbums 包含要显示的数据。