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:
parent
6b98b534c8
commit
7102ea80a9
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
Loading…
Reference in New Issue