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
|
* 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 {ɵTRANSITION_ID} from '@angular/platform-browser';
|
||||||
import {first} from 'rxjs/operators';
|
import {first} from 'rxjs/operators';
|
||||||
|
|
||||||
@ -45,12 +45,18 @@ the server-rendered app can be properly bootstrapped into a client app.`);
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
const platformState = platform.injector.get(PlatformState);
|
const platformState = platform.injector.get(PlatformState);
|
||||||
|
|
||||||
|
const asyncPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
|
// Run any BEFORE_APP_SERIALIZED callbacks just before rendering to string.
|
||||||
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
|
const callbacks = moduleRef.injector.get(BEFORE_APP_SERIALIZED, null);
|
||||||
if (callbacks) {
|
if (callbacks) {
|
||||||
for (const callback of callbacks) {
|
for (const callback of callbacks) {
|
||||||
try {
|
try {
|
||||||
callback();
|
const callbackResult = callback();
|
||||||
|
if (ɵisPromise(callbackResult)) {
|
||||||
|
asyncPromises.push(callbackResult);
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore exceptions.
|
// Ignore exceptions.
|
||||||
console.warn('Ignoring BEFORE_APP_SERIALIZED Exception: ', e);
|
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();
|
const complete = () => {
|
||||||
platform.destroy();
|
const output = platformState.renderToString();
|
||||||
return output;
|
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({
|
@NgModule({
|
||||||
bootstrap: [MyServerApp],
|
bootstrap: [MyServerApp],
|
||||||
declarations: [MyServerApp],
|
declarations: [MyServerApp],
|
||||||
@ -81,6 +99,39 @@ class RenderHookModule {
|
|||||||
class MultiRenderHookModule {
|
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!`})
|
@Component({selector: 'app', template: `Works too!`})
|
||||||
class MyServerApp2 {
|
class MyServerApp2 {
|
||||||
}
|
}
|
||||||
@ -699,6 +750,28 @@ class HiddenModule {
|
|||||||
called = true;
|
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', () => {
|
describe('http', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user