feat(platform-server): support document reference in render functions (#47032)

This commit updates the `renderModule` and `renderApplication` functions to also accept a document reference (in addition to the serialized document contents as a string). This should provide the necessary flexibility to NgUniversal (and other API consumers) to structure the logic to avoid serializing and parsing document multiple times during the SSR request.

PR Close #47032
This commit is contained in:
Andrew Kushnir
2022-08-03 14:19:40 -07:00
committed by Dylan Hunn
parent de1e28006b
commit 2b4d7f6733
4 changed files with 51 additions and 11 deletions
+8 -3
View File
@@ -81,9 +81,14 @@ export class ServerModule {
}
function _document(injector: Injector) {
let config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
const document = config && config.document ? parseDocument(config.document, config.url) :
getDOM().createHtmlDocument();
const config: PlatformConfig|null = injector.get(INITIAL_CONFIG, null);
let document: Document;
if (config && config.document) {
document = typeof config.document === 'string' ? parseDocument(config.document, config.url) :
config.document;
} else {
document = getDOM().createHtmlDocument();
}
// Tell ivy about the global document
ɵsetDocument(document);
return document;
+8 -5
View File
@@ -16,7 +16,7 @@ import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG} from './tokens';
import {TRANSFER_STATE_SERIALIZATION_PROVIDERS} from './transfer_state';
interface PlatformOptions {
document?: string;
document?: string|Document;
url?: string;
platformProviders?: Provider[];
}
@@ -95,14 +95,16 @@ the server-rendered app can be properly bootstrapped into a client app.`);
/**
* Renders a Module to string.
*
* `document` is the full document HTML of the page to render, as a string.
* `document` is the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* `url` is the URL for the current render request.
* `extraProviders` are the platform level providers for the current render request.
*
* @publicApi
*/
export function renderModule<T>(
module: Type<T>, options: {document?: string, url?: string, extraProviders?: StaticProvider[]}):
module: Type<T>,
options: {document?: string|Document, url?: string, extraProviders?: StaticProvider[]}):
Promise<string> {
const {document, url, extraProviders: platformProviders} = options;
const platform = _getPlatform(platformDynamicServer, {document, url, platformProviders});
@@ -130,7 +132,8 @@ export function renderModule<T>(
* - `appId` - a string identifier of this application. The appId is used to prefix all
* server-generated stylings and state keys of the application in TransferState
* use-cases.
* - `document` - the full document HTML of the page to render, as a string.
* - `document` - the document of the page to render, either as an HTML string or
* as a reference to the `document` instance.
* - `url` - the URL for the current render request.
* - `providers` - set of application level providers for the current render request.
* - `platformProviders` - the platform level providers for the current render request.
@@ -141,7 +144,7 @@ export function renderModule<T>(
*/
export function renderApplication<T>(rootComponent: Type<T>, options: {
appId: string,
document?: string,
document?: string|Document,
url?: string,
providers?: Array<Provider|ImportedNgModuleProviders>,
platformProviders?: Provider[],
@@ -11,7 +11,7 @@ import {DOCUMENT, isPlatformServer, PlatformLocation, ɵgetDOM as getDOM} from '
import {HTTP_INTERCEPTORS, HttpClient, HttpClientModule, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';
import {ApplicationRef, CompilerFactory, Component, destroyPlatform, getPlatform, HostBinding, HostListener, importProvidersFrom, Inject, Injectable, Input, NgModule, NgZone, OnInit, PLATFORM_ID, PlatformRef, Type, ViewEncapsulation} from '@angular/core';
import {inject, waitForAsync} from '@angular/core/testing';
import {inject, TestBed, waitForAsync} from '@angular/core/testing';
import {BrowserModule, makeStateKey, Title, TransferState} from '@angular/platform-browser';
import {BEFORE_APP_SERIALIZED, INITIAL_CONFIG, platformDynamicServer, PlatformState, renderModule, renderModuleFactory, ServerModule, ServerTransferStateModule} from '@angular/platform-server';
import {Observable} from 'rxjs';
@@ -750,6 +750,38 @@ describe('platform-server integration', () => {
});
}));
it(`using ${isStandalone ? 'renderApplication' : 'renderModule'} ` +
`should allow passing a document reference`,
waitForAsync(() => {
const document = TestBed.inject(DOCUMENT);
// Append root element based on the app selector.
const rootEl = document.createElement('app');
document.body.appendChild(rootEl);
// Append a special marker to verify that we use a correct instance
// of the document for rendering.
const markerEl = document.createComment('test marker');
document.body.appendChild(markerEl);
const options = {document};
const bootstrap = isStandalone ?
renderApplication(MyAsyncServerAppStandalone, {document, appId: 'simple-cmp'}) :
renderModule(AsyncServerModule, options);
bootstrap
.then(output => {
expect(output).toBe(
'<html><head><title>fakeTitle</title></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!<h1 textcontent="fine">fine</h1></app>' +
'<!--test marker--></body></html>');
called = true;
})
.finally(() => {
rootEl.remove();
markerEl.remove();
});
}));
it('works with SVG elements', waitForAsync(() => {
const options = {document: doc};
const bootstrap = isStandalone ?