Angular forRoot erklärt

Wenn ich beginne mich mit einer neuen Sprache oder einem neuen Framework auseinander zu setzen, nehme ich manche Dinge so hin wie sie sind ohne sie zu hinterfragen. In letzter Zeit beschäftige ich mich mehr mit bestimmten Angular-Dingen und in diesem Post möchte ich ein wenig Licht ins Dunkel bringen, wenn es um forRoot geht.

Das Beispielprojekt zum Beitrag findet ihr hier.

TLDR;

Um es kurz zu machen: In den meisten fällen braucht ihr die Funktion gar nicht mehr. Sie wird noch beim Routing verwendet, für eine einfache Angular-Anwendung spielt sie in der Regel allerdings keine Rolle mehr.

Die längere Antwort

Das ganze hängt damit zusammen, wie früher Services in der Anwendung registriert wurden. Sehen wir uns kurz einen einfachen Service an:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class SimpleService {

  constructor() { }
}

Das wichtige hier sind die Zeilen 3-5. @Injectable() gibt an, dass es sich um eine Klasse handelt, die über die Angular Dependency Injection eingefügt werden kann. providedIn: ‚root‘ gibt plump gesagt an, dass der Service in allen Modulen verfügbar ist.

Früher wurden die Services aber in den Modulen registriert. Wurde ein Service also in einem Modul registriert, dass wiederum vom root-Modul importiert wurde, war der Service auch dort verfügbar. Soweit so gut. Aber wo liegt das Problem?

Lazy Loading

Das Problem ist das Lazy Loading. Wenn bestimmte Module per Lazy Loading geladen werden, konnte die Anwendung ja noch nicht wissen, welche Services es gibt. Also muss der Service auch im root-Modul bereit gestellt werden. Zusätzlich wurde er aber im Modul, das per Lazy Loading nachgeladen wurde initialisiert und somit bleiben zwei Instanzen des Services übrig. Eigentlich sollte ein Service aber eine Singleton-Klasse sein.

Eine Klasse, von der es zwei Instanzen gibt, ist aber kein Singleton mehr. Da sind wir uns einig oder? Und genau hier kommt die forRoot-Methode ins Spiel.

forRoot wird aber immer wieder in Packages verwendet?

Richtig. Über die forRoot-Methode können z. B. auch Konfigurationen an andere Bibliotheken weiter gegeben werden, dazu ein andermal mehr.

Ein Beispiel

Sehen wir uns das Thema an einem Beispiel an. Als erstes benötigen wir eine leere Angular-Anwendung. In dieser erstelle ich als erstes einen Service:

nx generate @nrwl/angular:service _services/User

Der User-Service bekommt einfach nur einen Vornamen und einen Nachnamen. Die fertige Klasse sieht dann so aus:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class UserService {
  vorname: string = '';
  nachname: string = '';

  constructor() { }
}

Nun fügen wir den Service als erste in der App-Component ein und geben eine Überschrift aus:

import { Component } from '@angular/core';
import { UserService } from './_services/user.service';

