feat(platform-server): add API to render Module and ModuleFactory to string (#14381)

- PlatformState provides an interface to serialize the current Platform State as a string or Document.

- renderModule and renderModuleFactory are convenience methods to wait for Angular Application to stabilize and then render the state to a string.

- refactor code to remove defaultDoc from DomAdapter and inject DOCUMENT where it's needed.
This commit is contained in:
vikerman
2017-02-14 16:14:40 -08:00
committed by Igor Minar
parent 2f2b65bd38
commit b4d444a0a7
39 changed files with 462 additions and 187 deletions

View File

@ -7,7 +7,8 @@
*/
import {LocationChangeEvent, LocationChangeListener, PlatformLocation} from '@angular/common';
import {Injectable} from '@angular/core';
import {Inject, Injectable} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {Subject} from 'rxjs/Subject';
import * as url from 'url';
@ -27,7 +28,9 @@ export class ServerPlatformLocation implements PlatformLocation {
private _hash: string = '';
private _hashUpdate = new Subject<LocationChangeEvent>();
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(); }
constructor(@Inject(DOCUMENT) private _doc: any) {}
getBaseHrefFromDOM(): string { return getDOM().getBaseHref(this._doc); }
onPopState(fn: LocationChangeListener): void {
// No-op: a state stack is not implemented, so

View File

@ -22,14 +22,20 @@ const _attrToPropMap: {[key: string]: string} = {
'tabindex': 'tabIndex',
};
let defDoc: any = null;
const mapProps = ['attribs', 'x-attribsNamespace', 'x-attribsPrefix'];
function _notImplemented(methodName: string) {
return new Error('This method is not implemented in Parse5DomAdapter: ' + methodName);
}
/**
* Parses a document string to a Document object.
*/
export function parseDocument(html: string) {
return parse5.parse(html, {treeAdapter: parse5.treeAdapters.htmlparser2});
}
/* tslint:disable:requireParameterType */
/**
* A `DomAdapter` powered by the `parse5` NodeJS module.
@ -72,7 +78,6 @@ export class Parse5DomAdapter extends DomAdapter {
get attrToPropMap() { return _attrToPropMap; }
query(selector: any) { throw _notImplemented('query'); }
querySelector(el: any, selector: string): any { return this.querySelectorAll(el, selector)[0]; }
querySelectorAll(el: any, selector: string): any[] {
const res: any[] = [];
@ -468,7 +473,7 @@ export class Parse5DomAdapter extends DomAdapter {
}
createHtmlDocument(): Document {
const newDoc = treeAdapter.createDocument();
newDoc.title = 'fake title';
newDoc.title = 'fakeTitle';
const head = treeAdapter.createElement('head', null, []);
const body = treeAdapter.createElement('body', 'http://www.w3.org/1999/xhtml', []);
this.appendChild(newDoc, head);
@ -478,10 +483,9 @@ export class Parse5DomAdapter extends DomAdapter {
newDoc['_window'] = {};
return newDoc;
}
defaultDoc(): Document { return defDoc = defDoc || this.createHtmlDocument(); }
getBoundingClientRect(el: any): any { return {left: 0, top: 0, width: 0, height: 0}; }
getTitle(): string { return this.defaultDoc().title || ''; }
setTitle(newTitle: string) { this.defaultDoc().title = newTitle; }
getTitle(doc: Document): string { return doc.title || ''; }
setTitle(doc: Document, newTitle: string) { doc.title = newTitle; }
isTemplateElement(el: any): boolean {
return this.isElementNode(el) && this.tagName(el) === 'template';
}
@ -538,17 +542,17 @@ export class Parse5DomAdapter extends DomAdapter {
}
supportsDOMEvents(): boolean { return false; }
supportsNativeShadowDOM(): boolean { return false; }
getGlobalEventTarget(target: string): any {
getGlobalEventTarget(doc: Document, target: string): any {
if (target == 'window') {
return (<any>this.defaultDoc())._window;
return (<any>doc)._window;
} else if (target == 'document') {
return this.defaultDoc();
return doc;
} else if (target == 'body') {
return this.defaultDoc().body;
return doc.body;
}
}
getBaseHref(): string {
const base = this.querySelector(this.defaultDoc(), 'base');
getBaseHref(doc: Document): string {
const base = this.querySelector(doc, 'base');
let href = '';
if (base) {
href = this.getHref(base);

View File

@ -6,6 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
export {ServerModule, platformDynamicServer, platformServer} from './server';
export {PlatformState} from './platform_state';
export {INITIAL_CONFIG, ServerModule, platformDynamicServer, platformServer} from './server';
export {renderModule, renderModuleFactory} from './utils';
export * from './private_export';
export {VERSION} from './version';

View File

@ -0,0 +1,34 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const parse5 = require('parse5');
import {Injectable, Inject} from '@angular/core';
import {DOCUMENT} from '@angular/platform-browser';
import {getDOM} from './private_import_platform-browser';
/**
* Representation of the current platform state.
*
* @experimental
*/
@Injectable()
export class PlatformState {
constructor(@Inject(DOCUMENT) private _doc: any) {}
/**
* Renders the current state of the platform to string.
*/
renderToString(): string { return getDOM().getInnerHTML(this._doc); }
/**
* Returns the current DOM state.
*/
getDocument(): any { return this._doc; }
}

View File

@ -8,13 +8,14 @@
import {PlatformLocation} from '@angular/common';
import {platformCoreDynamic} from '@angular/compiler';
import {Injectable, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {Injectable, InjectionToken, Injector, NgModule, PLATFORM_INITIALIZER, PlatformRef, Provider, RootRenderer, createPlatformFactory, isDevMode, platformCore} from '@angular/core';
import {BrowserModule, DOCUMENT} from '@angular/platform-browser';
import {ServerPlatformLocation} from './location';
import {Parse5DomAdapter} from './parse5_adapter';
import {Parse5DomAdapter, parseDocument} from './parse5_adapter';
import {PlatformState} from './platform_state';
import {DebugDomRootRenderer} from './private_import_core';
import {DomAdapter, SharedStylesHost} from './private_import_platform-browser';
import {SharedStylesHost, getDOM} from './private_import_platform-browser';
import {ServerRootRenderer} from './server_renderer';
@ -23,15 +24,16 @@ function notSupported(feature: string): Error {
}
export const INTERNAL_SERVER_PLATFORM_PROVIDERS: Array<any /*Type | Provider | any[]*/> = [
{provide: PLATFORM_INITIALIZER, useValue: initParse5Adapter, multi: true},
{provide: DOCUMENT, useFactory: _document, deps: [Injector]},
{provide: PLATFORM_INITIALIZER, useFactory: initParse5Adapter, multi: true, deps: [Injector]},
{provide: PlatformLocation, useClass: ServerPlatformLocation},
PlatformState,
];
function initParse5Adapter() {
Parse5DomAdapter.makeCurrent();
function initParse5Adapter(injector: Injector) {
return () => { Parse5DomAdapter.makeCurrent(); };
}
export function _createConditionalRootRenderer(rootRenderer: any) {
if (isDevMode()) {
return new DebugDomRootRenderer(rootRenderer);
@ -46,15 +48,46 @@ export const SERVER_RENDER_PROVIDERS: Provider[] = [
SharedStylesHost
];
/**
* Config object passed to initialize the platform.
*
* @experimental
*/
export interface PlatformConfig {
document?: string;
url?: string;
}
/**
* The DI token for setting the initial config for the platform.
*
* @experimental
*/
export const INITIAL_CONFIG = new InjectionToken<PlatformConfig>('Server.INITIAL_CONFIG');
/**
* The ng module for the server.
*
* @experimental
*/
@NgModule({exports: [BrowserModule], providers: SERVER_RENDER_PROVIDERS})
@NgModule({
exports: [BrowserModule],
providers: [
SERVER_RENDER_PROVIDERS,
]
})
export class ServerModule {
}
function _document(injector: Injector) {
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
if (config && config.document) {
return parseDocument(config.document);
} else {
return getDOM().createHtmlDocument();
}
}
/**
* @experimental
*/

View File

@ -136,7 +136,7 @@ export class ServerRenderer implements Renderer {
}
listenGlobal(target: string, name: string, callback: Function): Function {
const renderElement = getDOM().getGlobalEventTarget(target);
const renderElement = getDOM().getGlobalEventTarget(this._rootRenderer.document, target);
return this.listen(renderElement, name, callback);
}

View File

@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, Provider, Type, destroyPlatform} from '@angular/core';
import {filter} from 'rxjs/operator/filter';
import {first} from 'rxjs/operator/first';
import {toPromise} from 'rxjs/operator/toPromise';
import {PlatformState} from './platform_state';
import {INITIAL_CONFIG, platformDynamicServer, platformServer} from './server';
const parse5 = require('parse5');
export interface PlatformOptions {
document?: string;
url?: string;
extraProviders?: Provider[];
}
function _getPlatform(
platformFactory: (extraProviders: Provider[]) => PlatformRef,
options: PlatformOptions): PlatformRef {
const extraProviders = options.extraProviders ? options.extraProviders : [];
return platformFactory([
{provide: INITIAL_CONFIG, useValue: {document: options.document, url: options.url}},
extraProviders
]);
}
function _render<T>(
platform: PlatformRef, moduleRefPromise: Promise<NgModuleRef<T>>): Promise<string> {
return moduleRefPromise.then((moduleRef) => {
const applicationRef: ApplicationRef = moduleRef.injector.get(ApplicationRef);
return toPromise
.call(first.call(filter.call(applicationRef.isStable, (isStable: boolean) => isStable)))
.then(() => {
const output = platform.injector.get(PlatformState).renderToString();
destroyPlatform();
return output;
});
});
}
/**
* Renders a Module to string.
*
* Do not use this in a production server environment. Use pre-compiled {@link NgModuleFactory} with
* {link renderModuleFactory} instead.
*
* @experimental
*/
export function renderModule<T>(module: Type<T>, options: PlatformOptions): Promise<string> {
const platform = _getPlatform(platformDynamicServer, options);
return _render(platform, platform.bootstrapModule(module));
}
/**
* Renders a {@link NgModuleFactory} to string.
*
* @experimental
*/
export function renderModuleFactory<T>(
moduleFactory: NgModuleFactory<T>, options: PlatformOptions): Promise<string> {
const platform = _getPlatform(platformServer, options);
return _render(platform, platform.bootstrapModuleFactory(moduleFactory));
}