Angular @ WPS: Entwicklung von Reale-Welt-Anwendungen ohne Orientierungsverlust

Angular @ WPS: Entwicklung von Reale-Welt-Anwendungen ohne Orientierungsverlust


von Richard Voß @richard_voss

 

Seit letztem Jahr entwicklen wir bei WPS immer öfter Anwendungen für unsere Kunden mit Hilfe von Angular 4, 5, 6 und auch 7. Angular ist ein populäres Framework (es wird auch als Plattform bezeichnet) zur Entwicklung von web-basierten Frond-Ends mit Typescript, HTML und CSS. Es ist Open Source Software und wird hauptsächlich durch Google vorangetrieben.

Das Framework macht es wesentlich einfacher, komplexe GUIs zu strukturieren, fördert die Wiederverwendung von Komponenten und – am wertvollsten – erlaubt es tatsächlich, testgetriebene Front-End-Entwicklung zu betreiben, die Spaß macht.

 

Webanwendungen sind nicht leicht zu entwickeln

Client-seitige Webanwendungen sind richtige Software und sollten auch wie richtige Software entwickelt werden.

Webbasierte Benutzerschnittstellen sind mittlerweile eine extrem beliebt Wahl für sehr viele Anwendungen, weil sie leicht zugänglich sind, (fast) keine Rollout- und Verteilungsprobleme haben, plattformübergreifend funktionieren, sehr gut aussehen können und weil man sie recht effizient entwickeln kann.

Allerdings erhält all diese Vorteile nicht ganz ohne Preis und ohne gute Architektur und geeignete Entwicklungstechniken endet man oft im Chaos:

  • Browser haben Inkompatibilitäten, die Umsetzung von Standards ist eher „fließend“
  • Javascript wird zur Laufzeit interpretiert, von genau diesen Browsern, und ist dynamisch typisiert
  • Hunderte von HTML-Dateien und potentiell hunderte CSS-Direktiven konsistent (frei von Widersprüchen) zu halten ist sehr schwer
  • viele Probleme wiederholen sich (konsistente visuelle Gestaltung, nutzerfreundliche Formularelemente, Navigation, …)

Angular adressiert viele dieser Probleme:

  • Es versteckt DOM-Manipulationen und implementiert Cross-Browser-Kompatibilität,
  • führt Komponenten ein und modularisiert sowohl Funktion als auch Gestaltung,
  • ermöglicht des getrennte Testen von kleineren Einheiten (Komponenten und Services) mit richtigen Unit Tests,
  • erlaubt die Aufteilung einer Anwendung in Module und ermöglicht die Wiederverwendung dieser Module,
  • beinhaltet Typescript und seine Werkzeuge, wodurch …
    • … der Compiler und statische Typisierung uns helfen, weniger Fehler zu machen
    • … wir statische Codeanalyse sinnvoll durchführen können, um unsere Architektur in Ordnung zu halten.
    • … wir fast Javascript vergessen können und produktiver werden
  • und es hat jede Menge fertiger Lösungen für Standardprobleme.

Aus diesen Gründen haben wir Angular ins Herz geschlossen und finden es häufig passend für diverse Vorhaben. Natürlich gibt es andere Frameworks und Werkzeuge und jede Menge Diskussion und Meinungen über Vor- und Nachteile, aber darum soll es hier nicht gehen.

 

Angular-Anwendungen sind auch nicht leicht zu entwickeln

A fool with a tool is still a fool.

Trotz all dieser Großartigkeit des Angular-Frameworks bleiben genug Fehler übrig, die man innerhalb machen kann. Wir haben einige erfolgreiche Muster aber auch Fallstricke gefunden und gesammelt. Dieser Artikel (und weitere) sollen einen Einblick in unsere „Angular Pattern Library“ geben.

Losgehen wird es mit Best Practices rund um den Entwurf von Komponenten, die man durchaus als das Herz einer Angular-Anwendung bezeichnen kann. Alles Andere (Services, Routing, Modularisierung, …) existiert im Framework zu dem Zweck, Komponenten zu unterstützen.

Um Angular-Komponenten in den Griff zu bekommen, glauben wir, sollte man:

  1. sie als Modularisierungstechnik verstehen und nutzen: 👍 Factor out Components
  2. die Modularisierung der visuellen Eigenschaften berücksichtigen: 👍 Manage Component Layout Boundaries
  3. Unit Tests schreiben und dabei Abhängigkeiten isolieren: 👍 Mock Components
  4. Delegation bevorzugen, denn Vererbung hat so seine Tücken: 👎 Avoid Component Class Inheritance with Template Duplication
  5. sich recht bald mit fortgeschrittenen Techniken vertraut machen: 👍 Use Advanced Template Transclusion

