Angular HttpClient如何使用共享应答重复http请求

Angular HttpClient如何使用共享应答重复http请求,angular,observable,Angular,Observable,我在Angular的HttpClient上遇到了一个问题。我想知道如何使其重播共享的请求被重复。考虑下面的示例代码: import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { shareReplay } from 'rxjs/operators'; /** Node */ export class Node { /** Attributes */ att

我在Angular的HttpClient上遇到了一个问题。我想知道如何使其重播共享的请求被重复。考虑下面的示例代码:

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { shareReplay } from 'rxjs/operators';

/** Node */
export class Node {

  /** Attributes */
  attributes$: Observable<any>;

  constructor(private http: HttpClient, private href: string) {
    this.attributes$ = http.get(href).pipe(shareReplay());
  }

  update(patches: any): Observable<any> {
    const req = this.http.patch(this.href, patches);

    // TODO: subscribe to request and update attributes

    return req;
  }
}
从'@angular/common/http'导入{HttpClient};
从“rxjs”导入{Observable};
从“rxjs/operators”导入{shareReplay};
/**节点*/
导出类节点{
/**属性*/
属性$:可观察的;
构造函数(私有http:HttpClient,私有href:string){
this.attributes$=http.get(href).pipe(shareReplay());
}
更新(补丁:任何):可观察{
const req=this.http.patch(this.href,patches);
//TODO:订阅请求并更新属性
返回请求;
}
}
我试图做的是在
补丁
请求发送到资源后,使
属性$
可观察通知一个新值

请注意,
属性$
observable仅在有人订阅第一个http请求时才执行该请求。访问属性不会产生应有的效果。这是我想保留的一个重要特性

有没有办法做到这一点


提前感谢

我最终解决了这个问题,因此我将与您分享我的解决方案。首先,我要说我使用TDD来解决这个问题。所以在这里,我将首先发布我使用的测试套件

import { HttpClient } from '@angular/common/http';
import {
  HttpClientTestingModule,
  HttpTestingController
} from '@angular/common/http/testing';
import { TestBed, inject } from '@angular/core/testing';
import { bufferCount } from 'rxjs/operators';
import { zip } from 'rxjs';

import { Node } from './node';

const rootHref = '/api/nodes/root';
const rootChildrenHref = '/api/nodes/root/children';

const rootResource = {
  _links: {
    'node-has-children': { href: rootChildrenHref }
  }
};

function expectOneRequest(controller: HttpTestingController, href: string, method: string, body: any) {
  // The following `expectOne()` will match the request's URL and METHOD.
  // If no requests or multiple requests matched that URL
  // `expectOne()` would throw.
  const req = controller.expectOne({
    method,
    url: href
  });

  // Respond with mock data, causing Observable to resolve.
  // Subscribe callback asserts that correct data was returned.
  req.flush(body);
}

describe('CoreModule Node', () => {
  let httpTestingController: HttpTestingController;
  let subject: Node;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ]
    });
  });

  beforeEach(inject([ HttpClient, HttpTestingController ],
    (http: HttpClient, testingController: HttpTestingController) => {
      subject = new Node(http, rootHref);
      httpTestingController = testingController;
    }));

  afterEach(() => {
    // After every test, assert that there are no more pending requests.
    httpTestingController.verify();
  });

  it('should be created', () => {
    expect(subject).toBeTruthy();
  });

  it('#attributes$ is provided', () => {
    expect(subject.attributes$).toBeTruthy();
  });

  it('#attributes$ is observable element', (done: DoneFn) => {
    subject.attributes$.subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#attributes$ observable is cached', (done: DoneFn) => {
    zip(subject.attributes$, subject.attributes$).subscribe(([root1, root2]) => {
      expect(root1).toBe(root2);
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#update() affect attributes$', (done: DoneFn) => {
    // the subscribe at the end of this pipe will trigger a children request
    // but the update() call will trigger a resource request that in turn
    // will trigger a children request. Therefore bufferCount will produce
    // an array of size two.

    subject.attributes$.pipe(bufferCount(2)).subscribe(collection => {
      expect<number>(collection.length).toEqual(2);
    });

    subject.update([]).subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
    expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });

  it('#update() return observable', (done: DoneFn) => {
    subject.update([]).subscribe(root => {
      expect(root).toBeTruthy();
      done();
    });

    expectOneRequest(httpTestingController, rootHref, 'PATCH', {});
    expectOneRequest(httpTestingController, rootHref, 'GET', rootResource);
  });
});
从'@angular/common/http'导入{HttpClient};
进口{
HttpClientTestingModule,
HttpTestingController
}来自“@angular/common/http/testing”;
从'@angular/core/testing'导入{TestBed,inject};
从“rxjs/operators”导入{bufferCount};
从“rxjs”导入{zip};
从“./Node”导入{Node};
const rootHref='/api/nodes/root';
const rootChildrenHref='/api/nodes/root/childrenhref';
常量根资源={
_链接:{
“节点有子节点”:{href:rootChildrenHref}
}
};
函数expectOneRequest(控制器:HttpTestingController,href:string,方法:string,正文:any){
//以下'expectOne()'将匹配请求的URL和方法。
//如果没有请求或多个请求与该URL匹配
//'expectOne()'会扔。
const req=controller.expectOne({
方法,,
url:href
});
//使用模拟数据进行响应,导致可观察到的问题得到解决。
//订阅回调断言返回了正确的数据。
要求冲洗(主体);
}
描述('核心模块节点',()=>{
设httpTestingController:httpTestingController;
让主题:节点;
在每个之前(()=>{
TestBed.configureTestingModule({
导入:[HttpClientTestingModule]
});
});
每次之前(注入([HttpClient,HttpTestingController],
(http:HttpClient,testingController:HttpTestingController)=>{
主题=新节点(http,rootHref);
httpTestingController=测试控制器;
}));
之后(()=>{
//在每次测试之后,断言不再有挂起的请求。
httpTestingController.verify();
});
它('应该创建',()=>{
expect(subject.toBeTruthy();
});
它(“#提供了属性$”,()=>{
expect(subject.attributes$).toBeTruthy();
});
it(“#attributes$是可观察元素”,(完成:DoneFn)=>{
subject.attributes$.subscribe(根=>{
expect(root.toBeTruthy();
完成();
});
expectOneRequest(httpTestingController,rootHref,'GET',rootResource);
});
它(“#属性$observable已缓存”,(完成:DoneFn)=>{
zip(subject.attributes$,subject.attributes$).subscribe([root1,root2])=>{
expect(root1)、toBe(root2);
完成();
});
expectOneRequest(httpTestingController,rootHref,'GET',rootResource);
});
它(“#update()影响属性$”,(完成:DoneFn)=>{
//此管道末尾的订阅将触发子请求
//但是update()调用将触发一个资源请求,该请求反过来
//将触发子请求。因此bufferCount将生成
//大小为2的数组。
subject.attributes$.pipe(缓冲计数(2)).subscribe(集合=>{
期望值(集合长度)。toEqual(2);
});
subject.update([]).subscribe(root=>{
expect(root.toBeTruthy();
完成();
});
expectOneRequest(httpTestingController,rootHref,'GET',rootResource);
expectOneRequest(httpTestingController,rootHref,'PATCH',{});
expectOneRequest(httpTestingController,rootHref,'GET',rootResource);
});
它(“#update()返回observable”,(done:DoneFn)=>{
subject.update([]).subscribe(root=>{
expect(root.toBeTruthy();
完成();
});
expectOneRequest(httpTestingController,rootHref,'PATCH',{});
expectOneRequest(httpTestingController,rootHref,'GET',rootResource);
});
});
正如您所看到的,测试套件验证HTTP请求调用是否仅在有人按预期订阅了observable时发出。有了这个测试套件,我提出了节点类的以下实现:

