アイテムのリストを返すRESTエンドポイント(一度に最大1000アイテム)があります。1000を超えるアイテムがある場合、応答のHTTPステータスは206で、Next-Range
があります。より多くのアイテムを取得するための次のリクエストで使用できるヘッダー。
私はan Angular 2 applicationで、これをHttp
とObservable
で実装しようとしています。私の問題は私がわからない複数のObservable
sをマージする方法アイテムのページ数に応じて最終的に1つの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
の値として返されます。
これは、expand演算子を使用して実装できます。実際にやりたいのは、再帰的なフラットマップを作成することです。それがまさに演算子expandが作成された目的です。
これがどのように機能するかのコードスニペットは次のとおりです。
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));
これは、「next」プロパティがtrueである最初のhttpリクエストをモックすることです。これは206ヘッダーと一致します。次に、 'next'プロパティがfalseである2番目の呼び出しを行います。
結果は、両方のリクエストの結果を含む配列です。エキスパンド演算子のおかげで、より多くのリクエストにも適用できます。
実用的な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;
}
}));
}
他の誰かがこれに遭遇した場合に備えて。私が使用しているパターンは、同じ拡張の概念を使用しています。ただし、上記のVisa Kopuの例のように、サーバーからの応答を別の種類のObservable
に変換する必要がある場合、これは実際には「完全な」例です。
フローがメソッドでキャプチャされるように、各「ステップ」を分割しました(最もコンパクトなバージョンを作成する代わりに)。この方法でもう少し学習しやすいと思います。
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を使用して再帰的にデータをフェッチする必要があり、階乗を計算する コードスニペット を作成しました。