我有一个REST端点,它返回一个项目列表,一次最多1000个项目。如果有超过1000个项目,则响应具有HTTP状态206,并且我可以在下一个获取更多项目的请求中使用Next-Range
标题。
我正在开发一个Angular 2应用程序并尝试使用Http
和Observable
来实现这一点。我的问题是,我不知道如何合并多个Observable
s ,具体取决于有多少个项目页面,最终返回一个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
的值,而不是项目数组。
答案 0 :(得分:11)
您可以使用expand运算符实现此功能。你真正想要做的是创建一个递归的flatmap。这正是为运营商展开的内容。
以下是其工作原理的代码段:
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请求是什么,它有一个&#39; next&#39;财产到真。这匹配您的206标头。然后我们再拨打第二个电话,其中包含下一个&#39;财产到假。
结果是一个包含两个请求结果的数组。由于扩展运算符,它也适用于更多请求。
可以在此处找到工作jsbin示例:this question
编辑:更新以使用从数组返回数组的http调用,最终结果是包含数组中所有元素的单个数组。
如果您希望结果中包含来自请求的单独数组的数组,只需删除flatmap并直接返回项目。在这里更新codepen: http://jsbin.com/wowituluqu/edit?js,console
答案 1 :(得分:3)
我对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;
}
);
答案 2 :(得分:1)
在最新版本中,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;
}
}));
}
答案 3 :(得分:0)
上述答案很有用。我必须以递归方式使用分页API获取数据,并创建code snippet 它计算阶乘。
答案 4 :(得分:0)
以防万一其他人遇到这个问题。我正在使用的模式使用了expand的相同概念。但是,当您需要将来自服务器的响应转换为另一种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;
};