Angular Universal ✨ "new" & improved
Last updated: February 21, 2020
Angular Universal is just a nickname for Angular SSR (server-side rendering). With Angular Universal (SSR) we are able to render our applications as HTML on the server (ie: Node.js), and return it to the browser!
This helps our Angular applications with SEO (search engine optimization), social-media previews, and even perceived performance. 🚤
In this article we're going to look at some of the great features of Angular Universal, and some new additions in the latest (v9 +)
release.
TL;DR
- Angular Universal is now easier than ever!
- Make sure your Angular application is already upgraded to the latest version
- Install the latest Universal schematics
ng add @nguniversal/express-engine
- If you're coming from an existing Universal schematic v8 (express-engine or hapi-engine) upgrade via "ng update" ie:
ng update @nguniversal/express-engine
- For a LIVE-reloading Node & browser dev-server:
ng run <app_name>:serve-ssr
- Automatically generate static prerendering (via guess-parser):
ng run <app_name>:prerender
Before we jump into everything, let's give a big shout-out to the Universal team & other amazing contributors for putting this one together. Including but not limited to:
🙏 Vikram Subramanian, Manfred Steyer, Alan Agius, Wagner Maciel, Adam Plumer, Minko Gechev
📣 Incase you missed the official announcement!
<script>My last set of tweets from the Angular team ♥️
— Vikram (@vikerman) January 4, 2020
Announcing Angular Universal 9.0 RC!!
- A pre-render builder in Angular Universal!
- Guesses static routes using guess-parser by @mgechev
(Built by our Eng resident Wagner Maciel)
cc @Tzmanics @mbaljeetsingh
(1/x) pic.twitter.com/zCuUJd2arG
What's new with Angular Universal?
The Angular Universal schematics has received some HUGE and long-awaited updates and improvements! Bringing us effortless static prerendering (with the help of Guess.js guess-parser predicting routes), and a vastly simplified & improved developer experience! Behind the scenes, the schematic also comes with new Angular "builders" (that do a lot of the heavy lifting), compiling your application in parallel, handling prerendering, and much more.
Angular Universal (server-side rendering) is now truly a 1st class citizen, having the developer experience and build automation that we all know and love from Angular itself.
Upgrading an existing (regular) Angular app
You can setup the latest Angular Universal schematics with an existing Angular CLI project, just make sure it has already been updated to the latest CLI (v9 +).
For help upgrading your Angular application itself from 8 to 9, please refer to https://update.angular.io/#8.0:9.0.
Upgrading an existing Angular Universal app
If you're coming from an existing Universal application that was generated with the v8 @nguniversal engine schematic (express-engine or hapi-engine). ( 🙏Alan Agius )
ng update
can now help you automatically upgrade to the latest schematic!
NOTE: During this update several backup files will be created, one of them for
server.ts
. If this file defers from the default one, you may need to copy some changes from the server.ts.bak to server.ts manually. These changes were needed to make the server.ts compatible with Ivy (ie: using the AppServer Module instead of NgFactory because it is no longer produced by default in Ivy)
Existing express-engine generated app:
$ ng update @nguniversal/express-engine
Existing hapi-engine generated app
$ ng update @nguniversal/hapi-engine
These updates will automatically:
- Update your existing project configuration and structure to be compatible with Ivy
- Remove the previous webpack config & @nguniversal/module-map-ngfactory-loader
- Add prerender & live-server "builder" configurations
TIP: Incase you see an error:
ERROR in The Angular Compiler requires TypeScript >=3.6.4 and <3.8.0 but 3.8.x was found instead.
Make sure you pin your TypeScript version to ~3.7.5, as Angular only supports 3.7.x at this moment.
Getting Started with Angular Universal ⚡️
NOTE: In this demo we're going to start off with a brand new (v9 +) Angular application scaffolded by the Angular CLI.
First, let's make sure we have the latest @angular/cli
installed.
$ npm i -g @angular/cli# ? Would you like to add Angular routing? (y/N) y
Now let's create a new Angular application (we are going to call our application angular-universal-v9
, but name yours whatever you prefer!)
$ ng new angular-universal-v9
The updated @nguniversal Schematics
Next, let's setup our application to utilize the latest Angular Universal schematics provided from @nguniversal/
, make sure to cd
into your new/existing applications root after running it.
ie: $ cd angular-universal-v9
$ ng add @nguniversal/express-engine@next
What's new with Angular Universal?
Let's open up the application in our favorite editor to take a look at what we have so far.
If you're coming from previous versions of the Angular Universal schematic (or it's all new to you), you'll notice that our project now has a few new important files, and some new additions to your package.json
scripts.
# Sample output:CREATE src/main.server.ts (298 bytes)CREATE src/app/app.server.module.ts (318 bytes)CREATE tsconfig.server.json (325 bytes)CREATE server.ts (1937 bytes)UPDATE package.json (1821 bytes)UPDATE angular.json (5205 bytes)UPDATE src/main.ts (432 bytes)UPDATE src/app/app.module.ts (438 bytes)UPDATE src/app/app-routing.module.ts (284 bytes)
Taking a look at our package.json
, we can see we now have a few new additional scripts / shortcuts to running some of the new features of the Angular Universal builders!
"scripts": { ... "dev:ssr": "ng run angular-universal-v9:serve-ssr", "serve:ssr": "node dist/angular-universal-v9/server/main.js", "build:ssr": "ng build --prod && ng run angular-universal-v9:server:production", "prerender": "ng run angular-universal-v9:prerender"}
"Live" Angular Universal Development
Let's fire up our first script just to see how much the developer experience has improved with the new Angular builders that will be handling all of the magical parellel compilation (behind the scenes).
$ npm run dev:ssr
This is now building both our Client AND Server bundles simultaneously via Angular builders. Kudos to the amazing effort by Manfred Steyer and others for making this happen!
** Angular Universal Live Development Server is listening on http://localhost:4200 **
Now fire up https://localhost:4200
as you would a normal Angular application, and take a look!
It may look like your standard Angular CLI generated application - but go ahead and "View Source"!
<!-- view-source:localhost:4200 --><!-- ... --><body> <app-root _nghost-sc20="" ng-version="9.0.0"> <div _ngcontent-sc20="" role="banner" class="toolbar"> <img _ngcontent-sc20="" width="40" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2v" /> <span _ngcontent-sc20="">Welcome</span> <!-- ... --> </div></app-root ></body>
We are getting a live-preview of our Angular Application server-side rendered! Go ahead and make some changes to app.component.html
(or any other file) and notice your terminal re-compiling both client&server bundles, and even live-reloading the webpage itself (via browsersync) once both have completed!
🥂 Amazing 🥂
A nice use-case for this is to actively test your entire application in real-time, letting you refresh different routes, ensuring that there aren't any errors with your application when being rendered from the server. (Take for example, you're using the global window
object in your code-base somewhere, you could spot the error before it gets to production and make the neccessary fixes.)
Angular Static Prerendering
Prerendering is now easier than ever thanks to the new schematics, and can be generated via one simple command!
$ ng run <app_name>:prerender
Or shorthand version in our package.json:
$ npm run prerender
Real-world Prerendering Demo 🧭
NOTE: Demo code can be found on GitHub here if you'd prefer to skip ahead!
For fun, let's setup a few quick routes & components so we can really take it for a test drive, and see what kind of options we have to handle more complex scenarios such as prerendering dynamic or parameter-based routes.
# example routes we're going to create & demo /home /products /products/:id (ie: /products/1124, /products/05919251, etc)
Let's create the home & products modules & routes real quick:
$ ng g m home --route home --module app$ ng g m products --route products --module app
Now let's add a quick dynamic component & route for specific /products/:id
pages now:
$ ng g c products/product --inline-template --inline-style --skip-tests
Next, let's update the newly created product.component.ts
component to bring in the dynamic routing param (for our little demo).
import { Component } from '@angular/core';import { ActivatedRoute } from '@angular/router';@Component({ selector: 'app-product', template: ` <p>Product ID = {{ productId }} // 👈</p> `, styles: [],})export class ProductComponent { public productId = this.route.snapshot.paramMap.get('id'); // 👈 constructor(private route: ActivatedRoute) {}}
Next, open up the products-routing.module.ts
to include this new dynamic component and param :id
that we're going to use.
import { NgModule } from '@angular/core';import { Routes, RouterModule } from '@angular/router';import { ProductsComponent } from './products.component';import { ProductComponent } from './product/product.component';const routes: Routes = [ { path: '', component: ProductsComponent }, { path: ':id', component: ProductComponent }, // 👈👈👈];@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule],})export class ProductsRoutingModule {}
Lastly, let's clean up our app.component.html
and make it super simple:
<h1>Universal (v9 +) Demo</h1><router-outlet></router-outlet>
Demo time
So let's fire up that prerendering script, and see what we get!
$ npm run prerender# Or:$ ng run angular-universal-v9:prerender
# sample outputPrerendering 2 route(s) to /angular-universal-v9/dist/angular-universal-v9/browserCREATE /angular-universal-v9/dist/angular-universal-v9/browser/home/index.html (27819 bytes)CREATE /angular-universal-v9/dist/angular-universal-v9/browser/products/index.html (27831 bytes)
It worked!! ✨✨✨
If you go to your /dist
folder and open up each index.html file inside /home/ and /products/ folders you should see the correct templates rendered inside, javascript bundles included, etc.
<!-- /dist/angular-universal-v9/browser/home/index.html --><p _ngcontent-sc21="">home works!</p><!-- /dist/angular-universal-v9/browser/products/index.html --><p _ngcontent-sc21="">products works!</p>
If you want to fire up a quick testing/demo server, you can utilize something like http-server
.
$ npm i -g http-server$ http-server ./dist/angular-universal-v9/browser
Should output:
Starting up http-server, serving ./dist/angular-universal-v9/browserAvailable on: http://127.0.0.1:8080 http://127.94.0.1:8080 http://127.94.0.2:8080 http://192.168.0.12:8080Hit CTRL-C to stop the server
Now you can test locally and view http://127.0.0.1:8080/home
or http://127.0.0.1:8080/products
!
But what about our dynamic / parameterized-routes like /products/:id
(that we have in our demo)?
It seems guess-parser is unable to be detect these routes automatically, but that makes sense. How could it know WHAT are -real- possible values here!
How can we handle these situations?
Prerendering Dynamic / Parameterized Routes
Luckily we have 2 new options for manually adding dynamic routes that can't be automatically detected via guess-parser. After all, we surely have a database full of IDs where these "products" get their data from, right?
One option during prerendering is that we can pass in the --routes
flag for each manual route in the terminal, - OR - pass in an entire .txt
file containing all of the additional routes needed.
TIP: Whichever method you use, you only need to include additional routes that aren't automatically detected by the guess-parser.
$ ng run <app_name>:prerender --routes '/products/1' --routes '/products/555'
Optimal Approach: The simpler approach would be to provide all of your required dynamic/parameter-based routes within the routes.txt
file, and utilize the --routesFile
flag.
Routes.txt file:
/products/1
/products/555
Now pass in the --routesFile
option.
$ ng run <app_name>:prerender --routesFile routes.txt
TIP: Ideally you'd want to create a script (ie: Node.js script) that hits an API/Database and gets all potential ID's (in our specific scenario), and outputs everything into a generated routes.txt automatically. Otherwise this would be quite the tedious and unmanageable task to handle manually!
Testing it in our Demo app:
If we created a routes.txt
file with those two ID's, (1, 555) we should get the sample output prerendered below!
Prerendering 4 route(s) to /angular-universal-v9/dist/angular-universal-v9/browserCREATE /angular-universal-v9/dist/angular-universal-v9/browser/products/index.html (1310 bytes)CREATE /angular-universal-v9/dist/angular-universal-v9/browser/products/1/index.html (1282 bytes)CREATE /angular-universal-v9/dist/angular-universal-v9/browser/products/555/index.html (1284 bytes)CREATE /angular-universal-v9/dist/angular-universal-v9/browser/home/index.html (1298 bytes)
We just prerendered routes with dynamic parameters!
Dynamic Server-side rendering
Sometimes our applications are constantly in a state of change, and we are unable to simply prerender pages. In this instance, we need to do standard dynamic SSR, utilizing a live Node.js server that receieves page requests, serializes our application, and returns the html in real-time.
For this we have two scripts in our package.json
, and that server.ts
file at the root of your project that you may have noticed!
// Build the client/server$ npm run build:ssr// Run the Node.js server$ npm run serve:ssr
These scripts will bundle up the client/server bundles, the Node.js express server (found in server.ts
), and then RUN the Node.js application at http://localhost:4000
so we can test it!
Fire up your browser to http://localhost:4000
and you can see everything in action, with View Source populated just like before.
The only difference is now these pages are being server-side rendered on-demand PER request!
In Conclusion
- Angular Universal is now easier than ever!
- Make sure your Angular application is already upgraded to the latest (v9 +)
- If you're coming from an existing Universal schematic v8 (express-engine or hapi-engine) upgrade via "ng update" ie:
ng update @nguniversal/express-engine
- Install the latest Universal schematics
ng add @nguniversal/express-engine
- For a LIVE-reloading Node & browser dev-server:
ng run <app_name>:serve-ssr
- Prerendering: (via guess-parser):
ng run <app_name>:prerender
- Dynamic SSR:
npm run build:ssr
🚀 What's next?
Follow the Author @MarkPieszak
Follow @Trilon_io to keep up to date for future articles on Angular Universal, Node.js, NestJS, and everything in-between when they're released!
Learn NestJS - Official NestJS Courses 📚
Level-up your NestJS and Node.js ecosystem skills in these incremental workshop-style courses, from the NestJS Creator himself, and help support the NestJS framework! 🐈🚀 The NestJS Fundamentals Course is now LIVE and 25% off for a limited time!
🎉 NEW - NestJS Course Extensions now live!
- NestJS Advanced Concepts Course now LIVE!
- NestJS Authentication / Authorization Course now LIVE!
- NestJS GraphQL Course (code-first & schema-first approaches) are now LIVE!
- NestJS Authentication / Authorization Course now LIVE!