【问题标题】:Fetch data once with Observables in Angular 2在 Angular 2 中使用 Observables 获取一次数据
【发布时间】:2017-03-24 15:05:59
【问题描述】:

我有一个服务,它在我的许多 Angular 2 组件中多次使用。它从 Web API 获取客户数据并返回一个 Observable:

getCustomers() {
   return this.http
        .get(this.baseURI + this.url)
        .map((r: Response) => {
            let a = r.json() as Customer[];                       
            return a;                         
        });               
}    

我将这个服务注入到我的根组件中,并且在每个想要访问客户的组件中,我都订阅了那个 Observable:

this.customerService.getCustomers().subscribe(v => this.items = v);

但是,订阅我的 Observable 的每个组件都会导致 HTTP 请求的另一次执行。但是只获取一次数据就足够了。 如果我尝试 share(),它并不能解决我的问题:

getCustomers() {
   return this.http
        .get(this.baseURI + this.url)
        .map((r: Response) => {
            let a = r.json() as Customer[];                       
            return a;                         
        }).share();               
}   

还是同样的问题。有什么建议我必须使用哪些运算符来只获取一次数据?

【问题讨论】:

    标签: javascript angular rxjs observable


    【解决方案1】:

    1) 您可以简单地将下载的数据保存在您的服务中:

    export class CustomersService {
      protected _customers: Array<Customer>;
    
      constructor(public http: Http) {}
    
      public getCustomers(): Observable<Array<Customer>> {
        return new Observable(observer => {
          if (this._customers) {
            observer.next(this._customers);
            return observer.complete();
          }
          this.http
            .get(this.baseURI + this.url)
            .map((r: Response) => (r.json() as Array<Customer>))
            .subscribe((customers: Array<Customer>) => {
              this._customers = customers;
              observer.next(this.customers);
              observer.complete();
            });
        });
      }
    }
    

    2) 采用refresh 参数的较短方法:

    export class CustomersService {
      protected _customers: Array<Customer>;
    
      constructor(public http: Http) {}
    
      public getCustomers(refresh?: boolean): Observable<Array<Customer>> {
        if (!refresh && this._customers) {
          return Observable.of(this._customers);
        }
        return this.http
                .get(this.baseURI + this.url)
                .map((c: Response) => (c.json() as Array<Customer>))
                .do((customers: Array<Customer>) => {
                    this._customers = customers;
                });
        });
      }
    }
    

    3) 利用ReplaySubject:

    export class CustomersService {
      protected _customers$: ReplaySubject<Array<Customer>> = new ReplaySubject(1);
      protected _customersInitialized: boolean;
    
      constructor(public http: Http) {}
    
      public getCustomers(refresh?: boolean): Observable<Array<Customer>> {
        if (refresh || !this._customersInitialized) {
          this._customersInitialized = true;
          this.http
            .get(this.baseURI + this.url)
            .map((c: Response) => (c.json() as Array<Customer>))
            .subscribe((customers: Array<Customer>) => {
              this._customers$.next(customers);
            });
        }
        return this._customers$.asObservable().skip(+refresh).distinctUntilChanged();
      }
    }
    

    然后:

    this.customersService.getCustomers()
        .subscribe(customers => this.customers = customers);
    

    您还可以通过这种方式公开SomeService 中始终保持最新的customers 字段以用于只读目的(例如在模板中显示):

    public get customers(): ReadonlyArray<Customer> {
      return this._customers;
    }
    

    【讨论】:

    • 稍微修改一下就可以了。你必须记住,在你的 Promise 中只有一个 observable 被创建,但没有被订阅。所以你的解决方案只有在你订阅这个创建的 observable 并在订阅中调用“resolve...”时才有效。
    • @David 你是怎么做到的?
    • 我通常会做整个this.customers? Observable.of(this.customers) : this.http...但很高兴看到这种更标准的方法
    • 走下ReplaySubject兔子洞后,我认为你想要的解决方案更好地遵循BehaviorSubject模式。
    • @Maverick,我添加了使用ReplaySubject 的方法的代码。我确信使用BehaviorSubject 也是可行的,但我更喜欢在获得真实数据之前不运行订阅者。
    【解决方案2】:

    我会创建一个父容器,获取一次数据,然后使用@Input 将其传递给子组件。

    家长:

    @Component({
        selector: 'BarFooHttpCaller',
        template: ´<child *ngIf="data.length > 0" [data]></child>´
    })
    
    class BarFooHttpCaller {
        private data: any;
        constructor(private foobar:Foobar) {
            this.data = {};
        }
    
        ngOnInit() {
            this.foobar.getCustomers().subscribe(() => {       
                console.log('httpdone') 
            });
            this.foobar.dataStream.subscribe((data) => {
                console.log('new data', data);
                this.data = data;
            })
        }
    }
    

    孩子:

    import { Component, Input } from '@angular/core';
    
    @Component({
        selector: 'child',
        template: ´<div>{{data}}</div>´
    })
    
    export class Child {
        @Input() data: any;
    
    }
    

    【讨论】:

      【解决方案3】:

      如果您希望多个孩子订阅同一个可观察对象,但只执行一次可观察对象,您可以执行以下操作。

      请注意,这确实符合 observable 的设计,因为我们在服务层 (Observable.fromPromis(stream.toPromise()) 中执行 observable 时应该从组件订阅中执行。查看https://www.bennadel.com/blog/3184-creating-leaky-abstractions-with-rxjs-in-angular-2-1-1.htm 了解更多信息.

        //declare observable to listen to
        private dataObservable: Observable<any>;
      
        getData(slug: string): Observable<any> {
      
          //If observable does not exist/is not running create a new one
          if (!this.dataObservable) {
      
              let stream = this.http.get(slug + "/api/Endpoint")
                  .map(this.extractData)
                  .finally(() => {
                      //Clear the observable now that it has been listened to
                      this.staffDataObservable = null;
                  });
      
              //Executes the http request immediately
              this.dataObservable = Observable.fromPromise(stream.toPromise());
      
          }        
      
          return this.staffDataObservable;
       }
      

      【讨论】:

        【解决方案4】:

        共享操作符可以让多个观察者使用同一流的结果。这可能很好,但每次调用 getCustomers() 时都会生成一个新的 observable 流,调用 share() 没有意义,因为您没有多次订阅此流。

        如果您想与多个观察者共享数据但只进行一次 http 调用,您只需创建第二个流,由 http 提供,包含数据。之后,您的所有组件都可以订阅它。

        代码可能是这样的

        @Injectable()
        class FooBar {
        
            public dataStream:Subject<any> = new Subject();
        
            constructor(private http:Http) {}
        
            public getCustomers() {
                return this.http
                .get(this.baseURI + this.url)
                .map((response:Response) => response.json())
                .map((data) => {
                    this.dataStream.next(data); 
                    return data;
                })
            }
        
        }
        
        
        @Component({})
        class BarFooHttpCaller {
            constructor(private foobar:Foobar) {}
        
            ngOnInit() {
                this.foobar.getCustomers().subscribe(() => { console.log('http done') });
                this.foobar.dataStream.subscribe((data) => {
                    console.log('new data', data);
                })
            }
        }
        
        @Component({})
        class OtherBarFoo {
            constructor(private foobar:Foobar) {}
        
            ngOnInit() {
                this.foobar.dataStream.subscribe((data) => {
                    console.log('new data', data);
                })
            }
        }
        

        【讨论】:

        • 好的,但在这种情况下,我总是必须从调用 getCustomers().subscribe(...) 的组件开始。在我的应用程序中,我永远无法知道首先访问哪个组件(用户可能输入另一个 URL,而不是另一个用户)。当我在服务本身中调用订阅时,它并没有真正起作用。在这种情况下我应该如何修改您的示例?
        • 您有很多可能性,将数据存储在 dataStream 之外并验证它是否已经提供,如果是,则返回 dataStream 而不是 http 的。您还可以进行一个简单的去抖动,可以预先调用多个 getCustomers(),这样您就可以避免处理并发性。另一种方法是在顶级路由的组件中调用该方法,然后订阅子组件。
        【解决方案5】:

        无需自定义实现。管道可以解决问题:

        getCustomers$(): Observable<Customer> {
           return this.http
                .get<Customer>(this.baseURI + this.url)
                .pipe(shareReplay(1));               
        }    
        

        我在这里做了几件事:

        1. 添加shareReplay(1)管道,以确保请求只完成一次(只需要回答问题)
        2. 删除map 并输入get 调用
        3. 带有$ 的后缀方法名称表示返回Observable

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-02-23
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-07
          • 2023-03-11
          • 1970-01-01
          相关资源
          最近更新 更多