La platform universal a été ajouté à angular dans la version 4 d’angular. Elle permet maintenant de générer les pages depuis le serveur.
La méthode « magique » de la plateforme universal est le renderModuleFactory provenant de @angular/platfom-server
La définition de ce renderModulefactory dans la doc d’Angular est
1 2 3 4 5 6 | renderModuleFactory( moduleFactory: NgModuleFactory, options: PlatformOptions ): Promise |
moduleFactory : Le premier argument de cette méthode est moduleFactory, qui est pour faire court une version compilé d’angular. Ce moduleFactory doit être buildé une seule fois.
options : quant à lui contient les informations sur la manière dont on souhaite voir notre application: PlatformOptions
PlatformOptions : Le type de PlatformOptions est définit dans les sources par l’interface suivante
1 2 3 4 5 6 7 | interface PlatformOptions { document?: string; url?: string; extraProviders?: Provider[] } |
– le document indique le contenu du fichier HTML que l’on souhaite avoir dans notre app.
– l’urlL indique la page de notre application que l’on souhaite rendre
Installation
# on génere le projet avec un rooting de base
1 2 3 4 | ng new sn-universal --rooting cd sn-universal |
# on installe ensuite plateform server nécéssaire pour le rendu coté serveur et pour la génération des pages html
1 2 3 | npm install --save @angular/platform-server |
# il faudra mettre à jour angular et tous les packages liés vers la version 4 minimum et tous les autres packages tel que core-js, rxjs, zone.js ( sinon nous aurons une erreur au build)
Création du module serveur
Nous allons maintenant créer le fichier qui va boostraper (initialiser) l’app depuis le serveur: src/app.server.module.ts
On crée un nouveau fichier nommé app.server.module.ts. Il est assez similaire au fichier app.module.ts de notre application, la grosse différence est que ce module est crée pour le serveur
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; @NgModule({ imports: [ ServerModule, AppModule ], bootstrap: [AppComponent] }) export class AppServerModule {} |
Mise à jour du module principal
Nous aurons aussi besoin d’indiquer à l’AppModule que tout le rendu se fera depuis le serveur.
Normalement, on bootstrap une app Angular depuis un AppModule qui importe le BrowserModule (qui met à disposition toute sorte de services et directives tel que par exemple ngIf )
PS: c’est platformBrowserDynamic qui est la méthode utilisée pour bootstrapper l’application pour l’app.
Mise à jour de app module afin de pouvoir utiliser le serveur
1 2 3 4 5 | // dans /src/app/app.modules.ts // BrowserModule devient BrowserModule.withServerTransition({ appId : 'sn-universal' }) |
withServerTransition prend un appId et c’est lui qui est partagé entre le client et le serveur. withServerTransition permet à universal de remplacer le html généré par le sien.
A ce point on peut compiler notre app avec la commande ngc
1 2 3 | $ node_modules/.bin/ngc |
Si on ouvre le dossier dist, on remarquera que dans le dossier out-tsc/app se trouve dans les fichiers générés src/app.server.module.ngfactory.ts un fichier qui déclare et exporte un AppServerModuleNgFactory de type NgModuleFactory.
1 2 3 4 | export const AppServerModuleNgFactory:i0.NgModuleFactory = i0.?cmf(i1.AppServerModule, [i2.AppComponent],(_l:any) => { |
Création du module server de l’application sous express
Il nous faut créer un fichier main.server.ts qui va s’occuper de prendre une url en argument te de faire le rendu de la bonne page de notre application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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}!`); }); |
HS
La création du serveur sous express est hors sujet, on ne s’y attardera pas donc.
Mise à jour de de la config typescript
Dans le fichier de config /src/tsconfig.app.json on ajoutera server.ts dans les fichiers à exclure
1 2 3 4 5 6 7 | "exclude": [ "server.ts", // ICI "test.ts", "**/*.spec.ts" ] |
Ajouter ensuite dans tysconfig.json angularCompilerOptions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | { "compileOnSave": false, "compilerOptions": { ... ], "lib": [ ... ] }, "angularCompilerOptions": { // ICI "genDir": "./dist/ngfactory", "entryModule": "./src/app/app.module#AppModule" } } |
angularCompilerOptions : permet de spécifier une propriété genDir qui correspond au dossier dans lequel les fichiers ngfactory générés seront exportés.
entryModule : on lui indique le chemin vers le module principal sur le lequel l’app bootstrap ( dans ce cas : AppModule)
Mise à jour du Script NPM
dans le package.json
1 2 3 4 5 6 7 8 9 10 11 12 | "scripts": { "ng": "ng", "prestart": "ng build --prod && ngc", // on ajoute cette ligne "start": "ng serve", // on supprime celle ci "start": "ts-node src/server.ts" // et la remplace par celle ci "build": "ng build", // on supprime celle ci "test": "ng test", "lint": "ng lint", "e2e": "ng e2e" }, |
prestart lancera d’abord ce script avant le start (grace au pre ajouté avant) ng build –prod && ngc puis le server est lancé
et zoo
on lance l’app
1 2 3 | npm run start |
et si tout ce passe bien vous devriez avoir un quelque chose comme ca
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | Mac-mini-de-Samuel:sn-universal saminou$ npm run start > sn-universal@0.0.0 prestart /Users/saminou/SitesSaminou/angular/sn-universal > ng build --prod && ngc Date: 2017-09-08T15:09:38.658Z Hash: 6c329e8c267199f5dbe3 Time: 14528ms chunk {0} polyfills.c9b879328f3396b2bbe8.bundle.js (polyfills) 64.1 kB {4} [initial] [rendered] chunk {1} main.0f96096c1bb38373edb0.bundle.js (main) 5.15 kB {3} [initial] [rendered] chunk {2} styles.d41d8cd98f00b204e980.bundle.css (styles) 0 bytes {4} [initial] [rendered] chunk {3} vendor.3f618d3b9434d244b572.bundle.js (vendor) 307 kB [initial] [rendered] chunk {4} inline.d6e0fc0a02fc19c9d76f.bundle.js (inline) 1.45 kB [entry] [rendered] > sn-universal@0.0.0 start /Users/saminou/SitesSaminou/angular/sn-universal > ts-node src/server.ts listening on http://localhost:4000! |
Si vous visitez http://localhost:4000! , dans le code source de la page vous devriez avoir le code correspondant à ce que vous avez en visu plutot que l’habituelle « app works »
Générer un composant
Utiliser le cli pour générer un composant est assez facile avec les commandes fournies par le cli.
1 2 3 4 5 6 | ng g c home # équivalent de ng generate component home |
malheureusement avec nos modifications on aura une erreur du type
1 2 3 4 5 | > ng g c home Error: More than one module matches. Use skip-import option to skip importing the component into the closest module. More than one module matches. Use skip-import option to skip importing the component into the closest module. |
Pour éviter cela il suffit de spécifier le module en ajoutant un flag.
1 2 3 4 | ng g c home --module=app.module.ts ng g c about --module=app.modue.ts |
Définition des routes
Ouvrons maintenant le fichier routing app-routing.module.ts généré lors de la création de notre projet via le flag –routing
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import { NgModule } from '@angular/core'; import { Routes, RouterModule } from '@angular/router'; // on ajoute nos 2 nouveaux modules import { HomeComponent} from './home/home.component'; import { AboutComponent } from './about/about.component'; // on met à jour le tableau des routes const routes: Routes = [ { path: '', component: HomeComponent }, { path: 'about', component: AboutComponent } ]; @NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule] }) export class AppRoutingModule { } |
Mise à jour des templates
Ouvrons le ficher /src/app.component.html et mettons le à jour avec les liens
1 2 3 |
Ajout des titles et meta tags à nos components
C’est assez simple , il suffit d’importer Meta et Title depuis @angular/platform-browser
puis de mettre à jour le constructeur
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import { Meta, Title } from '@angular/platform-browser'; # dans le constructeur export class HomeComponent implements OnInit { // on utilise l'injection de dépendance pour créer des instances de Meta et Title constructor(meta: Meta, title: Title) { title.setTitle('Supa Karma Page'); meta.addTags([ { name: 'author', content:'Samsam'}, { name: 'keywords', content:'angular, seo, universal'}, { name: 'description', content:'tada une page avec un code source qui est à jour'}, ]) } ngOnInit() { } } |
Et voila
Les pièges à éviter
Universal ne peut utiliser les objets globaux (window, document, localstorage) existants au runtime du navigateur.
Le DOM doit être manipulé par Angular et non de façon externe (vanilla Js, jQuery : par exemple document.domMethod() ou encore $(‘dom-element’))..
Le server side rendering n’est pas compatible avec le lazy loading des modules.