feat(platform-server): wait on returned BEFORE_APP_SERIALIZED promises (#29120)

This update gives external tooling the ability for async providers to
finish resolving before the document is serialized. This is not a
breaking change since render already returns a promise. All returned
promises from `BEFORE_APP_SERIALIZED` providers will wait to be
resolved or rejected. Any rejected promises will only console.warn().

PR Close #29120
This commit is contained in:
Adam Bradley 2019-03-05 16:30:45 -06:00 committed by Andrew Kushnir
parent 6b98b534c8
commit 7102ea80a9
2 changed files with 97 additions and 5 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type} from '@angular/core';
import {ApplicationRef, NgModuleFactory, NgModuleRef, PlatformRef, StaticProvider, Type, ɵisPromise} from '@angular/core';
import {ɵTRANSITION_ID} from '@angular/platform-browser';
import {first} from 'rxjs/operators';
@ -45,12 +45,18 @@ the server-rendered app can be properly bootstrapped into a client app.`);
.then(() => {
const platformState = platform.injector.get(PlatformState);
const asyncPromises: Promise<any>[] = [];
// 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();
const callbackResult = callback();
if (ɵisPromise(callbackResult)) {
asyncPromises.push(callbackResult);
}
} catch (e) {
// Ignore exceptions.
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
@ -58,9 +64,22 @@ the server-rendered app can be properly bootstrapped into a client app.`);
}
}
const output = platformState.renderToString();
platform.destroy();
return output;
const complete = () => {
const output = platformState.renderToString();
platform.destroy();
return output;
};
if (asyncPromises.length === 0) {
return complete();
}
return Promise
.all(asyncPromises.map(asyncPromise => {
return asyncPromise.catch(
e => { console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e); });
}))
.then(complete);
});
});
}

View File

@ -57,6 +57,24 @@ function getMetaRenderHook(doc: any) {
};
}
function getAsyncTitleRenderHook(doc: any) {
return () => {
// Async set the title as part of the render hook.
return new Promise(resolve => {
setTimeout(() => {
doc.title = 'AsyncRenderHook';
resolve();
});
});
};
}
function asyncRejectRenderHook() {
return () => {
return new Promise((_resolve, reject) => { setTimeout(() => { reject('reject'); }); });
};
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
@ -81,6 +99,39 @@ class RenderHookModule {
class MultiRenderHookModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{
provide: BEFORE_APP_SERIALIZED,
useFactory: getAsyncTitleRenderHook,
multi: true,
deps: [DOCUMENT]
},
]
})
class AsyncRenderHookModule {
}
@NgModule({
bootstrap: [MyServerApp],
declarations: [MyServerApp],
imports: [BrowserModule.withServerTransition({appId: 'render-hook'}), ServerModule],
providers: [
{provide: BEFORE_APP_SERIALIZED, useFactory: getMetaRenderHook, multi: true, deps: [DOCUMENT]},
{
provide: BEFORE_APP_SERIALIZED,
useFactory: getAsyncTitleRenderHook,
multi: true,
deps: [DOCUMENT]
},
{provide: BEFORE_APP_SERIALIZED, useFactory: asyncRejectRenderHook, multi: true},
]
})
class AsyncMultiRenderHookModule {
}
@Component({selector: 'app', template: `Works too!`})
class MyServerApp2 {
}
@ -699,6 +750,28 @@ class HiddenModule {
called = true;
});
}));
it('should call async render hooks', async(() => {
renderModule(AsyncRenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><title>AsyncRenderHook</title></head><body>' +
'<app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
called = true;
});
}));
it('should call multiple async and sync render hooks', async(() => {
const consoleSpy = spyOn(console, 'warn');
renderModule(AsyncMultiRenderHookModule, {document: doc}).then(output => {
// title should be added by the render hook.
expect(output).toBe(
'<html><head><meta name="description"><title>AsyncRenderHook</title></head>' +
'<body><app ng-version="0.0.0-PLACEHOLDER">Works!</app></body></html>');
expect(consoleSpy).toHaveBeenCalled();
called = true;
});
}));
});
describe('http', () => {