Server Side rendering avec Angular universal 9

07/03/20 dannyEnglish Version

Qu’allons nous faire ?

Nous allons appliquer le Server Side Rendering dans notre Application Web.
Nous utiliserons pour cela le framework javascript Angular version 9.0.5

Le projet Angular de base que nous utiliserons dispose déjà des caractéristiques suivantes

  • Généré avec Angular CLI
  • Le Routing
  • Le Lazy Loading
  • Le framework CSS Bootstrap

Tous les sources utilisés sont indiqués en fin de tutoriel.

L' application finale est à l'adresse suivante 


Avant de commencer

Pour être visité par un grand nombre d'utilisateurs, un site web se doit de remplir deux conditions essentielles.

  • S'afficher le plus rapidement possible.
  • Etre bien référencé par les moteurs de recherche.

La technique qui permet de le faire porte un nom.

  • Le Rendu Côté Serveur ou Server Side Rendering en anglais.


Nous allons appliquer cette technique dans un projet Angular.
Pour cela nous emploierons la technologie préconisée par les équipes de Google 

  • Angular Universal.

Cette technologie permettra d'améliorer le référencement naturel ou SEO (Search Engine Optimization) de notre site.


Création du projet Angular

Pour pouvoir continuer ce tutoriel nous devons bien evidemment disposer de certains éléments

  • Node.js : La plateforme javascript
  • Git : Le logiciel de gestion de versions. 
  • Angular CLI : L'outil fourni par Angular.
  • Visual Studio code : Un éditeur de code.

Vous pouvez consulter le tutoriel suivant qui vous explique en détails comment faire


Nous allons utiliser un projet existant

# Créez un répertoire demo (le nom est ici arbitraire)
mkdir demo

# Allez dans ce répertoire
cd demo

# Récupérez le code source sur votre poste de travail
git clone https://github.com/ganatan/angular-example-features.git

# Allez dans le répertoire qui a été créé
cd angular-example-features

# Exécutez l'installation des dépendances (ou librairies)
npm install

# Exécutez le programme
npm run start

# Vérifiez son fonctionnement en lançant dans votre navigateur la commande
http://localhost:4200/

Théorie

Les pages web générées avec des framework javascript utilise javascript.
Les moteurs de recherche ont à l'heure actuelle du mal à interpréter le javascript.

Nous allons vérifier cette notion de façon pratique.

Nous allons éxécuter notre application avec le script correspondant.

# Exécution de l'application
npm run start

# Affichage du site dans le navigateur
http://localhost:4200/

Nous allons vérifier le code source produit dans la page correspondante.
En utilisant le navigateur Chrome il faut taper Ctrl + U pour voir le code html.

On remarque que le code "Features" qui s'affiche dans le navigateur n'apparait pas dans le code.

<!doctype html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>AngularStarter</title>
  <base href="/">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>

<body>
  <app-root></app-root>
  <script src="runtime.js" type="module"></script>
  <script src="polyfills.js" type="module"></script>
  <script src="styles.js" type="module"></script>
  <script src="scripts.js" defer></script>
  <script src="vendor.js" type="module"></script>
  <script src="main.js" type="module"></script>
</body>

</html>

En cliquant sur main.js nous ouvrons ce fichier qui contient  le texte "Features"

Exécutons une compilation avec npm run build.
Le répertoire dist/angular-starter contient les fichiers main-es5.js et main-es2015.js.
Ouvrons ces fichiers avec notre éditeur VS code , puis effectuons une recherche (Ctrl + F) du texte "Features".
Ils contiennent le texte "Features".

Ces fichiers permettent de générer le fichier main.js.
Le fichier main.js est un fichier javascript, il sera donc mal interprété par les moteurs de recherche.

Nous verrons plus loin dans ce tutoriel qu'une fois le SSR appliqué le code apparait directement dans le code HTML et sera ainsi bien interprété par les moteurs de recherche.


Installation

L'outil que nous utiliserons pour appliquer le SSR à notre projet est

  • Angular universal version 9.0.1

La dernière version de cet outil est disponible ci-dessous


Angular Universal permet de générer des pages statiques via un processus appelé Server side rendering (SSR).

La procédure à suivre est détaillée sur le site officiel d'Angular.
https://angular.io/guide/universal

Nous allons utiliser une simple commande CLI

# Installation
ng add @nguniversal/express-engine

Angular universal

