Comme tu le sais peut-être, la team d'Angular vient d'intégrer Universal dans sa version 4 du framework. Ce module qui permet un rendu côté serveur est un vrai plus pour le SEO.

Avant de commencer, tu ne sais peut-être pas qu'elle est l'intérêt d'ajouter à ton application Angular un rendu côté serveur (Server Side Rendering ou SSR).

Tu l'as peut-être déjà remarqué : les robots ont un peu du mal à lire du Javascript, et encore plus s'il s'agit de charger des données dynamiquement (via des appels d'API par exemple). Le robot, lui, quand il va sur ton site Angular il ne voit que le contenu de ton index.html, c'est-à-dire un header, un body avec ton composant racine et puis basta. Le problème c'est que ton composant racine (conventionnellement appelé app-root) est chargé à la volée et le robot ne va pas le lire. Il se retrouve donc avec un <app-root></app-root>, sans aucun autre contenu à l'intérieur. 

Et ça peut poser problème, notamment si tu veux être indexé correctement sur les moteurs de recherche ou que ton trafic dépend essentiellement des réseaux sociaux. Après si tu te fiches des réseaux sociaux et que tu ne penses qu'à Google : ne t'inquiète pas, Google sait lire le Javascript et il attendra que l'intégralité de ta page soit chargé avant de l'indexer. J'ai testé et je confirme que ça fonctionne !

Mais si comme moi tu as besoin des réseaux sociaux pour te faire connaître, tu n'auras pas d'autres choix que de t'intéresser au module Universal d'Angular !

L'autre avantage aussi, c'est que ta page sera beaucoup plus rapide. Les modules essentiels à ta page seront chargés au préalable côté serveur. Et une fois l'app démarrée, le reste sera chargé sans que l'utilisateur ne se doute de rien. Angular a fait des tests que tu peux lire dans sa documentation : une app qui démarre sans aucune configuration met environ 1s, la version Universal met 0.04s.

Ce qui est requis

Déjà, tu as besoin d'installer Node.js sur ta machine. Je suppose aussi que tu as des connaissances en Angular. Pour plus de simplicité on utilisera Angular CLI. Si tu ne connais pas, je t'invite vraiment à t'y intéresser. Depuis que l'outil n'est plus en bêta, c'est devenu un standard dans le développement d'applications Angular. Donc, pour résumer :

Let's go

Commence par créer ton application grâce à l'outil Angular CLI avec le paramètre permettant de générer le fichier de configuration du routage (on est feignant nous !) :

ng new ng-ssr --routing

Va ensuite dans ton dossier ng-ssr et commence par installer le module @angular/platform-server, @angular/animationsexpress :

npm i -S @angular/platform-server @angular/animations express

Maintenant, tu vas éditer ton fichier app.module.ts sous src/app et tu modifies ton décorateur NgModule :

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule.withServerTransition({ appId: 'ng-ssr' }), // On change cette ligne
    FormsModule,
    HttpModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})

La configuration côté navigateur est prête désormais. Il faut maintenant que tu crées un fichier qui charge les modules côté serveur. Tu l'appelles 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: [
    ServerModule, // On charge le ServerModule
    AppModule     // On hérite des modules contenus dans l'AppModule
],
bootstrap: [AppComponent]
})
export class AppServerModule { }

Mettre en place le serveur Node.js

Maintenant, tu vas devoir mettre en place le serveur Node.js qui permettra le rendu de l'app qui sera balancée au client. Tu vas dans ton dossier src/ et tu ajoute un fichier server.ts. Tu peux recopier bêtement ce bout de code :

import 'reflect-metadata';
import 'zone.js/dist/zone-node';
import { platformServer, renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core';
import { AppServerModuleNgFactory } from '../dist/ngfactory/src/app/app.server.module.ngfactory';
import * as express from 'express';
import { readFileSync } from 'fs';
import { join } from 'path';

const PORT = 4000;

enableProdMode();

const app = express();

let template = readFileSync(join(__dirname, '..', 'dist', 'index.html')).toString();

app.engine('html', (_, options, callback) => {
  const opts = { document: template, url: options.req.url };

  renderModuleFactory(AppServerModuleNgFactory, opts)
    .then(html => callback(null, html));
});

app.set('view engine', 'html');
app.set('views', 'src');

app.get('*.*', express.static(join(__dirname, '..', 'dist')));

app.get('*', (req, res) => {
  res.render('index', { req });
});

app.listen(PORT, () => {
  console.log(`Listening on http://localhost:${PORT}...`);
});

C'est un fichier Node.js classique, qui intègre Express pour faciliter le routage. Ton IDE va peut-être râlé s'il ne reconnaît pas le module AppServerModuleNgFactory d'un fichier qui n'existe pas encore. C'est normal ! Ce fichier sera généré lors de la compilation de ton app. D'ailleurs, on va devoir modifier la configuration de TypeScript pour tout ça.

Configuration de TypeScript

Commence par modifier ton fichier src/tsconfig.app.json en excluant server.ts :

{
  "extends": "../tsconfig.json",
  "compilerOptions": {
    "outDir": "../out-tsc/app",
    "module": "es2015",
    "baseUrl": "",
    "types": []
  },
  "exclude": [
    "server.ts", # Là !
    "test.ts",
    "**/*.spec.ts"
  ]
}

Maintenant, ouvre ton fichier tsconfig.json (à la racine) et ajoute les options (angularCompilerOptions) pour la compilation de ton application Angular :

{
  "compileOnSave": false,
  "compilerOptions": {
    "outDir": "./dist/out-tsc",
    "baseUrl": "src",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "target": "es5",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2016",
      "dom"
    ]
  },
  "angularCompilerOptions": { 
    "genDir": "./dist/ngfactory",
    "entryModule": "./src/app/app.module#AppModule"
  }  
}

Ton application sera maintenant compilée dans un répertoire dist/ à la racine. 

Maintenant, une petite configuration à rajouter dans ton package.json pour te rendre la vie plus facile et demander à NPM de taper les lignes de commande pour toi :

{
  "scripts": {
    "prestart": "ng build --prod && ngc",
    "start": "ts-node src/server.ts", 
    "ng": "ng",
    "start": "ng serve",
    "build": "ng build",
    "test": "ng test",
    "lint": "ng lint",
    "e2e": "ng e2e"
  },
}

Maintenant, tu lances un coups de :

npm start

Et hop ! Tu vas sur http://localhost:4000 et là, amazing ! Tu viens de démarrer ton app avec un rendu côté serveur. 

Alors, si c'est une app déjà existante, tu auras peut-être des problèmes avec certains modules (Angular Material par exemple), parce que Node.js ne connait pas les objets documentwindow, etc. Là, le mieux est de créer un fichier app.common.module.ts dont les autres fichiers de chargement de modules hériteront, de mettre les modules commun (serveur + client) et de mettre les modules problématiques dans app.module.ts uniquement.

Il y a pas mal d'autres astuces à découvrir, notamment l'utilisation de Webpack au lieu de ts-node pour démarrer l'app, mais j'en parlerai dans un autre article :-) !


Besoin d'un site internet, d'une application, de services de motion design ou de community management ?