Transfer State avec Angular

Mis à jour : 08/09/2023 danny

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 ​​​​.

Transfer state avec 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.

src/app/modules/application/items/items.component.ts
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
src/main.ts
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);
}

src/app/app.browser.module.ts
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 { }
src/app/app.server.module.ts
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 { }
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';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
src/app/modules/items/items.component.ts
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;
  }

}
src/app/modules/items/items.component.spec.ts
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/

Comment créer une application From scratch ?

Créez votre compte ganatan

Téléchargez gratuitement vos guides complets

Démarrez avec angular CLI Démarrez avec angular CLI

Gérez le routing Gérez le routing

Appliquez le Lazy loading Appliquez le Lazy loading

Intégrez Bootstrap Intégrez Bootstrap


Utilisez Python avec Angular Utilisez Python avec Angular

Utilisez Django avec Angular Utilisez Django avec Angular

Utilisez Flask avec Angular Utilisez Flask avec Angular

Ganatan site Web avec Angular