Es gibt eine ganze Reihe weiterer DOs und DON’Ts in unserer Sammlung zu Komponenten, Services, Projektstruktur, Testing. Wir werden immer wieder weitere auswählen und und darüber hier schreiben.

 

👍 Factor out Components

Templates sind Code und für Code gilt das DRY principle. (Don’t Repeat Yourself)

Diese Empfehlung liegt auf der Hand, aber wir mussten selbst feststellen, dass man es nicht oft genug betonen kann. Zu einfach akzeptiert man die Duplizierung von Code wenn es „nur“ um ein paar Zeilen HTML geht.

Konsequenz der Empfehlung ist, dass man durchaus von einer wertvollen Komponente reden kann, selbst wenn ihre Typescript-Klasse fast leer ist. Der Wert kann im HTML-Template stecken. Und ja, das bedeutet, dass man mehr Komponenten erzeugen wird, wenn man dieser Empfehlung folgt.

Folgendes Beispiel illustriert die Verlockung, eine kleine Duplizierung von etwas Template-Code einfach mal zuzulassen…

@Component({
  selector: 'foo',
  template: '<h1>{{headline}}</h1><p>{{text}}</p>'
})
export class FooComponent {
  headline = 'Foo!';
  text = 'foo text';
  constructor(private fs: FooService) {
  this.headline = this.fs.getHeadline();
  }
}

@Component({
  selector: 'bar',
  template: '<h1>{{somethingCompletelyDifferentFromHeadline}}</h1><p>{{textButWeCallItDifferent}}</p>'
})
export class BarComponent {
  somethingCompletelyDifferentFromHeadline = 'Bar!';
  textButWeCallItDifferent = 'bar text';
}

Wäre dies die ganze Anwendung, wäre das natürlich zu vernachlässigen. Aber es wird so nicht bleiben, denn im Verlauf wird mehr Template-Code hinzukommen und dann wird man beginnen, Änderungen hin und zurück zu kopieren.

Dabei ist es an dieser Stelle noch sehr einfach, eine Komponente zu extrahieren, also machen wir es einfach mal:

@Component({
  selector: 'headline-and-text',
  template: '<h1>{{headline}}</h1><p>{{text}}</p>'
})
export class HeadlineAndTextComponent {
  @Input() headline: string;
  @Input() text: string;
}

@Component({
  selector: 'foo',
  template: '<headline-and-text [headline]="headline" [text]="text"></headline-and-text>'
})
export class FooComponent {
  headline = 'Foo!';
  text = 'foo text';
  constructor(private fs: FooService) {
  this.headline = this.fs.getHeadline();
  }
}

@Component({
  selector: 'bar',
  template: '<headline-and-text [headline]="somethingCompletelyDifferentFromHeadline" [text]="textButWeCallItDifferent"></headline-and-text>'
})
export class BarComponent {
  somethingCompletelyDifferentFromHeadline = 'Bar!';
  textButWeCallItDifferent = 'bar text';
}

Wichtig hierbei ist, dass FooComponent und BarComponent tatsächlich unterschiedlich sind und nur ein teilweise gleiches Template verwenden. Wollten wir nun zum Beispiel immer eine Art Fußzeile zu dieser Struktur aus Überschrift und Inhalt hinzufügen, wäre das nur eine Änderung.

@Component({
  selector: 'headline-and-text',
  template: '<h1>{{headline}}</h1><p>{{text}}</p><footer>Call {{number}}!</footer>'
})
export class HeadlineAndTextComponent {
  @Input() headline: string;
  @Input() text: string;
  number: string;

  constructor(ps: PhoneService) {
  this.number = ps.determinePhoneNumber();
  }
}

Wir haben dafür jetzt sogar eine Service-Abhängigkeit eingefügt, und das ohne Änderung an FooComponent oder BarComponent. Weiteres HTML und CSS-Direktiven hinzuzufügen wäre genau so einfach und nur an einer Stelle notwendig.

 

und weiter?

Das Extrahieren von Komponenten kann das Layout zerstören, deswegen wird es im nächsten Artikel weitergehen mit der Frage, wie man Layout-Grenzen zwischen Komponenten festlegt.

2018-12-07T15:34:10+00:006. Dezember 2018|