Angular HttpClient Howto повторить http запрос с общим ответом - PullRequest
0 голосов
/ 04 июля 2018

У меня проблема с HttpClient of Angular. Я хотел бы знать, как сделать запрос с его повторным воспроизведением для повторения. Рассмотрим следующий пример кода:

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

Я пытаюсь заставить наблюдаемое attributes$ уведомлять о новом значении после отправки на ресурс PATCH запроса.

Обратите внимание, что наблюдаемая attributes$ выполняет только первый http-запрос, если кто-то на него подписан. Доступ к атрибуту не имеет никакого эффекта, как и должно быть. Это важная особенность, которую я хочу сохранить.

Есть ли способ сделать это?

Заранее спасибо

1 Ответ

0 голосов
/ 06 июля 2018

В итоге я решил проблему, поэтому поделюсь с вами своим решением. Сначала начните говорить, что я подхожу к проблеме, используя 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);
  });
});

Как вы можете видеть, набор тестов проверяет, что вызовы HTTP-запроса выполняются, только если кто-то подписывается на наблюдаемое, как предполагалось. С этим набором тестов я придумаю следующую реализацию класса Node:

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

Как вы можете видеть, я учусь на ответе, указанном в комментарии, сделанном @jonrsharpe, но представляю использование настраиваемой обработки подписки в обозримом. Таким образом, я могу отложить HTTP-запрос до первой подписки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...