【问题标题】:caching results with angular2 http service [duplicate]使用angular2 http服务缓存结果[重复]
【发布时间】:2015-12-05 11:22:55
【问题描述】:

我通过服务公开了一个 HTTP GET 请求,并且有几个组件正在使用这些数据(用户的个人资料详细信息)。我希望第一个组件请求实际执行对服务器的 HTTP GET 请求并缓存结果,以便后续请求将使用缓存的数据,而不是再次调用服务器。

这是服务的一个示例,您如何建议使用 Angular2 和 typescript 实现此缓存层。

import {Inject, Injectable} from 'angular2/core';
import {Http, Headers} from "angular2/http";
import {JsonHeaders} from "./BaseHeaders";
import {ProfileDetails} from "../models/profileDetails";

@Injectable()
export class ProfileService{
    myProfileDetails: ProfileDetails = null;

    constructor(private http:Http) {

    }

    getUserProfile(userId:number) {
        return this.http.get('/users/' + userId + '/profile/', {
                headers: headers
            })
            .map(response =>  {
                if(response.status==400) {
                    return "FAILURE";
                } else if(response.status == 200) {
                    this.myProfileDetails = new ProfileDetails(response.json());
                    return this.myProfileDetails;
                }
            });
    }
}

【问题讨论】:

  • 我认为您正在寻找share。我有一个plnkr,因此您可以看到它正在工作。请注意,这不是缓存,但它可能对您有用:)(运行一次并查看网络选项卡,然后从 http.get 中删除 .share() 并查看差异)。
  • 我尝试了您的方法,但是当从两个不同的组件调用 getUserProfile(使用 .share())时,GET 请求仍会在服务器上执行两次。这是因为 ProfileService 是使用 @Inject(ProfileService) profileService 注入到调用组件的两个构造函数中的。我在这里错过了什么?
  • 这取决于。如果您在每个组件中注入服务,您将获得两个不同的实例(通过注入我的意思是使用providers/viewProviers)。如果是这种情况,您应该只将它注入您的顶级组件(在这两者之间)。如果不是这种情况,您应该尽可能添加更多代码和重现。
  • 它们都使用相同的 ProfileService 实例(我通过放置一些私有 i 整数来验证这一点,每次调用方法时将其增加一个并将其打印到日志中,它会打印 0、1、2。 ..所以这意味着每次都使用相同的实例)。然而,由于某种原因,每次调用 getUserProfile 方法时,都会在服务器上再次执行 GET 请求。
  • 你说得对,我刚刚尝试并遇到了同样的问题。我发现使用该方法返回一个share(),它每次都会返回一个不同的共享(这是有道理的,一开始没看到)。但是,如果您重构它以在构造函数中发出请求并将其分配给变量,它将起作用。 TL;DR plnkr 与示例工作:plnkr.co/edit/kvha8GH0b9qkw98xLZO5?p=preview

标签: angular typescript


【解决方案1】:

share() 运算符仅在第一个请求上工作,当所有订阅都已提供服务并且您创建另一个订阅时,它将不起作用,它将发出另一个请求。 (这种情况很常见,至于你总是创建/销毁组件的 angular2 SPA)

我使用ReplaySubject 来存储来自 http 可观察对象的值。 ReplaySubject observable 可以为其订阅者提供先前的价值。

服务:

@Injectable()
export class DataService {
    private dataObs$ = new ReplaySubject(1);

    constructor(private http: HttpClient) { }

    getData(forceRefresh?: boolean) {
        // If the Subject was NOT subscribed before OR if forceRefresh is requested 
        if (!this.dataObs$.observers.length || forceRefresh) {
            this.http.get('http://jsonplaceholder.typicode.com/posts/2').subscribe(
                data => this.dataObs$.next(data),
                error => {
                    this.dataObs$.error(error);
                    // Recreate the Observable as after Error we cannot emit data anymore
                    this.dataObs$ = new ReplaySubject(1);
                }
            );
        }

        return this.dataObs$;
    }
}

组件:

@Component({
  selector: 'my-app',
  template: `<div (click)="getData()">getData from AppComponent</div>`
})
export class AppComponent {
    constructor(private dataService: DataService) {}

getData() {
    this.dataService.getData().subscribe(
        requestData => {
            console.log('ChildComponent', requestData);
        },
        // handle the error, otherwise will break the Observable
        error => console.log(error)
    );
}
    }
}

fully working PLUNKER
(观察控制台和网络选项卡)