@Component({
  selector: 'chris-codeblog-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'for-root';

  constructor(public userService: UserService) {
    this.userService.vorname = 'Super';
    this.userService.nachname = 'Man';
  }
}
<h1>Willkommen {{userService.vorname}} {{userService.nachname}}</h1>

<router-outlet></router-outlet>

Nun legen wir eine neue Komponente für das Hauptmodul, ein neues Modul und eine neue Komponente für das neue Modul an. Mit –route=einstellungen wird eine neue Route generiert und das Modul automatisch per LazyLoading geladen:

nx generate @nrwl/angular:component _view/home --module=app
nx generate @nrwl/angular:module einstellungen --route=einstellungen --module=app
nx generate @nrwl/angular:component _view/einstellungen/uebersicht --module=einstellungen

Nun ergänzen wir im App-Modul die neue Route zur Home-Komponente Dort wird außerdem der UserService eingefügt. Gleiches erledigen wir in der Einstellungen-Komponente. Am Ende sollten eure Dateien und Klassen wie folg aussehen:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';
import { NxWelcomeComponent } from './nx-welcome.component';
import { RouterModule } from '@angular/router';
import { HomeComponent } from './_view/home/home.component';

@NgModule({
  declarations: [AppComponent, NxWelcomeComponent, HomeComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot(
      [
        { path: '', component: HomeComponent },
        {
          path: 'einstellungen',
          loadChildren: () =>
            import('./einstellungen/einstellungen.module').then(
              (m) => m.EinstellungenModule
            ),
        },
      ],
      { initialNavigation: 'enabledBlocking' }
    ),
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
<router-outlet></router-outlet>
import { Component, OnInit } from '@angular/core';
import { UserService } from '../../_services/user.service';

@Component({
  selector: 'chris-codeblog-home',
  templateUrl: './home.component.html',
  styleUrls: ['./home.component.scss'],
})
export class HomeComponent implements OnInit {
  constructor(public userService: UserService) {}

  ngOnInit(): void {}
}
<h1>Willkommen {{ userService.vorname }} {{ userService.nachname }}</h1>

<a routerLink="/einstellungen">Einstellungen</a>
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { EinstellungenComponent } from './einstellungen.component';

const routes: Routes = [{ path: '', component: EinstellungenComponent }];

@NgModule({
  declarations: [EinstellungenComponent],
  imports: [CommonModule, RouterModule.forChild(routes)],
})
export class EinstellungenModule {}
import { Component, OnInit } from '@angular/core';
import { UserService } from '../_services/user.service';

@Component({
  selector: 'chris-codeblog-einstellungen',
  templateUrl: './einstellungen.component.html',
  styleUrls: ['./einstellungen.component.css'],
})
export class EinstellungenComponent implements OnInit {
  constructor(public userService: UserService) {}

  ngOnInit(): void {}
}
<h1>Einstellungen von {{userService.vorname}} {{userService.nachname}}</h1>

<a routerLink="/">Zurück</a>

Das Problem

Bisher sieht alles noch gut aus. Der UserService wird über alle Komponenten geteilt und richtig angezeigt. Zuständig dafür ist folgender Code:

@Injectable({
  providedIn: 'root'
})

Wir entfernen das Objekt, so dass die UserService-Klasse und ergänzen noch ein klein bisschen Code, damit wir sehen, wann der Service erzeugt wird (und wie oft er erzeugt wird). Das Ergebnis:

import { Injectable } from '@angular/core';

@Injectable()
export class UserService {
  vorname: string = '';
  nachname: string = '';

  constructor() { }
}

Nun wird die Anwendung erst mal nicht mehr funktionieren, da der Service noch nicht erzeugt wurde. Wir müssen Angular also erst wieder mitteilen, dass UserService einer Klasse ist die über für die DependencyInjection zur Verfügung stehen soll. Im App-Modul ergänzen wir also die Zeile providers: [UserService] (Zeile 19):

@NgModule({
  declarations: [AppComponent, NxWelcomeComponent, HomeComponent],
  imports: [
    BrowserModule,
    RouterModule.forRoot(
      [
        { path: '', component: HomeComponent },
        {
          path: 'einstellungen',
          loadChildren: () =>
            import('./einstellungen/einstellungen.module').then(
              (m) => m.EinstellungenModule
            ),
        },
      ],
      { initialNavigation: 'enabledBlocking' }
    ),
  ],
  providers: [UserService],
  bootstrap: [AppComponent],
})

Damit funktioniert die Anwendung wieder, so wie wir es uns erwarten würden. Wo liegt also das Problem? Das Problem liegt darin, wenn der Service ursprünglich in einem Modul deklariert wird, dass per LazyLoading nachgeladen wird. Wollen wir den Service auch in unserem Hauptmodul verwenden, muss der Service in beiden Modulen in dem providers-Array zur Verfügung gestellt werden. Hier das Einstellungen-Modul:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { EinstellungenComponent } from './einstellungen.component';
import { UserService } from '../_services/user.service';

const routes: Routes = [{ path: '', component: EinstellungenComponent }];

@NgModule({
  declarations: [EinstellungenComponent],
  imports: [CommonModule, RouterModule.forChild(routes)],
  providers: [UserService]
})
export class EinstellungenModule {}

Jetzt haben wir genau das Problem, das im Einstellungen-Module eine neue Instanz des UserService erzeugt wird. Anstelle von „Einstellungen von Super Man“ bekommen wir als Überschrift nun nur noch „Einstellungen von“, da der Name in unserem App-Modul gesetzt wird. Nun kommen wir zu der forRoot-Methode, mit der wir Angular mitteilen, dass es in dem Einstellungen-Modul, dass per LazyLoading geladen wird ebenfalls diesen Service gibt:

import { ModuleWithProviders, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Routes, RouterModule } from '@angular/router';
import { EinstellungenComponent } from './einstellungen.component';
import { UserService } from '../_services/user.service';

const routes: Routes = [{ path: '', component: EinstellungenComponent }];

@NgModule({
  declarations: [EinstellungenComponent],
  imports: [CommonModule, RouterModule.forChild(routes)],
  providers: []
})
export class EinstellungenModule {
  forRoot(): ModuleWithProviders<EinstellungenModule> {
    return {
      ngModule: EinstellungenModule,
      providers: [UserService]
    }
  }
}

Nun wird der Service wieder nur noch einmal erzeugt, so wie wir es erwarten.

Ich hoffe ich konnte die ganze Thematik einigermaßen verständlich erklären. Gerne könnt ihr eure Fragen dazu in die Kommentare schreiben.

Angular forRoot erklärt

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Nach oben scrollen