import { HttpClient } from '@angular/common/http';
import { Observable, Subject } from 'rxjs';
import { mergeMap, shareReplay } from 'rxjs/operators';

/** Node
 *
 * A node is the representation of an artifact on the Sorbotics Platform
 * Registry.
 */
export class Node {

  /** Attributes */
  public attributes$: Observable<any>;

  private attributesSubject$: Subject<any> = new Subject();

  private initialFetch = false;

  constructor(private http: HttpClient, private href: string) {

    // the attributes$ observable is a custom implementation that allow us to
    // perform the http request on the first subscription
    this.attributes$ = new Observable(subscriber => {
      /* the Node resource is fetched if this is the first subscription to the
         observable */
      if (!this.initialFetch) {
        this.initialFetch = true;

        this.fetchResource()
          .subscribe(resource => this.attributesSubject$.next(resource));
      }

      // connect this subscriber to the subject
      this.attributesSubject$
        .subscribe(resource => subscriber.next(resource));
    });
  }

  /* Fetch Node resource on the Platform Registry */
  private fetchResource: () => Observable<any> =
    () => this.http.get(this.href)

  /** Update node
   *
   * This method implement the update of the node attributes. Once the update
   * is performed successfully the attributes$ observable will push new values 
   * to subscribed parties.
   *
   * @param patches Set of patches that describe the update.
   */
  public update(patches: any): Observable<any> {
    const req = this.http.patch(this.href, patches)
      .pipe(mergeMap(() => this.fetchResource()), shareReplay(1));

    req.subscribe(resource => this.attributesSubject$.next(resource));

    return req;
  }
}
从'@angular/common/http'导入{HttpClient};
从“rxjs”导入{observeable,Subject};
从“rxjs/operators”导入{mergeMap,shareReplay};
/**节点
*
*节点是Sorbotics平台上工件的表示
*登记处。
*/
导出类节点{
/**属性*/
公共属性$:可见;
私有属性Subject$:Subject=new Subject();
private initialFetch=false;
构造函数(私有http:HttpClient,私有href:string){
//attributes$observable是一个自定义实现,它允许我们
//对第一个订阅执行http请求
this.attributes$=newobservable(订阅者=>{
/*如果这是对的第一个订阅,则获取节点资源
可观察*/
如果(!this.initialFetch){
this.initialFetch=true;
this.fetchResource()
.subscribe(资源=>this.attributesubject$.next(资源));
}
//将此订阅服务器连接到主题
此.attributesSubject