Angular 2 Http, Observables и рекурсивные запросы

У меня есть конечная точка REST, которая возвращает список элементов, максимум 1000 элементов за раз. Если имеется более 1000 элементов, ответ имеет статус HTTP 206, и есть Next-Range заголовок, который я могу использовать в своем следующем запросе для получения большего количества предметов.

Я работаю над приложением Angular 2 и пытаюсь реализовать это с Http а также Observable, Моя проблема в том, что я не знаю, как объединить несколько Observable в зависимости от количества страниц элементов и, наконец, вернуть один Observable что мой компонент может подписаться.

Вот где я получил мою текущую реализацию TypeScript:

// NOTE: Non-working example!

getAllItems(): Observable<any[]> {
  // array of all items, possibly received with multiple requests
  const allItems: any[] = [];

  // inner function for getting a range of items
  const getRange = (range?: string) => {
    const headers: Headers = new Headers();
    if (range) {
      headers.set('Range', range);
    }

    return this.http.get('http://api/endpoint', { headers })
      .map((res: Response) => {
        // add all to received items
        // (maybe not needed if the responses can be merged some other way?)
        allItems.push.apply(allItems, res.json());

        // partial content
        if (res.status === 206) {
          const nextRange = res.headers.get('Next-Range');

          // get next range of items
          return getRange(nextRange);
        }

        return allItems;
      });
  };

  // get first range
  return getRange();
}

Тем не менее, это не работает. Если я правильно понял, Observable возвращается как значение начального Observable а не массив предметов.

4 ответа

Решение

Вы можете реализовать это с помощью оператора расширения. Что вы действительно хотите сделать, это создать рекурсивную плоскую карту. Это именно то, для чего был создан оператор расширения.

Вот фрагмент кода того, как это работает:

let times = true;
// This is a mock method for your http.get call
const httpMock = () => {
  if(times) {
    times = false;
    return Rx.Observable.of({items: ["1", "2", "3"], next: true});
  } else {
    return Rx.Observable.of({items: ["4", "5", "6"], next: false});
  }
}

httpMock()
  .expand(obj => {
    // In your case, the obj will be the response
    // implement your logic here if the 206 http header is found
    if(obj.next) {
      // If you have next values, just call the http.get method again
      // In my example it's the httpMock
      return httpMock();
    } else {
      return Rx.Observable.empty();
    }
  })
  .map(obj => obj.items.flatMap(array => array)) 
  .reduce((acc, x) => acc.concat(x), []);
  .subscribe((val) => console.log(val));

Что делает, так это макетирует первый http-запрос, который имеет свойство 'next' для true. Это соответствует вашему заголовку 206. Затем мы делаем второй вызов, который имеет свойство next для false.

Результатом является массив, содержащий результаты обоих запросов. Это применимо и для большего количества запросов благодаря оператору расширения.

Рабочий пример jsbin можно найти здесь: http://jsbin.com/wowituluqu/edit?js,console

РЕДАКТИРОВАТЬ: обновлено для работы с вызовом http, который возвращает массив из массивов, а конечным результатом является один массив, который содержит все элементы из массивов.

Если вы хотите получить в результате массив с отдельными массивами из запроса, все еще находящимися внутри, просто удалите плоскую карту и верните элементы напрямую. Обновите codepen здесь: http://codepen.io/anon/pen/xRZyaZ?editors=0010

Я работал с небольшими изменениями в примере с KwintenP:

// service.ts

getAllItems(): Observable<any[]> {
  const getRange = (range?: string): Observable<any> => {
    const headers: Headers = new Headers();
    if (range) {
      headers.set('Range', range);
    }

    return this.http.get('http://api/endpoint', { headers });
  };

  return getRange().expand((res: Response) => {
    if (res.status === 206) {
      const nextRange = res.headers.get('Next-Range');

      return getRange(nextRange);
    } else {
      return Observable.empty();
    }
  }).map((res: Response) => res.json());
}

В компоненте, который подписывается на ObservableМне пришлось добавить завершенный обработчик:

// component.ts

const temp = [];

service.getAllItems().subscribe(
  items => {
    // page received, push items to temp
    temp.push.apply(temp, items);
  },
  err => {
    // handle error
  },
  () => {
    // completed, expose temp to component
    this.items = temp;
  }
);

В последней версии angular 6+ (ответ сам по себе возвращает JSON), RxJs 6+ (использует операторы в конвейерном режиме).

getAllItems(): Observable<any[]> {
 const getRange = (range?: string): Observable<any> => {
 const headers: Headers = new Headers();
 if (range) {
   headers.set('Range', range);
 }
 return this.http.get('http://api/endpoint', { headers });
};

return getRange().pipe(expand((res: Response) => {
  if (res['status'] === 206) {
   const nextRange = res['headers'].get('Next-Range');
   return getRange(nextRange);
  } else {
  return EMPTY;
  }
 }));
}

Просто на тот случай, если кто-то еще столкнется с этим. Шаблон, который я использую, использует ту же концепцию расширения. Однако это действительно "полный" пример, когда вам нужно преобразовать ответы с сервера в другой вид Observable как пример Visa Kopu выше.

Я разбил каждый "шаг", чтобы поток описывался в методах (вместо написания самой компактной версии). Я думаю, что это немного более познавательно.

import {Injectable} from '@angular/core';
import {HttpClient, HttpParams, HttpResponse} from '@angular/common/http';
import {EMPTY, Observable} from 'rxjs';
import {expand, map} from 'rxjs/operators';

// this service is consuming a backend api that is calling/proxying a Salesforce query that is paginated
@Injectable({providedIn: 'root'})
export class ExampleAccountService {

    constructor(protected http: HttpClient) {
    }

    // this method maps the 'pages' of AccountsResponse objects to a single Observable array of Account objects
    allAccounts(): Observable<Account[]> {
        const accounts: Account[] = [];
        return this.aPageOfAccounts(null).pipe(
            map((ret: HttpResponse<AccountsResponse>) => {
                for (const account of ret.body.accounts) {
                    accounts.push(account);
                }
                return accounts;
            })
        );
    }

    // recursively fetch pages of accounts until there are no more pages
    private aPageOfAccounts(page): Observable<HttpResponse<AccountsResponse>> {
        return this.fetchAccountsFromServer(page).pipe(
            expand((res: HttpResponse<AccountsResponse>) => {
                if (res.body.nextRecordsUrl) {
                    return this.aPageOfAccounts(res.body.nextRecordsUrl);
                } else {
                    return EMPTY;
                }
            }));
    }

    // this one does the actual fetch to the server
    private fetchAccountsFromServer(page: string): Observable<HttpResponse<AccountsResponse>> {
        const options = createRequestOption({page});
        return this.http.get<AccountsResponse>(`https://wherever.com/accounts/page`,
            {params: options, observe: 'response'});
    }
}

export class AccountsResponse {
    constructor(public totalSize?: number,
                public done?: boolean,
                public nextRecordsUrl?: string,
                public accounts?: Account[]) {
    }
}

export class Account {
    constructor(public id?: string,
                public name?: string
    ) {

    }
}

export const createRequestOption = (req?: any): HttpParams => {
    let options: HttpParams = new HttpParams();
    if (req) {
        Object.keys(req).forEach((key) => {
            if (key !== 'sort') {
                options = options.set(key, req[key]);
            }
        });
        if (req.sort) {
            req.sort.forEach((val) => {
                options = options.append('sort', val);
            });
        }
    }
    return options;
};

Ответы выше полезны. Мне пришлось извлекать данные с помощью API подкачки рекурсивным способом, и я создал фрагмент кода, который вычисляет факториал.

Другие вопросы по тегам