Angular 2 Http,Observables和递归请求

时间:2016-11-10 13:50:14

标签: angular rxjs observable angular2-http

我有一个REST端点,它返回一个项目列表,一次最多1000个项目。如果有超过1000个项目,则响应具有HTTP状态206,并且我可以在下一个获取更多项目的请求中使用Next-Range标题。

我正在开发一个Angular 2应用程序并尝试使用HttpObservable来实现这一点。我的问题是,我不知道如何合并多个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的值,而不是项目数组。

5 个答案:

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