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:
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
34
modules/@angular/platform-server/src/platform_state.ts
Normal file
34
modules/@angular/platform-server/src/platform_state.ts
Normal 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; }
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
71
modules/@angular/platform-server/src/utils.ts
Normal file
71
modules/@angular/platform-server/src/utils.ts
Normal 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));
|
||||
}
|
Reference in New Issue
Block a user