Tutoriels Backend
Transfer State avec Angular
Le module httpclient associé au Serveur side rendering (SSR) génèrent deux requêtes lors d'un appel d'api.
Ce qui double la charge de travail sur un backend.
L’objectif de ce tutoriel est donc d’améliorer le SSR de notre application Angular.
Nous allons rajouter dans notre projet deux modules ServerTransferStateModule et BrowserTransferStateModule.
Nous utiliserons le framework javascript Angular .
Qu’allons nous faire ?
Remarque importante.
Cette fonctionnalité n'est plus utile sur Angular 15 l'équipe de google ayant résolu le dysfonctionnement.
Ce tutoriel n'est donc utile qu'avec Angular 14 ou des versions antérieures.
Il s'agit de l'étape de notre guide Angular qui nous permettra d'obtenir une Application Web de type PWA.
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
- Server Side Rendering
- HttpClient
Tous les sources créés sont indiqués en fin de tutoriel.
L' application est à l'adresse suivante
Vérification
Un appel d'api dans notre code lance à priori deux requêtes sur le serveur d'api.
Nous allons tout d'abord vérifier la théorie.
Le code que nous allons analyser se trouve dans le fichier items.component.ts
Pour cela nous allons procéder à quelques modifications dans ce fichier.
import { Component, OnInit } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { ItemsService } from './items.service';
@Component({
selector: 'app-items',
templateUrl: './items.component.html',
styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit {
items: any;
loaded: boolean;
constructor(
private itemsService: ItemsService,
@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
this.loaded = false;
}
ngOnInit(): void {
this.getUsers();
}
getUsers(): void {
this.itemsService.getItems('https://jsonplaceholder.typicode.com/users')
.subscribe(
items => {
const platform = isPlatformBrowser(this.platformId) ?
'in the browser' : 'on the server';
console.log(`getUsers : Running ${platform} with appId=${this.appId}`);
this.loaded = true;
this.items = items;
});
}
resetUsers(): void {
this.items = null;
this.loaded = true;
}
}
Nous allons maintenant vérifier cette théorie.
Cas n° 1 (sans SSR)
- npm run start
- Lancer Chrome
- Activer les outils de développement avec Ctrl + Maj + J
- Lancer http://localhost:4200/httpclient
- dans la console de chrome vérifier un appel de requête dans le browser
- getUsers : Running in the browser with appId=angular-starter
Cas n° 2 (avec SSR)
- npm run build:ssr
- npm run serve:ssr
- Lancer Chrome
- Activer les outils de développement avec Ctrl + Maj + J
- Lancer http://localhost:4000/httpclient
- dans la console de chrome vérifier un appel de requête dans le browser
- getUsers : Running in the browser with appId=angular-starter
- Dans la console de lancement du prompt vérifier un appel de requête dans le server
- Node server listening on http://localhost:4000
- getUsers : Running on the server with appId=angular-starter
Modification
La solution consiste à utiliser deux modules d'angular.
ServerTransferStateModule et BrowserTransferStateModule.
Pour cela il nous faut modifier quelques uns de nos fichiers.
Les étapes sont les suivantes.
- Modifier main.ts
- Créer src/app/app.browser.module.ts
- Modifier app.server.module.ts
- Modifier app.module.ts (rajout de CommonModule)
- Modifier items.component.ts
- Modifier items.component.spec.ts
- Modifier tslint.json
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppBrowserModule } from './app/app.browser.module';
import { environment } from './environments/environment';
if (environment.production) {
enableProdMode();
}
function bootstrap() {
platformBrowserDynamic().bootstrapModule(AppBrowserModule)
.catch(err => console.error(err));
};
if (document.readyState === 'complete') {
bootstrap();
} else {
document.addEventListener('DOMContentLoaded', bootstrap);
}
import { NgModule } from '@angular/core';
import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
BrowserModule.withServerTransition({ appId: 'angular-starter' }),
BrowserTransferStateModule
],
bootstrap: [AppComponent],
})
export class AppBrowserModule { }
import { NgModule } from '@angular/core';
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server';
import { BrowserModule } from '@angular/platform-browser';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
@NgModule({
imports: [
AppModule,
ServerModule,
ServerTransferStateModule,
BrowserModule.withServerTransition({ appId: 'angular-starter' }),
],
bootstrap: [AppComponent],
})
export class AppServerModule { }
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';
import { HttpClientModule } from '@angular/common/http';
@NgModule({
declarations: [
AppComponent,
HomeComponent,
NotFoundComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
HttpClientModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
import { Component, OnInit } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { TransferState, makeStateKey } from '@angular/platform-browser';
import { PLATFORM_ID, APP_ID, Inject } from '@angular/core';
import { ItemsService } from './items.service';
const STATE_KEY_ITEMS = makeStateKey('items');
@Component({
selector: 'app-items',
templateUrl: './items.component.html',
styleUrls: ['./items.component.css']
})
export class ItemsComponent implements OnInit {
// items: any;
items: any = [];
loaded: boolean;
constructor(
private state: TransferState,
private itemsService: ItemsService,
@Inject(PLATFORM_ID) private platformId: object,
@Inject(APP_ID) private appId: string) {
this.loaded = false;
}
ngOnInit(): void {
this.getUsers();
}
getUsers(): void {
this.loaded = false;
this.items = this.state.get(STATE_KEY_ITEMS, <any> []);
if (this.items.length === 0) {
this.itemsService.getItems('https://jsonplaceholder.typicode.com/users')
.subscribe(
items => {
const platform = isPlatformBrowser(this.platformId) ?
'in the browser' : 'on the server';
console.log(`getUsers : Running ${platform} with appId=${this.appId}`);
this.items = items;
this.loaded = true;
this.state.set(STATE_KEY_ITEMS, <any> items);
});
} else {
this.loaded = true;
}
}
resetUsers(): void {
this.items = null;
this.loaded = true;
}
}
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientModule } from '@angular/common/http';
import { ItemsComponent } from './items.component';
import { BrowserTransferStateModule } from '@angular/platform-browser';
describe('ItemsComponent', () => {
let component: ItemsComponent;
let fixture: ComponentFixture<ItemsComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
HttpClientModule,
BrowserTransferStateModule
],
declarations: [ItemsComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(ItemsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
Conclusion
Effectuer le test précédent pour constater qu'il n'y a plus qu'un appel d'api sur le serveur.
- npm run build:ssr
- npm run server:ssr
- localhost:4000/httpclient
Il ne reste plus qu'à tester les différents scripts Angular pour finaliser l'application.
# 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/
Code source
Le code source utilisé en début de tutoriel est disponible sur github
https://github.com/ganatan/angular-httpclient
Le code source obtenu à la fin de ce tutoriel est disponible sur github
https://github.com/ganatan/angular-transferstate
Les étapes suivantes vous permettront d'obtenir une application prototype
- Etape 1 : Démarrer avec Angular
- Etape 2 : Routing avec Angular
- Etape 3 : Lazy loading avec Angular
- Etape 4 : Bootstrap avec Angular
- Etape 5 : Modules avec Angular
- Etape 6 : Components avec Angular
- Etape 7 : Services avec Angular
- Etape 8 : Template Driven Forms avec Angular
- Etape 9 : Charts avec Angular
- Etape 10 : Server Side Rendering avec angular
- Etape 11 : HttpClient avec Angular
- Etape 12 : Transfer State avec Angular
- Etape 13 : Progressive Web App avec Angular
- Etape 14 : Search Engine Optimization avec Angular
La dernière étape permet d'obtenir un exemple d'application
Le code source de cette application finale est disponible sur GitHub
https://github.com/ganatan/angular-app