Angular Universal with APP_INITIALIZER
In the previous post I described how the APP_INITIALIZER
token in Angular could be used to delay the bootstrap process of the application so configuration could be loaded from a JSON file.
How can this be extended for Angular Universal?
Note: With Angular 17 and above, Angular Universal has been merged into Angular CLI. This means Angular Universal server-side rendering (SSR) and pre-rendering aka server-side generation (SSG) can be added to an existing Angular application with:
ng add @angular/ssr
Prior to Angular 17, (which is the way I've implemented it in my GitHub sample code), Angular Universal is added to an Angular application with:
ng add @nguniversal/express-engine
Regardless how Angular Universal is added, the core functionality and architecture is identical.
Adding Angular Universal brings in a few additional files to the application. These files are critical for the server-side bootstrap process, and are executed in a Node.js process. You can read a little more about it here.
Out of the box, Angular Universal will continue the usual compile-time configuration mode, so what do we need to do to change to runtime configuration?
The good news is that by default the plumbing is already done for us when the Angular Universal files are created, including the re-use of the AppModule
from Angular Universal's AppServerModule
:
import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {}
So there's very little we need to do! We can attempt to run the application in SSR mode with (pre-Angular 17):
npm run dev:ssr
and SSG mode with
npm run prerender
However, we'll have a problem. Our function for loading the configuration (remember the factory that gives us the APP_INITIALIZER
?) can't find or load the config.json
because is written to be executed in a browser. We need to modify it to run in a Node.js process.
So, back to the AppConfigService
class.
The first thing we need is a way to detect whether or not we're in a browser. There are several ways to do this, but I found the library 'browser-or-node' is perfect for my needs.
Once added, we go back to the AppConfigService
class:
import { isBrowser } from 'browser-or-node';
...
loadAppConfig(): Promise<void> {
if (isBrowser) {
return this.loadConfigForBrowser();
}
else {
return this.loadConfigForNode();
}
}
...
private loadConfigForNode(): Promise<void> {
let config = require('../../assets/config.json');
this.appConfig = config;
return Promise.resolve();
}
The critical difference is that client-side loading (loadConfigForBrowser()
) uses the 'fetch
' method - returning a Promise, whilst server-side loading (loadConfigForNode()
) uses 'require
' which simply loads the JSON object in the file. In order to maintain the return type I had to return a resolved Promise
.
Now when we run npm run ssr
or npm run prerender
we'll get the configuration loaded at runtime as intended on the server.
In order to illustrate this, I added a couple of trivial services such as the ClockService
and RandomImageService
.
When using SSG or SSR, you can view the content produced server-side using the browser's developer tools (F12 or Ctrl-I for Chrome or Edge).
Once the content is loaded, as per normal Angular Universal operation, the application is 'hydrated' and these additional services will begin to operate normally on the client-side.
And that's all for the APP_INITIALIZER
method. We'll look at the second technique for Angular runtime configuration in my next couple of posts.
Happy coding!