feat(platform-server): provide a way to hook into renderModule* (#19023)

A multi RENDER_MODULE_HOOK provider can provide function that will be called with the current document just before the document is rendered to
string.

This hook can for example be used for the state transfer module to serialize any server state that needs to be transported to the client, just before the current platform state is rendered to string.

PR Close #19023
This commit is contained in:
Vikram Subramanian 2017-09-04 00:38:42 -07:00 committed by Miško Hevery
parent 15945c8791
commit 8dfc3c386a
5 changed files with 97 additions and 4 deletions

View File

@ -8,7 +8,7 @@
export {PlatformState} from './platform_state'; export {PlatformState} from './platform_state';
export {ServerModule, platformDynamicServer, platformServer} from './server'; export {ServerModule, platformDynamicServer, platformServer} from './server';
export {INITIAL_CONFIG, PlatformConfig} from './tokens'; export {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformConfig} from './tokens';
export {renderModule, renderModuleFactory} from './utils'; export {renderModule, renderModuleFactory} from './utils';
export * from './private_export'; export * from './private_export';

View File

@ -24,3 +24,12 @@ export interface PlatformConfig {
* @experimental * @experimental
*/ */
export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL_CONFIG'); export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL_CONFIG');
/**
* A function that will be executed when calling `renderModuleFactory` or `renderModule` just
* before current platform state is rendered to string.
*
* @experimental
*/
export const BEFORE_APP_SERIALIZED =
new InjectionToken<Array<() => void>>('Server.RENDER_MODULE_HOOK');

View File

@ -14,7 +14,7 @@ import {toPromise} from 'rxjs/operator/toPromise';
import {PlatformState} from './platform_state'; import {PlatformState} from './platform_state';
import {platformDynamicServer, platformServer} from './server'; import {platformDynamicServer, platformServer} from './server';
import {INITIAL_CONFIG} from './tokens'; import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
interface PlatformOptions { interface PlatformOptions {
document?: string; document?: string;
@ -45,7 +45,22 @@ the server-rendered app can be properly bootstrapped into a client app.`);
return toPromise return toPromise
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable))) .call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
.then(() => { .then(() => {
const output = platform.injector.get(PlatformState).renderToString(); const platformState = platform.injector.get(PlatformState);
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
if (callbacks) {
for (const callback of callbacks) {
try {
callback();
} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
}
}
}
const output = platformState.renderToString();
platform.destroy(); platform.destroy();
return output; return output;
}); });

View File

@ -16,7 +16,7 @@ import {Http, HttpModule, Response, ResponseOptions, XHRBackend} from '@angular/
import {MockBackend, MockConnection} from '@angular/http/testing'; import {MockBackend, MockConnection} from '@angular/http/testing';
import {BrowserModule, DOCUMENT, Title} from '@angular/platform-browser'; import {BrowserModule, DOCUMENT, Title} from '@angular/platform-browser';
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
import {INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server'; import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, PlatformState, ServerModule, platformDynamicServer, renderModule, renderModuleFactory} from '@angular/platform-server';
import {Subscription} from 'rxjs/Subscription'; import {Subscription} from 'rxjs/Subscription';
import {filter} from 'rxjs/operator/filter'; import {filter} from 'rxjs/operator/filter';
import {first} from 'rxjs/operator/first'; import {first} from 'rxjs/operator/first';
@ -38,6 +38,50 @@ class MyServerApp {
class ExampleModule { class ExampleModule {
} }
function getTitleRenderHook(doc: any) {
return () => {
// Set the title as part of the render hook.
doc.title = 'RenderHook';
};
}
function exceptionRenderHook() {
throw new Error('error');
}
function getMetaRenderHook(doc: any) {
return () => {
// Add a meta tag before rendering the document.
const metaElement = doc.createElement('meta');
metaElement.setAttribute('name', 'description');
doc.head.appendChild(metaElement);
};
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]},
]
})
class RenderHookModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{provide: BEFORE_APP_SERIALIZED, useFactory: getTitleRenderHook, multi: true, deps: [DOCUMENT]},
{provide: BEFORE_APP_SERIALIZED, useValue: exceptionRenderHook, multi: true},
{provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]},
]
})
class MultiRenderHookModule {
}
@Component({selector: 'app', template: `Works too!`}) @Component({selector: 'app', template: `Works too!`})
class MyServerApp2 { class MyServerApp2 {
} }
@ -469,6 +513,28 @@ export function main() {
called = true; called = true;
}); });
})); }));
it('should call render hook', async(() => {
renderModule(RenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><title>RenderHook</title></head><body>' +
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
called = true;
});
}));
it('should call mutliple render hooks', async(() => {
const consoleSpy = spyOn(console, 'warn');
renderModule(MultiRenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><title>RenderHook</title><meta name="description"></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
expect(consoleSpy).toHaveBeenCalled();
called = true;
});
}));
}); });
describe('http', () => { describe('http', () => {

View File

@ -1,3 +1,6 @@
/** @experimental */
export declare const BEFORE_APP_SERIALIZED: InjectionToken<(() => void)[]>;
/** @experimental */ /** @experimental */
export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>; export declare const INITIAL_CONFIG: InjectionToken<PlatformConfig>;