feat(common): new HttpClient API

HttpClient is an evolution of the existing Angular HTTP API, which exists
alongside of it in a separate package, @angular/common/http. This structure
ensures that existing codebases can slowly migrate to the new API.

The new API improves significantly on the ergonomics and features of the legacy
API. A partial list of new features includes:

* Typed, synchronous response body access, including support for JSON body types
* JSON is an assumed default and no longer needs to be explicitly parsed
* Interceptors allow middleware logic to be inserted into the pipeline
* Immutable request/response objects
* Progress events for both request upload and response download
* Post-request verification & flush based testing framework
This commit is contained in:
Alex Rickabaugh
2017-03-22 17:13:24 -07:00
committed by Jason Aden
parent 2a7ebbe982
commit 37797e2b4e
48 changed files with 5599 additions and 40 deletions

View File

@ -8,9 +8,11 @@
const xhr2: any = require('xhr2');
import {Injectable, Provider} from '@angular/core';
import {Injectable, Optional, Provider} from '@angular/core';
import {BrowserXhr, Connection, ConnectionBackend, Http, ReadyState, Request, RequestOptions, Response, XHRBackend, XSRFStrategy} from '@angular/http';
import {HttpClient, HttpRequest, HttpHandler, HttpInterceptor, HttpResponse, HTTP_INTERCEPTORS, HttpBackend, XhrFactory, ɵinterceptingHandler as interceptingHandler} from '@angular/common/http';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import {Subscription} from 'rxjs/Subscription';
@ -33,13 +35,9 @@ export class ServerXsrfStrategy implements XSRFStrategy {
configureRequest(req: Request): void {}
}
export class ZoneMacroTaskConnection implements Connection {
response: Observable<Response>;
lastConnection: Connection;
constructor(public request: Request, backend: XHRBackend) {
validateRequestUrl(request.url);
this.response = new Observable((observer: Observer<Response>) => {
export abstract class ZoneMacroTaskWrapper<S, R> {
wrap(request: S): Observable<R> {
return new Observable((observer: Observer<R>) => {
let task: Task = null !;
let scheduled: boolean = false;
let sub: Subscription|null = null;
@ -50,25 +48,26 @@ export class ZoneMacroTaskConnection implements Connection {
task = _task;
scheduled = true;
this.lastConnection = backend.createConnection(request);
sub = (this.lastConnection.response as Observable<Response>)
.subscribe(
res => savedResult = res,
err => {
if (!scheduled) {
throw new Error('invoke twice');
}
savedError = err;
scheduled = false;
task.invoke();
},
() => {
if (!scheduled) {
throw new Error('invoke twice');
}
scheduled = false;
task.invoke();
});
const delegate = this.delegate(request);
sub = delegate.subscribe(
res => savedResult = res,
err => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
savedError = err;
scheduled = false;
task.invoke();
},
() => {
if (!scheduled) {
throw new Error(
'An http observable was completed twice. This shouldn\'t happen, please file a bug.');
}
scheduled = false;
task.invoke();
});
};
const cancelTask = (_task: Task) => {
@ -91,11 +90,11 @@ export class ZoneMacroTaskConnection implements Connection {
}
};
// MockBackend is currently synchronous, which means that if scheduleTask is by
// MockBackend for Http is synchronous, which means that if scheduleTask is by
// scheduleMacroTask, the request will hit MockBackend and the response will be
// sent, causing task.invoke() to be called.
const _task = Zone.current.scheduleMacroTask(
'ZoneMacroTaskConnection.subscribe', onComplete, {}, () => null, cancelTask);
'ZoneMacroTaskWrapper.subscribe', onComplete, {}, () => null, cancelTask);
scheduleTask(_task);
return () => {
@ -111,6 +110,25 @@ export class ZoneMacroTaskConnection implements Connection {
});
}
protected abstract delegate(request: S): Observable<R>;
}
export class ZoneMacroTaskConnection extends ZoneMacroTaskWrapper<Request, Response> implements
Connection {
response: Observable<Response>;
lastConnection: Connection;
constructor(public request: Request, private backend: XHRBackend) {
super();
validateRequestUrl(request.url);
this.response = this.wrap(request);
}
delegate(request: Request): Observable<Response> {
this.lastConnection = this.backend.createConnection(request);
return this.lastConnection.response as Observable<Response>;
}
get readyState(): ReadyState {
return !!this.lastConnection ? this.lastConnection.readyState : ReadyState.Unsent;
}
@ -124,13 +142,34 @@ export class ZoneMacroTaskBackend implements ConnectionBackend {
}
}
export class ZoneClientBackend extends
ZoneMacroTaskWrapper<HttpRequest<any>, HttpResponse<any>> implements HttpBackend {
constructor(private backend: HttpBackend) { super(); }
handle(request: HttpRequest<any>): Observable<HttpResponse<any>> { return this.wrap(request); }
protected delegate(request: HttpRequest<any>): Observable<HttpResponse<any>> {
return this.backend.handle(request);
}
}
export function httpFactory(xhrBackend: XHRBackend, options: RequestOptions) {
const macroBackend = new ZoneMacroTaskBackend(xhrBackend);
return new Http(macroBackend, options);
}
export function zoneWrappedInterceptingHandler(
backend: HttpBackend, interceptors: HttpInterceptor[] | null) {
const realBackend: HttpBackend = interceptingHandler(backend, interceptors);
return new ZoneClientBackend(realBackend);
}
export const SERVER_HTTP_PROVIDERS: Provider[] = [
{provide: Http, useFactory: httpFactory, deps: [XHRBackend, RequestOptions]},
{provide: BrowserXhr, useClass: ServerXhr},
{provide: XSRFStrategy, useClass: ServerXsrfStrategy},
{provide: BrowserXhr, useClass: ServerXhr}, {provide: XSRFStrategy, useClass: ServerXsrfStrategy},
{
provide: HttpHandler,
useFactory: zoneWrappedInterceptingHandler,
deps: [HttpBackend, [new Optional(), HTTP_INTERCEPTORS]]
}
];

View File

@ -8,6 +8,7 @@
import {ɵAnimationEngine} from '@angular/animations/browser';
import {PlatformLocation, ɵPLATFORM_SERVER_ID as PLATFORM_SERVER_ID} from '@angular/common';
import {HttpClientModule} from '@angular/common/http';
import {platformCoreDynamic} from '@angular/compiler';
import {Injectable, InjectionToken, Injector, NgModule, NgZone, PLATFORM_ID, PLATFORM_INITIALIZER, PlatformRef, Provider, RendererFactory2, RootRenderer, Testability, createPlatformFactory, isDevMode, platformCore, ɵALLOW_MULTIPLE_PLATFORMS as ALLOW_MULTIPLE_PLATFORMS} from '@angular/core';
import {HttpModule} from '@angular/http';
@ -62,7 +63,7 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [
*/
@NgModule({
exports: [BrowserModule],
imports: [HttpModule, NoopAnimationsModule],
imports: [HttpModule, HttpClientModule, NoopAnimationsModule],
providers: [
SERVER_RENDER_PROVIDERS,
SERVER_HTTP_PROVIDERS,

View File

@ -12,6 +12,7 @@
"@angular/animations/browser": ["../../dist/packages/animations/browser"],
"@angular/core": ["../../dist/packages/core"],
"@angular/common": ["../../dist/packages/common"],
"@angular/common/http": ["../../dist/packages/common/http"],
"@angular/compiler": ["../../dist/packages/compiler"],
"@angular/http": ["../../dist/packages/http"],
"@angular/platform-browser": ["../../dist/packages/platform-browser"],