Sekrab Garage

Angular SSR Update

Replacing Angular Universal with SSR version 17.0

Angular January 23, 24
Subscribe to Sekrab Parts newsletter.

Angular has adopted Universal and made it a kid of its own! Today I will rewrite some old code we used to isolated express server as we documented before with the most extreme version of serving the same build for multiple languages from Express, we are also using a standalone version as we covered recently.

Let’s dig.

Find GitHub branch of cricketere with server implementation

anchorSetup

We need to first install SSR package and remove any reference to Angular Universal. (Assuming we have upgraded to Angular 17.0)

npm install @angular/ssr

npm uninstall @nguniversal/common @nguniversal/express-engine @nguniversal/builders

Using ng add @angular/ssr rewrites the server.ts and it claims to add a config file somewhere. Yeah, nope!

In our last attempt for a skimmed down, standalone server.ts, it looked like this

// the last server.ts
import { ngExpressEngine } from '@nguniversal/express-engine';
import 'zone.js/dist/zone-node';
// ...
const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    importProvidersFrom(ServerModule),
    // providers: providers array...
  ],
});

// export the bare minimum, let nodejs take care of everything else
export const AppEngine = ngExpressEngine({
  bootstrap: _app
});

Now the ngExpressEngine is gone. Here are three sources to dig into to see if we can create our own skimmed down engine.

The original Universal Express Engine received options, created the return function, and used the CommonEngine to render. We can recreate a template engine with much less abstraction in our server.ts. Here is the outcome.

anchorChanges

  • First, remember to import zone.js directly instead of the deep path zone.js/dist/zone-node
  • The CommonEngine instance can be created directly with at least bootstrap property set
  • Export the express engine function
  • Get rid of a lot of abstractions
  • The URL property cannot be set in Angular, it better be set in Express route.
  • Use provideServerRendering, instead of importProvidersFrom

The new server.ts now looks like this:

// server.ts
// remove nguniversal references
// import { ngExpressEngine } from '@nguniversal/express-engine';

// change import from deep to shallow:
import 'zone.js';

// add this
import { CommonEngine, CommonEngineRenderOptions } from '@angular/ssr';

// the standalone bootstrapper 
const _app = () => bootstrapApplication(AppComponent, {
  providers: [
    // this new line from @angular/platform-server
    provideServerRendering(),
    // provide what we need for our multilingual build
    // ... providers array
  ],
});

// create engine from CommonEngine, pass the bootstrap property
const engine = new CommonEngine({ bootstrap: _app });

// custom express template angine, lets call it cr for cricket
export function crExpressEgine(
  filePath: string,
  options: object,
  callback: (err?: Error | null, html?: string) => void,
) {
  try {
    // grab the options passed in our Express server
    const renderOptions = { ...options } as CommonEngineRenderOptions;

    // set documentFilePath to the first arugment of render
    renderOptions.documentFilePath = filePath;

    // the options contain settings.view value
    // which is set by app.set('views', './client') in Express server
    // assign it to publicPath
    renderOptions.publicPath = (options as any).settings?.views;
    
    // then render
    engine
      .render(renderOptions)
      .then((html) => callback(null, html))
      .catch(callback);
  } catch (err) {
    callback(err);
  }
};

Our Express route code, which is where the main call for all URLs are caught and processed:

// routes.js
// this won't change (it's defined by outputPath of angular.json server build)
const ssr = require('./ng/main');

// app is an express app created earlier
module.exports = function (app, config) {
  // we change the engine to crExpressEngine
  app.engine('html', ssr.crExpressEgine);
  app.set('view engine', 'html');
  // here we set the views for publicPath
  app.set('views',  './client');

  app.get('*'), (req, res) => {
    const { protocol, originalUrl, headers } = req;
		
    // serve the main index file
    res.render(`client/index.html`, {
      // set the URL here
      url: `${protocol}://${headers.host}${originalUrl}`,
      // pass providers here, if any, for example "serverUrl"
      providers: [
        {
          provide: 'serverUrl',
          useValue: res.locals.serverUrl // something already saved
        }
      ],
      // we can also pass other options
      // document: use this to generate different DOM content
      // turn off inlinecriticalcss
      // inlineCriticalCss: false 
    });
  });
};