Pour rappel angular CLI utilise via la directive ng add le principe des schematics pour modifier notre code et l'adapter à la nouvelle fonctionnalité (ici le ssr).

De nombreuses opérations ont été effectuées automatiquement sur notre projet.

Si nous avions dû réaliser cette opération manuellement voici les différentes étapes que nous aurions dû suivre.

  • Installation des nouvelles dépendances nécessaires
  • Modification du fichier main.ts
  • Modification du fichier app.module.ts
  • Modification du fichier angular.json
  • Création du fichier src/app/app.server.module.ts
  • Création du fichier src/main.server.ts
  • Création du fichier server.ts
  • Création du fichier tsconfig.server.json
  • Modification du fichier angular.json
  • Modification du fichier package.json

Installation des dépendances.


# Installer les nouvelles dépendances dans package.json
npm install --save @angular/platform-server
npm install --save @nguniversal/express-engine
npm install --save express
npm install --save @nguniversal/builders
npm install --save @types/express
src/main.ts
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

document.addEventListener('DOMContentLoaded', () => {
  platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));
});

Modification du fichier app.module.ts
Dans ce tutoriel nous allons rajouter la valeur appId pour identifier l'application.

src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'angular-starter' }),
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Modification du fichier angular.json.
Modification de outputPath

  • dist/angular-starter/browser à la place de dist/angular-starter.
angular.json
"builder": "@angular-devkit/build-angular:browser",
"options": {
  "outputPath": "dist/angular-starter/browser",
  "index": "src/index.html",
  "main": "src/main.ts",
  "polyfills": "src/polyfills.ts",
  "tsConfig": "tsconfig.app.json",
  "aot": true,
  "assets": ["src/favicon.ico", "src/assets"],
  "styles": [
    "src/styles.css",
    "node_modules/@fortawesome/fontawesome-free/css/all.min.css",
    "node_modules/bootstrap/dist/css/bootstrap.min.css",
    "src/assets/params/css/index.css"
  ],
  "scripts": [
    "node_modules/jquery/dist/jquery.min.js",
    "node_modules/bootstrap/dist/js/bootstrap.min.js",
    "src/assets/params/js/index.js"
  ]
},

Création du fichier app.server.module.ts

src/app/app.server.module.ts
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 {}

Création du fichier main.server.ts

src/main.server.ts
import { enableProdMode } from '@angular/core';

import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

export { AppServerModule } from './app/app.server.module';
export { renderModule, renderModuleFactory } from '@angular/platform-server';

Création du fichier server.ts

Le port utilisé par défaut est 4000 nous pouvons le changer si nécessaire dans ce fichier.

server.ts
import 'zone.js/dist/zone-node';

import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';

import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// The Express app is exported so that it can be used by serverless Functions.
export function app() {
  const server = express();
  const distFolder = join(process.cwd(), 'dist/angular-starter/browser');
  const indexHtml = existsSync(join(distFolder, 'index.original.html')) ? 'index.original.html' : 'index';

  // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
  server.engine('html', ngExpressEngine({
    bootstrap: AppServerModule,
  }));

  server.set('view engine', 'html');
  server.set('views', distFolder);

  // Example Express Rest API endpoints
  // app.get('/api/**', (req, res) => { });
  // Serve static files from /browser
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // All regular routes use the Universal engine
  server.get('*', (req, res) => {
    res.render(indexHtml, { req, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] });
  });

  return server;
}

function run() {
  const port = process.env.PORT || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

// Webpack will replace 'require' with '__webpack_require__'
// '__non_webpack_require__' is a proxy to Node 'require'
// The below code is to ensure that the server is run only when not requiring the bundle.
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  run();
}

export * from './src/main.server';

Création du fichier

  • tsconfig.server.json
tsconfig.server.json
{
  "extends": "./tsconfig.app.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app-server",
    "module": "commonjs",
    "types": [
      "node"
    ]
  },
  "files": [
    "src/main.server.ts",
    "server.ts"
  ],
  "angularCompilerOptions": {
    "entryModule": "./src/app/app.server.module#AppServerModule"
  }
}

Modifications du fichier angular.json

Les propriétés "server", "serve-ssr" et "prerender" sont rajoutées après la propriété e2e.

