feat(platform-server): support @angular/http from @angular/platform-server

This change installs HttpModule with ServerModule, and overrides bindings to
service Http requests made from the server with the 'xhr2' NPM package.

Outgoing requests are wrapped in a Zone macro-task, so they will be tracked
within the Angular zone and cause the isStable API to show 'false' until they
return. This is essential for Universal support of server-side HTTP.
This commit is contained in:
Alex Rickabaugh
2017-02-10 17:00:27 -08:00
committed by Igor Minar
parent 30380d010b
commit 9559d3e949
13 changed files with 270 additions and 9 deletions

View File

@ -7,11 +7,14 @@
*/
import {PlatformLocation} from '@angular/common';
import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
import {ApplicationRef, CompilerFactory, Component, NgModule, NgModuleRef, NgZone, PlatformRef, destroyPlatform, getPlatform} from '@angular/core';
import {async, inject} from '@angular/core/testing';
import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/http';
import {MockBackend, MockConnection} from '@angular/http/testing';
import {DOCUMENT} from '@angular/platform-browser';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
import {Subscription} from 'rxjs/Subscription';
import {filter} from 'rxjs/operator/filter';
import {first} from 'rxjs/operator/first';
@ -21,7 +24,15 @@ import {toPromise} from 'rxjs/operator/toPromise';
class MyServerApp {
}
@NgModule({declarations: [MyServerApp], imports: [ServerModule], bootstrap: [MyServerApp]})
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [ServerModule],
providers: [
MockBackend,
{provide: XHRBackend, useExisting: MockBackend},
]
})
class ExampleModule {
}
@ -55,6 +66,30 @@ class MyStylesApp {
class ExampleStylesModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [HttpModule, ServerModule],
providers: [
MockBackend,
{provide: XHRBackend, useExisting: MockBackend},
]
})
export class HttpBeforeExampleModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [ServerModule, HttpModule],
providers: [
MockBackend,
{provide: XHRBackend, useExisting: MockBackend},
]
})
export class HttpAfterExampleModule {
}
export function main() {
if (getDOM().supportsDOMEvents()) return; // NODE only
@ -196,5 +231,86 @@ export function main() {
});
})));
});
describe('http', () => {
it('can inject Http', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(ExampleModule).then(ref => {
expect(ref.injector.get(Http) instanceof Http).toBeTruthy();
});
}));
it('can make Http requests', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(ExampleModule).then(ref => {
const mock = ref.injector.get(MockBackend);
const http = ref.injector.get(Http);
ref.injector.get(NgZone).run(() => {
NgZone.assertInAngularZone();
mock.connections.subscribe((mc: MockConnection) => {
NgZone.assertInAngularZone();
expect(mc.request.url).toBe('/testing');
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
});
http.get('/testing').subscribe(resp => {
NgZone.assertInAngularZone();
expect(resp.text()).toBe('success!');
});
});
});
}));
it('requests are macrotasks', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(ExampleModule).then(ref => {
const mock = ref.injector.get(MockBackend);
const http = ref.injector.get(Http);
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
ref.injector.get(NgZone).run(() => {
NgZone.assertInAngularZone();
mock.connections.subscribe((mc: MockConnection) => {
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
});
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
});
});
}));
it('works when HttpModule is included before ServerModule', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(HttpBeforeExampleModule).then(ref => {
const mock = ref.injector.get(MockBackend);
const http = ref.injector.get(Http);
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
ref.injector.get(NgZone).run(() => {
NgZone.assertInAngularZone();
mock.connections.subscribe((mc: MockConnection) => {
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
});
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
});
});
}));
it('works when HttpModule is included after ServerModule', async(() => {
const platform = platformDynamicServer(
[{provide: INITIAL_CONFIG, useValue: {document: '<app></app>'}}]);
platform.bootstrapModule(HttpAfterExampleModule).then(ref => {
const mock = ref.injector.get(MockBackend);
const http = ref.injector.get(Http);
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeFalsy();
ref.injector.get(NgZone).run(() => {
NgZone.assertInAngularZone();
mock.connections.subscribe((mc: MockConnection) => {
expect(ref.injector.get(NgZone).hasPendingMacrotasks).toBeTruthy();
mc.mockRespond(new Response(new ResponseOptions({body: 'success!', status: 200})));
});
http.get('/testing').subscribe(resp => { expect(resp.text()).toBe('success!'); });
});
});
}));
});
});
}