Angular Profi-Tipps

22.01.2021Tom Trapp
Mobile Angular

Hallo!

Welche Tipps und Tricks gibt es, um ein modernes Angular Projekt wartbarer und robuster zu machen?

Sollten Sie mit Angular noch nicht so vertraut sein, werfen Sie einen Blick auf unser Angular b-nova ToDo List Tutorial.

Strict Mode

Wäre es nicht elegant, wenn potenzielle Fehler in Bezug auf null oder undefined Variablen frühzeitig erkannt werden können? Wenn man quasi ‘gezwungen’ ist, auf sauberen und robusten Code zu achten? Genau das erlaubt Angular und TypeScript mit dem sogenannten Strict Mode.

Bei neue Projekte kann der Strict Mode mit dem Parameter --strict beim ng new aktiviert werden.

Bei vorhandenen Projekten wird der Strict Mode mit einigen Configs aktiviert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// tsconfig.json
{
  "compilerOptions": {
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  },
  "angularCompilerOptions": {
    "strictInjectionParameters": true,
    "strictInputAccessModifiers": true,
    "strictTemplates": true
  }
}
1
2
3
4
5
6
// angular.json, inside projects.[projectName]
"schematics": {
    "@schematics/angular:application": {
        "strict": true
    }
}

Beispielsweise würde bei aktiviertem Strict Mode der Compiler bei folgendem Code einen Fehler ausgeben, da der String content nicht initialisiert ist.

1
2
3
4
5
6
7
export class ToDo {
    content:string;
    completed:boolean = false;
}

// (property) ToDo.content: string
// Property 'content' has no initializer and is not definitely assigned in the constructor.ts(2564)

Einen weiteren Teil des Strict Modes lernen wir direkt im nächsten Punkt kennen.

Use Bundle Budgets

Wie bei vielen anderen Projekten geht es auch bei Angular um Performance und um die Grösse der Applikation. In Angular werden bestimmte Teile der Applikation in sogenannte Bundles aufgeteilt, beispielsweise befinden sich im vendor Bundle alle importierten Dependencies.

Mit der sogenannten Bundle Budgets Funktionalität lässt sich die Grösse dieser Bundles überwachen, Sie erhalten Warnungen oder gar Buildfehler, wenn bestimmte Schwellwerte (nach oben oder nach unten) verletzt werden.

Die Bundle Budgets werden mit dem Strict Mode aktiviert und in der angular.json Datei definiert:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
  "configurations": {
    "production": {
      "budgets": [
        {
          "type": "bundle",
          "name": "vendor",
          "baseline": "500kb",
          "warning": "100kb",
          "error": "200kb"
        }
      ]
    }
  }
}

Wichtig zu erwähnen ist noch, dass Budgets nur bei einem produktiven Build wirklich Sinn machen, da dort alle Optimierungen (Minifying, Tree Shaking usw.) angewendet werden. Im oberen Beispiel würde es zu einer Warnung kommen, wenn das Bundle vendor kleiner als 400kb oder grösser als 600 kb wird. Ebenso würden der Build fehlschlagen, wenn das Bundle kleiner als 300kb bzw. grösser als 700kb wird.

Aber wann kommt es zu einer enormen Vergrösserung eines Bundles? Dies kann beispielsweise bei falschen Import-Statements (bspw. beim Auto Import) o. ä. passieren.

Aliases for Import Statements

Sicherlich kennt jeder Angular-Entwickler das Problem mit den relativen Pfaden bei Import Statements. Diese sehen normalerweise wie folgt aus:

1
2
3
import { ToDo } from "../../../interfaces/ToDo";
import { HelloWorld } from "../../../services/HelloWorld";
import { Fruit } from "../../../models/Fruit";

Ein einfacher Trick erlaubt es uns, Aliases für bestimmte Ordner in Import Statements zu nutzen, diese Aliases muss man in der tsconfig.json Datei angeben:

1
2
3
4
5
6
7
8
9
// tsconfig.json
  "compilerOptions": {
    "baseUrl": "./",
    "paths": {
      "@interfaces/*": ["src/app/interfaces/*"],
      "@services/*": ["src/app/services/*"],
      "@models/*": ["src/app/models/*"]
    }
  }

Zu beachten ist hier, dass die Imports immer relativ zur baseUrl (in diesem Fall dem root Level der Anwendung) sind.