Built, Express server run, and tested. Works as expected.

A note about the URL, in a previous article we ran into an issue with reverse proxy, and had to set the the URL from a different source, as follows:

// fixing URL with reverse proxy
let proto = req.protocol;
if (req.headers && req.headers['x-forwarded-proto']) {
    // use this instead
    proto = req.headers['x-forwarded-proto'].toString();
}
// also, always use req.get('host')
const url = `${proto}://${req.get('host')}`;

This is better than the one documented in Angular.

anchorPassing request and response

We cannot provide req and res as we did before, we used to depend on REQUEST token from nguniversal library. But we don’t need to most of the time, we can inject the values we want from request directly into the express providers array. Here are a couple of examples: a json config file from the server, and the server URL:

// routes.js
//...
res.render(..., 
  // ... provide a config.json added in express
  providers: [
    {
      provide: 'localConfig',
      useValue: localConfig // some require('../config.json')
    },
    {
      provide: 'serverUrl',
      useValue: `${req.protocol}://${req.headers.host}`;
    }
  ]
});

Then when needed, simply inject directly

// some service in Angular
constructor(
  // inject from server
  @Optional() @Inject('localConfig') private localConfig: any,
  @Optional() @Inject('serverUrl') private serverUrl: string
)

If however we are using a standalone function that has no constructor, like the Http interceptor function, and we need to use inject, it’s a bit more troublesome. (Why Angular?!). There are a couple of ways.

anchorGetting from Injector

The first way is not documented, and it uses a function marked as deprecated, it has been marked for quite a while, but it is still being used under the hood. That’s how I found it, by tracing my steps back to the source. Injecting the Injector itself to get whatever is in it.

// in a standalone function like http interceptor function
export const ProjectHttpInterceptorFn: HttpInterceptorFn = 
(req: HttpRequest<any>, next: HttpHandlerFn) => {

  // Injector and inject from '@angular/core';
  // this is a depricated
  const serverUrl = inject(Injector).get('serverUrl', null);
  // use serverUrl for something like:
  let url = req.url;
  if (serverUrl) {
    url = `${serverUrl}/${req.url}`;
  }
  // ...
}

This is marked as deprecated.

anchorRe-creating injection tokens

Looking at how Angular Universal Express Engine provided request and response, we get a hint of how we should do it the proper way.

We will follow the same line of thought, but let’s get rid of the extra typing to keep it simple, to add Request, Response and our serverUrl. The steps we need to go through:

  • Curse Angular
  • Angular: define a new injection token
  • Angular consumer: inject optionally (dependency injection)
  • Angular server file: expose new attributes for the render function, that maps to a static providers
  • Angular server file: create the providers function from incoming attributes
  • Express: pass the values in the new exposed attributes.
  • Make peace with Angular.

In a new file, token.ts inside our app, we’ll define the injection tokens:

// app/token
// new tokens, we can import Request and Response from 'express' for better typing
export const SERVER_URL: InjectionToken<string> = new InjectionToken('description of token');

export const REQUEST: InjectionToken<any> = new InjectionToken('REQUEST Token');
export const RESPONSE: InjectionToken<any> = new InjectionToken('RESPONSE Token');

Then in a consumer, like the Http interceptor function, directly inject and use.

// http interceptor function
export const LocalInterceptorFn: HttpInterceptorFn = (req: HttpRequest<any>, next: HttpHandlerFn) => {
  // make it optional so that it doesn't break in browser
  const serverUrl = inject(SERVER_URL, {optional: true});
  const req = inject(REQUEST, {optional: true});
  //... use it
}

In our server.ts we create the provider body for our new optional tokens, and we append it to the list of providers of the options attribute. The value of these tokens, will be passed with renderOptions list. Like this:

// server.ts
// rewrite by passing the new options
export function crExpressEgine(
  //...
) {
  try {
    // we can extend the type to a new type here with extra attributes
    // but not today
    const renderOptions = { ...options } as CommonEngineRenderOptions;

    // add new providers for our tokens
    renderOptions.providers = [
      ...renderOptions.providers,
      {
        // our token
        provide: SERVER_URL,
        // new Express attribute
        useValue: renderOptions['serverUrlPath']
      },
      {
        provide: REQUEST,
        useValue: renderOptions['serverReq']
      },
      {
        provide: RESPONSE,
        useValue: renderOptions['serverRes']
      }
    ];
  // ... render
  }
};

Finally, in the Express route, we just pass the req and res and serverUrlpath in render

// routes.js
app.get('...', (req, res) => {
  res.render(`../index/index.${res.locals.lang}.url.html`, {
    // add req, and res directly
    serverReq: req,
    serverRes: res,
    serverUrlPath: res.locals.serverUrl
    // ...
  });
});
Note, I change the names of properties on purpose not to get myself confused about which refers to which, it’s a good habit.

Now we make peace. It’s unfortunate there isn’t an easier way to collect the providers by string! Angular? Why?

anchorMerging providers

In order not to repeat ourselves between the client and server apps, we need to merge the providers into one. Angular provides a function out of the box for that purpose: mergeApplicationConfig (Find it here). Here is where we create a new config file for the providers list:

// in a new app.config
// in client app, export the config:
export const appConfig: ApplicationConfig = {
  providers: [
    // pass client providers, like LOCAL_ID, Interceptors, routers...
    ...CoreProviders
  ]
}

// in browser main.ts
bootstrapApplication(AppComponent, appConfig);

// in server.ts
// create extra providers and merge
const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};

const _app = () => bootstrapApplication(AppComponent,
  mergeApplicationConfig(appConfig, serverConfig)
);

ApplicationConfig is nothing but a providers array, so I am not sure what the point is! I simply export the array, and expand it. Like this:

// app.config
// my preferred, less posh way
export const appProviders = [
  // ...
];

// main.ts
bootstrapApplication(AppComponent, {providers: appProviders});

// server.ts
const _app = () => bootstrapApplication(AppComponent,
  { providers: [...appProviders, provideServerRendering()] }
);

anchorProviding Http Cache

The transfer cache last time I checked was automatically setup and used. In this version, I did not get my API to work on the server. Something missing. Sleeves rolled up.

The withHttpTransferCacheOptions is a hydration feature. Hmmm!

anchorPartial Hydration

The new addition is partial hydration. Let’s add the provideClientHydration to the main bootstrapper: the app.config list of providers, that will be merged into server.ts (it must be added to browser as well.)

// app.config
export const appProviders = [
  // ...
  provideClientHydration(),
];

Building, running in server, and the Http request is cached. It is a GET request, and there are no extra headers sent. So the default settings are good enough. There was no need to add withHttpTransferCacheOptions. Great.

So this is it. I tried innerHTML manipulation and had no errors. I also updated Sekrab Garage website to use partial hydration and I can see the difference. The page transition from static HTML to client side was flickery. Now the hydration is smooth.

anchorPrerendering

The last bit to fix is our prerender builder. The source code of devkit is here.

The following lines are the core difference:

// these lines in the old nguniversal
const { ɵInlineCriticalCssProcessor: InlineCriticalCssProcessor } = await loadEsmModule<
    typeof import('@nguniversal/common/tools')
  >('@nguniversal/common/tools');

// new line in angular/rss
const { InlineCriticalCssProcessor } = await import(
  '../../utils/index-file/inline-critical-css'
);

Unfortunately we don’t have access to that file (inline-critical-css). It is never exposed. Are we stuck? May be. The other solution is to bring it home (I don’t like this). I am demoralized. I’ll have to let go of my Angular builder, and rely on my prerender routes via express server. Which still works, no changes needed except the render attributes, as shown above.

So there you go, thank you for reading through this, did you make peace with Angular today? 🔻