I have recently begun converting my Angular 7.2.0 app to an Angular universal app. The app is successfully rendering server-side, and transfers state fairly quickly on fast PCs and internet connections, but unfortunately until the hand-off, the rendered page is missing any data normally fetched from the API.
I have added the serverURL
to a provided token from the server, and while rendering server-side, all http requests are made using the fully qualified url. However, it appears that does not matter, since all http calls are cancelled before they complete! My server shows no calls to the api endpoints, and the client logs an error of {}
(which shows up as [Error]
by default).
I am using the TransferStateModule
, as well as the TransferHttpCacheModule
, but nothing has worked so far. It seems like the express-engine render just will not wait for async http calls. It affects the http calls in ngOnInit
of components, the constructor
s of services, and any in resolver
s, so I don't know how else to get the data.
For things like config data, I am injecting the necessary fields as a token, and checking that token on server-side renders, but I cannot do that for all data in the app.
Below are the relevant sections of my server and app modules
app.ts (node express server)
app.get('*.*', express.static(APP_CONFIG.client_root, {maxAge: 0}));
// Render Angular Universal
if (APP_CONFIG.universal) {
// Our index.html we'll use as our template
const templateFile = HelpersService.tryLoad(join(APP_CONFIG.client_root, './index.html'));
const config = configService.getConfig();
const template = templateFile.toString();
const win = domino.createWindow(template);
const fakeanimation = {
value: () => {
return {
enumerable: true,
configurable: true
};
},
};
global['window'] = win;
global['document'] = {...win.document,
createElement: () => {},
body: {
...win.document.body,
style: {
...win.document.body.style,
transform: fakeanimation,
opacity: fakeanimation,
bottom: fakeanimation,
left: fakeanimation,
}
}
};
global['navigator'] = win.navigator;
global['location'] = win.location;
const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('../ssr/ssr.js');
app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP),
]
}));
app.set('view engine', 'html');
app.set('views', APP_CONFIG.client_root);
app.get('*', (req, res, next) => {
const protocol = res.locals.protocol || req.protocol;
const showPort = ((APP_CONFIG.port === 80 && protocol === 'http') || (protocol === 'https')) ? false : true;
const serverUrl = `${protocol}://${req.hostname}${showPort ? ':'+APP_CONFIG.port : ''}`;
res.render('index', {
req,
document: template,
url: req.url,
providers: [
{
provide: 'serverURL',
useValue: serverUrl
},
{
provide: 'SERVER_CONFIG',
useValue: config || {}
},
{
provide: 'SERVER_AUTH',
useValue: {signedIn: !!res.locals.auth}
},
]
});
});
}
app.module.ts
@NgModule({
bootstrap: [
AppComponent
],
imports: [
BrowserModule.withServerTransition({appId: 'myapp'}),
BrowserAnimationsModule,
SharedModule,
TransferHttpCacheModule,
BrowserTransferStateModule,
RouterModule.forRoot(
...
app.server.module.ts
@NgModule({
imports: [
// The AppServerModule should import your AppModule followed
// by the ServerModule from @angular/platform-server.
AppModule,
ServerModule,
ServerTransferStateModule,
ModuleMapLoaderModule // <-- *Important* to have lazy-loaded routes work
],
// Since the bootstrapped component is not inherited from your
// imported AppModule, it needs to be repeated here.
bootstrap: [AppComponent],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: ServerSideRequestInterceptor,
multi: true,
},
]
})
export class AppServerModule {}
The ServerSideRequestInterceptor
reads the serverURL
injection token, and prepends it to all requests starting with /
.
I feel like I am very close to cracking this, but I cannot for the life of me tell how to make it wait for the http calls in my app.
EDIT: Here's the Repo I made with working universal and angular 7: https://github.com/swimmadude66/AngularPWASeed/tree/universal7
Thanks to @CaerusKaru for solving this one in the @nguniversal issues page! https://github.com/angular/universal/issues/1046#issuecomment-455408250
Essentially, the problem was the interceptor
I use to append the full server path to api calls. It was using Object.assign(request, {url: fullServerUrl});
to set the new url for all http requests beginning with /
. The appropriate method is apparently request.clone({url: fullServerUrl});
and changing that one snippet caused the data to flow like a rushing stream.
Hope this helps someone else, and I will be turning the repro-repo (hard to say and type) in to boilerplate of Angular7 and Universal as a working starter point.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With