Sind diese Aliases definiert, können wir diese in den Import Statements verwenden, viel sauberer und einfach cool!

1
2
3
import { ToDo } from "@interfaces/ToDo";
import { HelloWorld } from "@services/HelloWorld";
import { Fruit } from "@models/Fruit";

Use Services

Ein weiterer Tipp ist die Auslagerung von Logik aus der Komponente in dezentrale, wiederverwendbare Services. Dadurch steigt generell die Wartbarkeit und die Übersicht im Code. Es gibt aber Kleinigkeiten zu beachten.

Services können sehr einfach per Angular CLI Command erstellt werden:

1
ng generate service service/MyService

Ein Service wird mit der Injectable Annotation definiert:

1
2
3
4
5
6
7
import { Injectable } from '@angular/core';

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

Ein weiterer grosser Vorteil eines Injectable Services ist, dass dieser tree-shakeable ist und somit, bei Nichtbenutzung, aus dem Bundle ausgelassen wird.

Generell sollte man sim Hinterkopf behalten, dass standardmässig alle Services als Singleton fungieren und somit die Instanz des Services immer zwischen den Komponenten geshared wird.

Will man eine Instanz des Services pro Komponente (non-singleton) kann man dies über die Providers Definition in der Komponente erreichen.

1
2
3
4
5
6
7
8
@Component({
    selector: 'app-mycomponent',
    templateUrl: './app.mycomponent.html',
    styleUrls: ['./app.mycomponent.css'],
    providers: [MyService]
})

export class MyComponent {}

Die Instance des MyService kann dann in dieser Komponente und ihren parent und child Komponenten verwendet werden.

Allgemein gesehen sollte aber ein Service immer wiederverwendbar und unabhängig sein – sprich ein Singleton.

Use Pipes

Sogenannte Pipes ermöglichen die Transformation von Texten, Nummern, Datumsangaben, Währung etc. in Template-Expressions. So kann ein Datum beispielsweise für die Ausgabe korrekt formatiert werden.

Angular bietet out-of-the-box schon zahlreiche Pipes an, wie beispielsweise die UpperCasePipe. Will man beispielsweise die Variable name immer grossgeschrieben darstellen, kann man dies direkt im HTML in der Template-Expression machen:

1
<p>{{ name | uppercase }}</p>

Beim Aufruf macher Pipes lassen sich auch noch Parameter wie beispielsweise ein Datumsformat mitgeben:

1
<p>My birthday is {{ birthday | date:"dd/MM/yy" }} </p>

Selbstverständlich lassen sich Pipes auch aneinanderreihen:

1
<p>My birthday is {{ birthday | date:"dd/MM/yy" | uppercase }} </p>

Interessanter wird es dann, wenn man Pipes selbst implementiert, um gewisse Transformationen zu erreichen. Beispielhaft wollen wir eine Pipe erstellen, welche “super cool” vor den Wert schreibt.

Glücklicherweise bietet auch hier die Angular CLI ein praktisches Command an:

1
ng generate pipe pipes/SuperCool

Dadurch wird eine neue Pipe angelegt und automatisch im app.modules.ts referenziert.

Unsere Pipe ist schlussendlich eine TypeScript Klasse, mit einer Pipe Annotation, welche das Interface PipeTransform implementiert und eine transform Methode besitzt.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'superCool'
})
export class SuperCoolPipe implements PipeTransform {

  transform(value: string): string {
    return "super cool " + value;
  }
}

In der Annotation Pipe gibt es eine Variable name, mit diesem Wert wird die Pipe in der Template-Expression aufgerufen.

1
2
3
<p>{{ toDo.content | superCool }}</p>

<!-- super cool Hello World -->

Wichtig zu wissen ist, dass eine Pipe per default pure ist und ihre Werte somit cached. Dies bedeutet auch, dass die Pipe nur bei einem echten (pure) Change an dem Wert erneut ausgeführt wird. Dadurch erhöht sich die Performance und Sie sollten bei Formatierungen o. ä. immer zur Pipe greifen.

Nun kennen Sie ein paar Tipps und Tricks für Ihr Angular Projekt! Sollten Sie Unterstützung in diesem Bereich brauchen, kontaktieren Sie uns!

Tom Trapp

Tom Trapp – Problemlöser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.