angular.json
"e2e": {
    "builder": "@angular-devkit/build-angular:protractor",
    "options": {
      "protractorConfig": "e2e/protractor.conf.js",
      "devServerTarget": "angular-starter:serve"
    },
    "configurations": {
      "production": {
        "devServerTarget": "angular-starter:serve:production"
      }
    }
  },
  "server": {
    "builder": "@angular-devkit/build-angular:server",
    "options": {
      "outputPath": "dist/angular-starter/server",
      "main": "server.ts",
      "tsConfig": "tsconfig.server.json"
    },
    "configurations": {
      "production": {
        "outputHashing": "media",
        "fileReplacements": [
          {
            "replace": "src/environments/environment.ts",
            "with": "src/environments/environment.prod.ts"
          }
        ],
        "sourceMap": false,
        "optimization": true
      }
    }
  },
  "serve-ssr": {
    "builder": "@nguniversal/builders:ssr-dev-server",
    "options": {
      "browserTarget": "angular-starter:build",
      "serverTarget": "angular-starter:server"
    },
    "configurations": {
      "production": {
        "browserTarget": "angular-starter:build:production",
        "serverTarget": "angular-starter:server:production"
      }
    }
  },
  "prerender": {
    "builder": "@nguniversal/builders:prerender",
    "options": {
      "browserTarget": "angular-starter:build:production",
      "serverTarget": "angular-starter:server:production",
      "routes": [
        "/"
      ]
    },
    "configurations": {
      "production": {}
    }
  }

Modification du fichier package.json

package.json
"scripts": {
    ...
  "dev:ssr": "ng run angular-starter:serve-ssr",
  "serve:ssr": "node dist/angular-starter/server/main.js",
  "build:ssr": "ng build --prod && ng run angular-starter:server:production",
  "prerender": "ng run angular-starter:prerender"
    ...
}

Mise à jour

Nous pouvons en profiter pour mettre à jour les dépendances du fichier package.json et adapter les descripteurs de version.

Les dépendances suivantes

  • @nguniversal/express-engine

Peuvent être mises à jour avec les versions

  • 9.0.1

Le fichier contiendra au final les dépendances suivantes.

  "dependencies": {
    "@angular/animations": "9.0.5",
    "@angular/common": "9.0.5",
    "@angular/compiler": "9.0.5",
    "@angular/core": "9.0.5",
    "@angular/forms": "9.0.5",
    "@angular/platform-browser": "9.0.5",
    "@angular/platform-browser-dynamic": "9.0.5",
    "@angular/platform-server": "9.0.5",
    "@angular/router": "9.0.5",
    "@fortawesome/fontawesome-free": "5.12.1",
    "@nguniversal/express-engine": "9.0.1",
    "bootstrap": "4.4.1",
    "chart.js": "2.9.3",
    "express": "4.17.1",
    "jquery": "3.4.1",
    "ng2-charts": "2.3.0",
    "rxjs": "6.5.4",
    "tslib": "1.11.1",
    "zone.js": "0.10.2"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.900.5",
    "@angular/cli": "9.0.5",
    "@angular/compiler-cli": "9.0.5",
    "@angular/language-service": "9.0.5",
    "@nguniversal/builders": "9.0.1",
    "@types/express": "4.17.3",
    "@types/node": "13.9.0",
    "@types/jasmine": "3.5.8",
    "@types/jasminewd2": "2.0.8",
    "codelyzer": "5.2.1",
    "jasmine-core": "3.5.0",
    "jasmine-spec-reporter": "4.2.1",
    "karma": "4.4.1",
    "karma-chrome-launcher": "3.1.0",
    "karma-coverage-istanbul-reporter": "2.1.1",
    "karma-jasmine": "3.1.1",
    "karma-jasmine-html-reporter": "1.5.2",
    "protractor": "5.4.3",
    "ts-node": "8.6.2",
    "tslint": "6.0.0",
    "typescript": "3.7.5"
  }

Conclusion

Il ne reste plus qu'à tester tous les scripts précédents et finaliser par le SSR.

# Développement
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test
npm run e2e

# AOT Compilation
npm run build

# SSR Compilation
npm run build:ssr
npm run serve:ssr
http://localhost:4000/

Enfin nous allons vérifier le code source produit dans la page correspondante à la compilation SSR..
En utilsant le navigateur Chrome il faut taper Ctrl + U pour voir le code html.

On remarque que le code "Features" s'affiche cette fois dans le navigateur.
La page sera dès lors bien interprétée par les moteurs de recherche.