【讨论】:

  • Gunter 的回答非常有帮助,但鉴于它使用纯 RxJS 功能,您的回答似乎更好。找到它是多么困难,但是一旦你知道使用什么(ReplaySubject!),它是多么的好和简单。谢谢! :)
  • 谢谢,很高兴您喜欢它。我还刚刚编辑了帖子,并介绍了另外 2 个未涵盖的情况:请求失败的情况,现在它将发出另一个请求,以及您同时发出多个请求的情况。
  • 非常好,因为你的回答,我了解了更多关于 RxJs 的知识:P 但现在好奇:如何将它变成延迟加载?我想.subscribe(...) 应该换成别的东西。我正在尝试找到应该到位的内容。
  • 这个有一个小问题,当你的url参数改变时,请求必须重做,或者说最好是对相同的url进行缓存
  • 只是好奇,你也可以使用BehaviorSubject吗?使用缓冲区为 1 的 ReplaySubject 似乎等同于 BehaviorSubject IMO 的相同目的。我可以看到ReplaySubject 的唯一好处是您不需要初始值,我想这可以简化事情。
【解决方案2】:

我省略了userId 处理。它需要管理data 的数组和observable 的数组(每个请求的userId 一个)。

import {Injectable} from '@angular/core';
import {Http, Headers} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/observable/of';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/map';
import {Data} from './data';

@Injectable()
export class DataService {
  private url:string = 'https://cors-test.appspot.com/test';

  private data: Data;
  private observable: Observable<any>;

  constructor(private http:Http) {}

  getData() {
    if(this.data) {
      // if `data` is available just return it as `Observable`
      return Observable.of(this.data); 
    } else if(this.observable) {
      // if `this.observable` is set then the request is in progress
      // return the `Observable` for the ongoing request
      return this.observable;
    } else {
      // example header (not necessary)
      let headers = new Headers();
      headers.append('Content-Type', 'application/json');
      // create the request, store the `Observable` for subsequent subscribers
      this.observable = this.http.get(this.url, {
        headers: headers
      })
      .map(response =>  {
        // when the cached data is available we don't need the `Observable` reference anymore
        this.observable = null;

        if(response.status == 400) {
          return "FAILURE";
        } else if(response.status == 200) {
          this.data = new Data(response.json());
          return this.data;
        }
        // make it shared so more than one subscriber can get the result
      })
      .share();
      return this.observable;
    }
  }
}

Plunker example

您可以在https://stackoverflow.com/a/36296015/217408找到另一个有趣的解决方案

【讨论】:

  • 优秀的完整示例,即使使用 this.observable 检查。 .share() 非常重要,一开始不知道要寻找什么就不容易弄清楚。 Observable.of() 是我个人所寻找的。现在只需添加一点检查,以便在数据超过一定时间时重复请求:)
  • @Günter:你能分享一下你的 plunker 代码吗? :)
  • @pdfarhad 好主意 :) 更新了答案。该代码包含几个现在应该修复的错误。
  • 我花了一段时间才发现private observable: Observable; 的类型是{},我们需要的是'private observable: Observable;'
  • 这太棒了!我必须使用import 'rxjs/add/observable/of'; 才能让它工作。
【解决方案3】:

关于您的最后一条评论,这是我能想到的最简单的方法:创建一个具有一个属性并且该属性将保存请求的服务。

class Service {
  _data;
  get data() {
    return this._data;
  }
  set data(value) {
    this._data = value;
  }
}

就这么简单。 plnkr 中的其他所有内容都不会受到影响。我从服务中删除了请求,因为它将自动实例化(我们不这样做new Service...,而且我不知道通过构造函数传递参数的简单方法)。

所以,现在,我们有了 Service,我们现在要做的是在我们的组件中发出请求并将其分配给 Service 变量 data

class App {
  constructor(http: Http, svc: Service) {

    // Some dynamic id
    let someDynamicId = 2;

    // Use the dynamic id in the request
    svc.data = http.get('http://someUrl/someId/'+someDynamicId).share();

    // Subscribe to the result
    svc.data.subscribe((result) => {
      /* Do something with the result */
    });
  }
}

请记住,我们的 Service 实例对于每个组件都是相同的,因此当我们为 data 分配一个值时,它将反映在每个组件中。

这是plnkr 的工作示例。

参考

【讨论】:

  • 嗨,很好的例子,但它不起作用,例如如果你有一个点击事件的请求,它每次都会发出一个新的 xhr 请求 例如:plnkr.co/edit/Z8amRJmxQ70z9ltBALbk?p=preview(点击蓝色方并观察网络选项卡)。在我的应用程序中,我创建了一个新的 ReplaySubject Observable 来缓存 HTTP,我想使用 share() 方法,但奇怪的是为什么在某些情况下它不起作用。
  • 啊,实际上我明白了(经过一些测试并阅读了 rxjs 文档),因此它将与所有现有订阅共享相同的可观察值,但是一旦没有订阅并且您创建一个新的一个,那么它将请求一个新值,因此是一个新的 xhr 请求。
  • @Eric Martinez : plunker 不再运行了......
猜你喜欢
  • 1970-01-01
  • 2017-05-23
  • 2017-11-24
  • 2022-08-12
  • 1970-01-01
  • 1970-01-01
  • 2018-03-23
  • 2017-05-24
  • 1970-01-01
相关资源
最近更新 更多