About my blog

I write about the technical and non-technical aspects of software development

How it works

Microsoft ASP.NETASP.Net
BlogEngine.NET BlogEngine.NET
Azure DevOpsAzure DevOps

Contact info

 Email
 Contact

Follow me

Prod-20240407.1

Angular Universal with APP_INITIALIZER

The APP_INITIALIZER token in Angular can also be used to delay the bootstrapping of the application for Angular Universal

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.

Angular Universal runtime configuration page screenshot

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!

 


You Might Also Like


Would you like to share your thoughts?

Your email address will not be published. Required fields are marked *

Comments are closed