Angular Universal SSR(Server-Side Rendering)
Gopalkrishna Hegde
Application Development Associate Manager @ Accenture | Angular, Cloud, .NET
Angular Universal SSR (Server-Side Rendering)
?
What is Angular Universal?
Angular Universal is a pre-rendering solution for Angular to be short cut.
In a normal Angular/Single Page application we bring data to the client and build HTML and render it on the browser.
But in some situations, we may need to render the data on the server ahead of time and then send the rendered HTML to the client. This is achieved by Angular Universal. It allows server-side rendering of the HTML.
?
Implementing the Angular Universal step by step using real life example.
Sure! Let's modify the example to use standalone components and lazy loading of routes. We'll also ensure the SSR configuration remains the same, using a configuration file to define the SSR routes.
?Step-by-Step Implementation
? Here example is shown for Standalone components. If you are using Angular version prior to 14.0 you should import the components in the app.modules.ts and in the route.ts you have to use loadchildren instead of loadcomponent for lazyloading.
1. Create a New Angular Project
ng new universalExample --routing
cd universalExample
Add Angular Universal to the Project
Add Angular Universal to your project using the Angular CLI:
ng add @nguniversal/express-engine
This command adds the necessary files and dependencies for Angular Universal, including server-specific files like server.ts.
Let us create an array of routes for the pages that should be server-side rendered (SSR). We will also set up a configuration file to define these routes, making it easy to manage in a real-life scenario with many pages.
?
1. Create Configuration File for SSR Routes
?
First, create a configuration file to define the SSR routes.
?
src/assets/config.json
?
```json
{
? "ssrRoutes": ["/", "/about", "/login", "/register"]
}
```
?
?2. Implement Standalone Components
?
Let's create standalone components for Home, About, Login, Register, and Dashboard.
?
** Run the following CLI commands to create the components
?
ng generate component home --standalone
ng generate component about --standalone
ng generate component login --standalone
ng generate component register --standalone
ng generate component dashboard --standalone
```
?
3. Implement the Components
?
Here are simple implementations for each standalone component:
?
src/app/home/home.component.ts
?
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
?
@Component({
? selector: 'app-home',
? standalone: true,
? imports: [CommonModule, RouterModule],
? templateUrl: './home.component.html',
? styleUrls: ['./home.component.css']
})
export class HomeComponent {}
```
?
src/app/home/home.component.html
?
```html
<h2>Home</h2>
<p>Welcome to the home page. This page is server-side rendered.</p>
```
?
src/app/about/about.component.ts
?
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
?
@Component({
? selector: 'app-about',
? standalone: true,
? imports: [CommonModule, RouterModule],
? templateUrl: './about.component.html',
? styleUrls: ['./about.component.css']
})
export class AboutComponent {}
```
?
src/app/about/about.component.html
?
```html
<h2>About</h2>
<p>This is the about page. This page is server-side rendered.</p>
```
?
src/app/login/login.component.ts
?
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Router } from '@angular/router';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
?
@Component({
? selector: 'app-login',
? standalone: true,
? imports: [CommonModule, RouterModule, ReactiveFormsModule],
? templateUrl: './login.component.html',
? styleUrls: ['./login.component.css']
})
export class LoginComponent {
? loginForm: FormGroup;
?
? constructor(private fb: FormBuilder, private router: Router) {
??? this.loginForm = this.fb.group({
????? username: ['', [Validators.required]],
????? password: ['', [Validators.required]]
??? });
? }
?
? onSubmit(): void {
??? if (this.loginForm.valid) {
????? // Simulate successful login
????? this.router.navigate(['/dashboard']);
??? }
? }
}
```
?
src/app/login/login.component.html
?
```html
<h2>Login</h2>
<form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
? <div>
??? <label for="username">Username</label>
??? <input id="username" formControlName="username" />
??? <div *ngIf="loginForm.get('username').invalid && loginForm.get('username').touched">
????? Username is required.
??? </div>
? </div>
? <div>
??? <label for="password">Password</label>
??? <input id="password" type="password" formControlName="password" />
??? <div *ngIf="loginForm.get('password').invalid && loginForm.get('password').touched">
????? Password is required.
??? </div>
? </div>
? <button type="submit" [disabled]="loginForm.invalid">Login</button>
</form>
```
?
src/app/register/register.component.ts
?
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, FormGroup, Validators } from '@angular/forms';
?
@Component({
? selector: 'app-register',
? standalone: true,
? imports: [CommonModule, ReactiveFormsModule],
? templateUrl: './register.component.html',
? styleUrls: ['./register.component.css']
})
export class RegisterComponent {
? registerForm: FormGroup;
?
? constructor(private fb: FormBuilder) {
??? this.registerForm = this.fb.group({
????? name: ['', [Validators.required, Validators.minLength(3)]],
????? email: ['', [Validators.required, Validators.email]],
????? password: ['', [Validators.required, Validators.minLength(6)]]
??? });
? }
?
? onSubmit(): void {
??? if (this.registerForm.valid) {
????? console.log('Form Submitted', this.registerForm.value);
??? }
? }
}
```
?
src/app/register/register.component.html
?
```html
<h2>Register</h2>
<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
? <div>
??? <label for="name">Name</label>
??? <input id="name" formControlName="name" />
??? <div *ngIf="registerForm.get('name').invalid && registerForm.get('name').touched">
????? Name is required and must be at least 3 characters long.
??? </div>
? </div>
? <div>
??? <label for="email">Email</label>
??? <input id="email" formControlName="email" />
??? <div *ngIf="registerForm.get('email').invalid && registerForm.get('email').touched">
领英推荐
????? Valid email is required.
??? </div>
? </div>
? <div>
??? <label for="password">Password</label>
??? <input id="password" type="password" formControlName="password" />
??? <div *ngIf="registerForm.get('password').invalid && registerForm.get('password').touched">
????? Password is required and must be at least 6 characters long.
??? </div>
? </div>
? <button type="submit" [disabled]="registerForm.invalid">Register</button>
</form>
```
?
src/app/dashboard/dashboard.component.ts
?
```typescript
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
?
@Component({
? selector: 'app-dashboard',
? standalone: true,
? imports: [CommonModule],
? templateUrl: './dashboard.component.html',
? styleUrls: ['./dashboard.component.css']
})
export class DashboardComponent {}
```
?
src/app/dashboard/dashboard.component.html
?
```html
<h2>Dashboard</h2>
<p>This is the dashboard page. This page is client-side rendered.</p>
```
?
#### 4. Update Routing for Lazy Loading
?
Update the routing module to use lazy loading for the standalone components.
?
src/app/app-routing.module.ts
?
```typescript
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
?
const routes: Routes = [
? { path: '', loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) },
? { path: 'about', loadComponent: () => import('./about/about.component').then(m => m.AboutComponent) },
? { path: 'login', loadComponent: () => import('./login/login.component').then(m => m.LoginComponent) },
? { path: 'register', loadComponent: () => import('./register/register.component').then(m => m.RegisterComponent) },
? { path: 'dashboard', loadComponent: () => import('./dashboard/dashboard.component').then(m => m.DashboardComponent) },
];
?
@NgModule({
? imports: [RouterModule.forRoot(routes)],
? exports: [RouterModule]
})
export class AppRoutingModule { }
```
?
src/app/app.component.html
?
```html
<nav>
? <a routerLink="/">Home</a>
? <a routerLink="/about">About</a>
? <a routerLink="/login">Login</a>
? <a routerLink="/register">Register</a>
? <a routerLink="/dashboard">Dashboard</a>
</nav>
<router-outlet></router-outlet>
```
?
#### 5. Update the App Module
?
Ensure the app module is set up correctly to support the standalone components and routing.
?
src/app/app.module.ts
?
```typescript
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
?
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
?
@NgModule({
? declarations: [
??? AppComponent
? ],
? imports: [
??? BrowserModule.withServerTransition({ appId: 'serverApp' }),
??? AppRoutingModule
? ],
? providers: [],
? bootstrap: [AppComponent]
})
export class AppModule { }
```
?
#### 6. Load SSR Routes Configuration in the Server File
?
server.ts
?
```typescript
import 'zone.js/node';
?
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { readFileSync } from 'fs';
?
import { AppServerModule } from './src/main.server';
?
// Load SSR routes from configuration file
const config = JSON.parse(readFileSync(join(process.cwd(), 'src/assets/config.json'), 'utf8'));
const ssrRoutes = config.ssrRoutes;
?
// The Express app is exported so that it can be used by serverless Functions.
export function app(): express.Express {
? const server = express();
? const distFolder = join(process.cwd(), 'dist/universalExample/browser');
? const indexHtml
?
?= 'index.html';
?
? // Our Universal express-engine (found @ https://github.com/angular/universal/tree/main/modules/express-engine)
? server.engine('html', ngExpressEngine({
??? bootstrap: AppServerModule,
? }));
?
? server.set('view engine', 'html');
? server.set('views', distFolder);
?
? // Serve static files from /browser
? server.get('*.*', express.static(distFolder, {
??? maxAge: '1y'
? }));
?
? // SSR for routes in the configuration file
? server.get(ssrRoutes, (req, res) => {
??? res.render(indexHtml, { req });
? });
?
? // CSR for other routes
? server.get('*', (req, res) => {
??? res.sendFile(join(distFolder, indexHtml));
? });
?
? return server;
}
?
function run(): void {
? const port = process.env.PORT || 4000;
?
? // Start up the Node server
? const server = app();
? server.listen(port, () => {
??? console.log(`Node Express server listening on https://localhost:${port}`);
? });
}
?
declare const __non_webpack_require__: NodeRequire;
?
if (require.main === module) {
? run();
}
?
export * from './src/main.server';
```
?
#### 7. Build and Serve the Application
?
Build the application for both server and browser:
?
```bash
npm run build:ssr
npm run serve:ssr
```
?
### Running the Application
?
Navigate to https://localhost:4000 in your browser. You should see:
?
- The Home, About, Login, and Register pages rendered server-side (SSR).
Right click on each of these pages and click View Page Source. See the tag<app-root>. You can see the rendered html. This means html is rendered already on the server side.
- The Dashboard page rendered client-side (CSR).
?Right click on the page and select View Page Source. See the tag<app-root>. It will be empty. Means html will be rendered on the client side.
### Summary
?
In this guide, we configured an Angular Universal application to use standalone components and lazy loading of routes. We also used a configuration file to define the SSR routes. This involved:
?
1)??????? Create new Angular Project.
2)??????? Install Angular Universal
3)??????? Creating standalone components for the application
4)??????? Setting up lazy loading for the routes.
5)??????? Creating a configuration file to define SSR routes.
6)??????? Configuring the server to load SSR routes from the configuration file.
7)??????? Building and serving the application with SSR.
?
By configuring the server to render specific routes using Angular Universal and loading these routes from a configuration file, we can easily manage SSR for multiple pages.