FreshRSS

🔒
✇ Developer-Blog - Tales from the Web side

Die Temporal API – besseres Datehandling für JavaScript

Von heise online — 21. Juni 2021 um 11:58

Die Temporal API – besseres Datehandling für JavaScript

Tales from the Web side Sebastian Springer

Der ECMAScript-Standard wird stetig weiterentwickelt und JavaScript damit stetig verbessert. Eine der größten Baustellen war bisher der Umgang mit Datum und Zeit. Das ändert sich jedoch mit der neuen Temporal API.

JavaScript, Datum und Zeit, das ist so eine Geschichte für sich und sie nimmt leider kein gutes Ende. Jedes Mal, wenn ich ein neues Date-Objekt erzeuge oder noch schlimmer mit Datum und Zeit rechnen muss, frage ich mich, wie es eine Sprache mit einer so schlechten Datums- und Zeitimplementierung so weit bringen konnte. Aber seien wir mal ehrlich: Bei diesem Thema bekleckern sich auch andere Sprachen nicht mit Ruhm. Aber jetzt sprechen wir erstmal von JavaScript und einer sehr erfreulichen Entwicklung.

JavaScript erhält eine Spracherweiterung mit dem Namen Temporal, die sich mit Datum und Zeit beschäftigt und eine deutlich angenehmere Schnittstelle als die bisherige Date-API hat, die sich übrigens an einer der ersten Date-APIs von Java orientiert. Die neue Schnittstelle behebt nicht nur einige Schwächen der bisherigen, sondern gestaltet den Umgang mit Zeit und Datum deutlich moderner und greift einige Aspekte von modernen Bibliotheken wie beispielsweise die Unveränderbarkeit von Objekten auf. Aktuell befindet sich die API zwar noch als ECMAScript Proposal in der Stage 3. Das ist jedoch auch beinahe das Ende des Standardisierungsprozesses mit seinen insgesamt vier Stufen, die ein Feature durchlaufen muss, bevor es in den Standard aufgenommen wird. Üblicherweise gibt es in der dritten Stufe keine größeren Änderungen mehr an der Schnittstelle, sodass das, was wir jetzt in der Temporal API finden, mit großer Wahrscheinlichkeit auch das sein wird, was schließlich die Browserhersteller in ihre JavaScript Engines aufnehmen.

Für alle Ungeduldigen, die der Temporal API bereits jetzt eine Chance geben möchten, existiert ein Polyfill, das die Schnittstelle in aktuellen Browsern nachbildet. Das NPM-Paket mit dem Namen proposal-temporal enthält es und wird mit dem Kommando npm install proposal-temporal installiert. Die Temporal-Schnittstelle ist, wie alle nativen Sprachfeatures von JavaScript, plattformunabhängig. Das bedeutet, dass es sowohl clientseitig im Browser als auch serverseitig in Node.js zum Einsatz kommen kann. Das gleiche gilt für das Polyfill, es ist außerdem kompatibel mit allen gängigen Modulsystemen, sodass es in allen Umgebungen getestet werden kann.

Schnelleinstieg: Wie komme ich an mein Datum?

Es gibt mehrere Anwendungsfälle, denen man in JavaScript-Applikationen immer wieder begegnet und diese werden wir uns als Erstes ansehen. Beginnen wir mit dem aktuellen Zeitpunkt. Das Temporal-Objekt bietet für diesen Zweck das now-Objekt mit der plainDateTime-ISO-Methode. Gerade für den Austausch zwischen Systemen arbeiten Entwickler nicht nur mit einer Datumszeichenkette, sondern mit UNIX-Zeitstempeln, also den Sekunden, die seit dem 1.1.1970 00:00 vergangen sind. Hierfür bietet das now-Objekt die Methode instant, deren Rückgabewert die epochSeconds-Eigenschaft aufweist, die wiederum den UNIX-Zeitstempel enthält. Die letzte Aufgabe im Schnellstart mit der neuen Temporal-Schnittstelle ist das Erzeugen eines beliebigen Zeitpunkts. Dies wird über die PlainDateTime.from-Methode erreicht. Die from-Methode akzeptiert sowohl ein Konfigurationsobjekt als auch eine Zeichenkette, die das gewünschte Datum angibt. Das nachfolgende Listing zeigt die verschiedenen Szenarien.

import { Temporal } from "proposal-temporal"; 

const currentTime = Temporal.now.plainDateTimeISO();
console.log(currentTime.toString()); // Ausgabe:
2021-06-03T08:22:18.381338378

const timeStamp = Temporal.now.instant().epochSeconds;

console.log(timeStamp); // Ausgabe: 1622701338
const christmas = Temporal.PlainDateTime.from({
day: 24,
month: 12,
year: 2021,
hour: 20,
minute: 15,
second: 32
});
console.log(christmas.toString()); // Ausgabe: 2021-12-24T20:15:32

Die verschiedenen Klassen der Temporal API

Bestehende Applikationen werden es bei der Umstellung von der Date API auf die neue Temporal-Schnittstelle erforderlich machen, dass die Datums- und Zeitobjekte zwischen beiden Schnittstellen konvertiert werden. Listing 2 zeigt, wie eine solche Umwandlung funktionieren kann.

import { Temporal } from 'proposal-temporal';

const date = new Date('2020-12-24T20:15:32');
const temporal = new Temporal.ZonedDateTime(
BigInt(date.getTime() * 10 ** 6),
'Europe/Berlin');
console.log(temporal.toString());
//2020-12-24T20:15:32+01:00[Europe/Berlin]

const temporal2 = Temporal.ZonedDateTime.from({
year: 2020,
month: 12,
day: 24,
hour: 20,
minute: 15,
second: 32,
timeZone: 'Europe/Berlin'
});
const date2 = new Date(temporal2.epochMilliseconds);
console.log(date2.toLocaleString()); // Ausgabe: 24/12/2020, 20:15:32

Dieses Codebeispiel zeigt ein grundlegendes Problem der Date API von JavaScript sehr schön: Es ist nicht möglich, ein Date-Objekt zu erzeugen, das unabhängig von einer Zeitzone ist. Das Date-Objekt hat die lokale Zeitzone der Umgebung, in meinem Fall ist das die mitteleuropäische Zeit. Die Entsprechung dieses Konzepts in Temporal sind Objekte vom Typ ZonedDateTime. In einigen Fällen ist jedoch die Information über die Zeitzone in einem Datumsobjekt gar nicht notwendig. Dieses Problem löst die Temporal-Schnittstelle, indem es verschiedene Objekte gibt, um eine Datums- und Zeit-Kombination zu repräsentieren. Die folgende Abbildung aus dem Temporal Proposal fasst diesen Sachverhalt grafisch zusammen.

Temporal-Api.png
Die Klassen der Temporal API (Bild: https://tc39.es/proposal-temporal/docs/object-model.svg)

Die umfassendste Klasse der Schnittstelle ist ZonedDateTime, sie enthält die Information über den Zeitpunkt sowie die Zeitzone. Im Gegensatz dazu haben die Plain-Typen wie PlainDateTime oder PlainTime keine Information über Zeitzonen und stellen lediglich einen generischen Zeitpunkt dar. Alle Typen, außer Instant, arbeiten außerdem auf einem bestimmten Kalender. Standardmäßig nutzt Temporal den Gregorianischen Kalender, der in der ISO 8601-Norm standardisiert ist. Weitere Beispiele für Kalender, die an dieser Stelle zum Einsatz kommen können, sind der Buddhistische, der Persische oder der Japanische Kalender.

Rechnen mit Datumswerten

Ein großer Vorteil der Temporal API gegenüber der bisherigen Implementierung ist, dass sie in der Lage ist, mit Datumswerten zu rechnen und mit Zeitspannen umzugehen. Die Grundlage hierfür bildet die Duration-Klasse beziehungsweise Objekte, die der Struktur entsprechen, die diese Klasse vorgibt. Beim Rechnen mit Datumswerten kommt eine weitere Besonderheit der Temporal Schnittstelle zum Tragen: Die Schnittstelle arbeitet mit immutable Objekten, modifiziert das Ausgangsobjekt beispielsweise bei einer Addition nicht direkt, sondern gibt ein neues Objekt zurück. Wie das im Code funktioniert, zeigt folgendes Beispiel:

import { Temporal } from 'proposal-temporal'; 

const now = Temporal.now.plainDateTimeISO();
console.log(now.toString()); // Ausgabe: 2021-06-03T19:26:42.949202945
const future = now.add({days: 30});
console.log(future.toString()); // Ausgabe: 2021-07-03T19:26:42.949202945
console.log(now.toString()); // Ausgabe: 2021-06-03T19:26:42.949202945

Alle Zoned- und Plain-Klassen der Temporal API unterstützen die add- und subtract-Methoden. Diese ermöglichen, wie die Namen schon verraten, das Hinzufügen beziehungsweise Abziehen von Zeitspannen zu einem bestimmten Zeitpunkt. Beide Methoden akzeptieren einfache JavaScript-Objekte mit den Angaben, wie viele Zeiteinheiten, also beispielsweise Monate, Tage oder Stunden, abgezogen oder addiert werden sollen. Alternativ kann auch ein Duration-Objekt eingesetzt werden.

Ein weiterer Einsatzzweck für das Duration-Objekt ist die Bestimmung der Zeitspanne, die zwischen zwei Datumswerten liegt. Soll beispielsweise herausgefunden werden, wie viele Tage zwischen dem Beginn und dem Ende der Weihnachtsferien liegen, kann der folgende Quellcode eingesetzt werden.

import { Temporal } from 'proposal-temporal'; 

const start = Temporal.PlainDate.from({
day: 24,
month: 12,
year: 2021
});

const end = Temporal.PlainDate.from({
day: 8,
month: 1,
year: 2022
});

const diff = start.until(end);
const diff2 = end.since(start);
console.log(diff.toString()); // Ausgabe: P15D
console.log(diff2.toString()); // Ausgabe: P15D

Sowohl der Start- als auch der Endpunkt werden durch ein PlainDate-Objekt repräsentiert, da in diesem Fall weder Zeitzone noch Uhrzeit benötigt wird. Neben der hier verwendeten until-Methode gibt es mit der since-Methode auch noch das Gegenstück, das die Zeitspanne vom Ende bis zum Start misst. Das Ergebnis ist in beiden Fällen ein Duration-Objekt, das 15 Tage umfasst. Ein solches Objekt kann dann wieder in Kombination mit weiteren Temporal-Objekten verwendet werden, um beispielsweise eine Addition oder Subtraktion durchzuführen. Die Formatierung der Ausgabe, P15D, erscheint auf den ersten Blick etwas gewöhnungsbedürftig, folgt jedoch einem klaren Format. Die Zeichenkette wird durch das Zeichen P eingeleitet, danach folgen Jahre, Monate, Wochen und Tage. Anschließend werden Datum und Uhrzeit durch das Zeichen T getrennt gefolgt von Stunden, Minuten und Sekunden.

Wären in diesem Beispiel nicht nur Datumswerte, sondern auch Uhrzeiten beteiligt, gibt es mit der round-Methode die Möglichkeit, eine Zeitspanne auf ganze Tage zu runden. Die Methode akzeptiert ein Konfigurationsobjekt, dessen wichtigste Eigenschaften largestUnit, smallestUnit und roundingMode sind. Die ersten beiden geben die kleinste beziehungsweise größte Einheit an, auf die gerundet werden soll. Mit der roundingMode-Eigenschaft können Entwickler festlegen, wie gerundet wird. Der Code in folgendem Listing rundet beispielsweise auf volle Tage ab:

import { Temporal } from 'proposal-temporal'; 

const start = Temporal.PlainDateTime.from({
day: 24,
month: 12,
year: 2021,
hour: 7,
minute: 30
});

const end = Temporal.PlainDateTime.from({
day: 8,
month: 1,
year: 2022,
hour: 20,
minute: 15
});

console.log(start.until(end).toString()); // Ausgabe: P15DT12H45M
const diff = start.until(end).round({
smallestUnit: 'day',
roundingMode: 'floor'
});
console.log(diff.toString()); // Ausgabe: P15D

Das Ergebnis der round-Methode ist wiederum ein Duration-Objekt, das die Applikation weiterverwenden kann. Das Prinzip der Immutability bleibt also auch in diesem Fall gewahrt.

Fazit

Mit der Temporal API trägt das TC39 der Tatsache Rechnung, dass die Date-Funktionalität von JavaScript einige Schwächen aufweist und für die Umsetzung moderner Applikationen nicht die beste Wahl ist. Rund um den Umgang mit Datum und Zeit sind im Laufe der Zeit eine Vielzahl von Bibliotheken entstanden. Das Temporal Proposal greift viele der Ideen auf, die diese Bibliotheken verfolgen und integriert sie in den Sprachkern von JavaScript. Das hat den Vorteil, dass diese Features, sobald die Temporal API in den Standard aufgenommen wurde, nativ vom Browser unterstützt werden. Das bedeutet, dass es keinen zusätzlichen Overhead durch Bibliotheken mehr gibt und die Funktionalität potenziell performanter ist, da sie direkt im Browser umgesetzt ist. Diese Schnittstelle ist ein sehr gutes Beispiel dafür, dass sich JavaScript stetig weiterentwickelt und dabei auf die Anforderungen der Entwickler eingeht.

Links


URL dieses Artikels:
https://www.heise.de/-6069626

Links in diesem Artikel:
[1] https://tc39.es/proposal-temporal/docs/
[2] https://tc39.es/proposal-temporal/docs/cookbook.html

Copyright © 2021 Heise Medien

Adblock test (Why?)

✇ Developer-Blog - Tales from the Web side

Die Intl-API – Internationalisierung im Browser

Von heise online — 07. Mai 2021 um 16:35

Die Intl-API – Internationalisierung im Browser

Tales from the Web side Sebastian Springer

Internationalisierung ist in der Web-Entwicklung ein Thema, das häufig leider immer noch viel zu kurz kommt. Für die meisten endet der Begriff Internationalisierung auch schon bei der Übersetzung von Texten. Das ist jedoch eine Problemstellung, die der Browser nach wie vor den etablierten Bibliotheken überlässt. Wo alle modernen Browser jedoch mittlerweile dank der Intl API durchgängig punkten können, sind Themen wie die lokalisierte Formatierung von Zahlen, Datums- und Zeitwerten. Neben diesen Angaben bringt die Intl API noch Features zum lokalisierten String-Vergleich sowie Regeln für die Behandlung von Ein- und Mehrzahl.

Alle JavaScript-Umgebungen, also die bekannten Browser, Node.js und sogar der Internet Explorer, unterstützen die Intl API in Form des globalen Intl-Objekts. Es enthält neben der getCanonicalLocales-Methode die folgenden Konstruktoren:

  • Collator: Klasse für sprachabhängigen String-Vergleich
  • DateTimeFormat: Formatierung von Datums- und Zeitwerten
  • NumberFormat: Formatierung von Zahlen wie Währungen oder Prozentwerten
  • PluralRules: Unterstützung bei der Behandlung von Ein- und Mehrzahl

Die Intl API setzt durchgängig auf Gebietsschema-Informationen zur Steuerung der einzelnen Features. Zu diesem Zweck akzeptieren die Konstruktoren der Schnittstelle Locale-Strings, die den BCP-47-Vorgaben [1] folgen, also beispielsweise “de” oder “de-DE”. Erzeugt ein Script eine neue Instanz einer der Intl-Klassen, akzeptiert diese entweder ein Array von Locales, eine einzelne Locale oder keinen Wert. Ein ungültiger Wert führt dazu, dass der Browser einen RangeError auswirft.

Die getCanonicalLocales-Methode liefert für eine Eingabe einen gültigen Locale-Wert zurück und verhält sich dabei wie die Konstruktoren der Intl-Unterklassen. Den Wert “DE” wandelt die Methode in “de” um, ein Array mit den Werten “de”, “de-de” und “DE-DE” wird zu “de” und “de-DE” und die Eingabe “de-DEU” führt zu einem RangeError. Bei einem Array entfernt die getCanonicalLocales-Methode alle Duplikate.

Alle Intl-Klassen verfügen über eine statische Methode mit dem Namen supportedLocalesOf. Diese Methode akzeptiert einen Locale-String oder ein Array von Locales und liefert ein Array von Locales zurück, das die jeweilige Klasse direkt unterstützt, ohne auf Standard-Locale zurückzugreifen.

Collator: String-Vergleiche

JavaScript verfügt über die Array.prototype.sort-Methode zur Sortierung von Arrays. Diese Methode sortiert die einzelnen Elemente anhand ihrer UTF-16-Codewerte. Alternativ akzeptiert die Sort-Methode eine Vergleichsfunktion. Diese Vergleichsfunktion erhält bei jedem Aufruf zwei Werte, die miteinander verglichen werden. Ist der erste Wert kleiner als der zweite, gibt die Vergleichsfunktion eine Zahl kleiner als Null zurück, ist der erste Wert größer, wird eine Zahl größer Null zurückgegeben. Sind beide Werte gleich, ist der Rückgabewert der Vergleichsfunktion Null. Je nach Region, in der die Applikation ausgeführt wird, kann das Verhalten dieser Sortierung unterschiedlich sein. Die Collator-Klasse der Intl API nimmt Entwicklern diese Aufgabe ab und sorgt dafür, dass die Sortierung für die gewählte Region passend ist.

Die compare-Methode einer Collator-Instanz hat eine zur sort-Methode der JavaScript Arrays passende Signatur, akzeptiert also zwei Werte und gibt entweder 1, 0 oder –1 zurück. Das folgende Beispiel sortiert das Array mit den Werten “Birnen”, “Äpfel” und “Zitronen” für die Locale “de-DE”.

const col = new Intl.Collator('de');
const arr = [ 'Birnen', 'Äpfel', 'Zitronen' ];
console.log(arr.sort(col.compare)); // ["Äpfel", "Birnen", "Zitronen"]

Wie zu erwarten, ist das Ergebnis “Äpfel, Birnen, Zitronen”. Für die Locale “sv-SE”, also Schwedisch in Schweden, sieht das Ergebnis ganz anders aus, da hier das “Ä” nach dem “Z” einsortiert wird. Und das ist nur ein Beispiel, bei dem die Collator-Klasse hilfreich ist. Ein weiterer typischer Anwendungsfall ist das Sortieren von Zahlenwerten. Standardmäßig sortiert JavaScript folgendermaßen: 1, 132, 22. Die erste Ziffer ist hier also jeweils ausschlaggebend und nicht der Zahlenwert. Die Collator-Klasse sieht eine Lösung vor, nach der numerische Werte korrekt interpretiert werden können. Dieses Verhalten ist jedoch deaktiviert und muss bei der Erzeugung der Collator-Instanz zunächst aktiviert werden. Das erfolgt entweder über die Unicode-Erweiterung “kn” der Locale-Zeichenkette, also in diesem Fall beispielsweise “de-u-kn", oder über die numeric-Option, deren Wert auf true gesetzt wird. Solche Optionen akzeptieren alle Intl-Konstruktoren in Form eines Objekts als zweites Argument bei der Instanziierung.

const col = new Intl.Collator('de', {numeric: true});
const arr = [132, 1, 22 ];
console.log(arr.sort(col.compare)); // [1, 22, 132]

Zusätzlich zu diesen Optionen unterstützt der Collator noch weitere, beispielsweise wie Groß- und Kleinschreibung unterschieden werden sollen. Diese insgesamt flexibele Klasse sorgt dafür, dass für einen Stringvergleich kaum noch benutzerdefinierte Funktionen geschrieben werden müssen.

DateTimeFormat: Formatierung von Datum und Zeit

Eine der großen Schwächen der JavaScript Date-Klasse ist, dass sich die mit ihr erzeugten Objekte kaum formatiert ausgeben lassen. Dafür benötigt eine Applikation immer eine zusätzliche Bibliothek. Die Intl API löst zumindest einen Teil dieser Probleme. Die toString-Methode des Date-Objekts liefert den folgenden Wert: “Fri Jun 05 2020 10:15:00 GMT+0200 (Central European Summer Time)”. In einer Web-Applikation ist eine solche Zeichenkette jedoch wenig sinnvoll. Normalerweise sollen nur Datum oder Zeit in der korrekten Formatierung angezeigt werden – also beim Datum etwa 05.06.2020, 05/06/2020 oder 06/05/2020. Die Date-Klasse unterstützt eine solche Formatierung nicht direkt, sondern nur über einzelne Methoden wie getDay oder getMonth, die allerdings nur einstellige Ausgaben für den 5. beziehungsweise Juni liefern. Entwickler müssen hier also wieder selbst tätig werden. Die DateTimeFormat-Klasse der Intl API liefert mit ihrer format-Methode eine elegantere Lösung:

const dateTime = new Intl.DateTimeFormat('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});

console.log(dateTime.format(date)); // 05.06.2020

Eine Änderung der Locale auf “en-US” führt zur Ausgabe von “06/05/2020” und “en-GB” sorgt für “05/06/2020”. Das Options-Objekt der DateTimeFormat-Klasse entscheidet über das Aussehen und auch den Inhalt der einzelnen Elemente des Date-Objekts. Wird die day-Eigenschaft beispielsweise weggelassen, fällt auch die Tagesangabe bei der Ausgabe weg. Die meisten Eigenschaften des Options-Objekts unterstützen die Werte “2-digit”, also auf zwei Stellen aufgefüllt, und “numeric”, was je nach Wert eine einstellige, zweistellige oder bei der Jahreszahl eine vierstellige Ausgabe produziert. Bei der Formatierung von Zeitwerten verhält sich die DateTimeFormat-Klasse wie das Datum:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
});

console.log(dateTime.format(date)); //10:15:00

Beim Formatieren von Datum und Uhrzeit hilft die DateTimeFormat-Klasse, allerdings sind die Möglichkeiten eingeschränkt. Eine komplett freie Formatierung ist nicht vorgesehen. Es gibt aber die formatToParts-Methode, die jeden einzelnen Teil des Ergebnisses als ein separates Array-Element enthält:

const dateTime = new Intl.DateTimeFormat('de-DE', {
hour: '2-digit',
minute: '2-digit',
});

console.log(dateTime.formatToParts(date));

/* Ausgabe:
[{type: "hour", value: "10"},
{type: "literal", value: ":"},
{type: "minute", value: "15"}]*/

Mit den einzelnen Teilen des resultierenden Objekts lassen sich beliebige Formate realisieren. Hier ist jedoch umfangreiche Handarbeit erforderlich – es wird jedoch deutlich einfacher als mit dem nativen Date-Objekt von JavaScript.

NumberFormat: Zahlen formatieren

Die NumberFormat-Klasse kümmert sich, wie der Name andeutet, um das Formatieren von Zahlenwerten und zwar in drei verschiedenen Arten. Die style-Eigenschaft bestimmt, ob es sich um eine gewöhnliche Zahl (decimal), einen Währungswert (currency) oder einen Prozentwert (percent) handelt. Der Standardwert für die Option style ist “decimal” und useGrouping weist “true” auf, sodass immer ein Tausendertrennzeichen verwendet wird und die im Beispiel verwendete Option auch weggelassen werden könnte. Der nachfolgende Code gibt die Zahl 3141,59 mit Tausendertrenner und zwei Nachkommastellen aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('de-DE', {
useGrouping: true,
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // 3.141,59

Mit der Option useGrouping kann gewählt werden, ob der Tausendertrenner angezeigt werden soll. Die Änderung der Locale im Konstruktor auf den Wert “en-US” führt zur Ausgabe von 3,141.59.

Ein weiteres wichtiges Feature der NumberFormat-Klasse ist die Formatierung von Währungswerten. Dieses Feature wird durch die style-Eigenschaft mit dem Wert “currency” aktiviert. Zusätzlich dazu muss die Währung über die currency-Eigenschaft übergeben werden. Erfolgt dies nicht, wirft der Browser einen TypeError aus. Zur Auswahl der Werte stehen die ISO-4217-Währungscodes wie “EUR” oder “USD” bereit. Der nachstehende Code gibt den Wert €3,141.59 aus:

const number = Math.PI * 1000; 
const numberFormatter = new Intl.NumberFormat('en-GB', {
style: "currency",
currency: "EUR",
maximumFractionDigits: 2,
});

const formattedNumber = numberFormatter.format(number);

console.log(formattedNumber); // €3.141,59

Die currencyDisplay-Eigenschaft steuert die Anzeige des Währungssymbols. Der Standardwert “symbol” zeigt in diesem Beispiel das Euro-Symbol an. Weitere mögliche Werte sind “code” für den ISO-Code und “name” für den ausführlichen Namen der Währung.

PluralRules: Einzahl oder Mehrzahl, das ist hier die Frage

Eine Instanz der PluralRules-Klasse gibt mit der select-Methode für eine Zahl eine Zeichenkette zurück, die angibt, ob es sich um Ein- oder Mehrzahl handelt. Zugegebenermaßen ist diese Klasse für die deutsche Locale eher unspektakulär. Bei der type-Option mit dem Standardwert “cardinal” gibt die Methode für die Zahl 1 den Wert “one” und für alle übrigen den Wert “other” zurück. Dieser Typ steht für Mengen. Mit dem “ordinal”-Typ kann gezählt werden, was für die deutsche Locale durchgängig die Werte “other” zurückgibt. Anders sieht die Situation für die Locale “en-US” aus, wie folgendes Codebeispiel zeigt:

const arr = new Array(24).fill('').map((v, i) => i);
const pr = new Intl.PluralRules('en-US', {type: 'ordinal'});

arr.forEach(v => console.log(v, pr.select(v)));

Für alle Werte, die auf 1 enden, gibt die select-Methode “one”, für 2 den Wert “two”, für 3 den Wert “few” aus, und für alle weiteren Eingaben liefert select die Ausgabe “other” zurück. Eine Umstellung auf den “cardinal”-Typ liefert wieder “one” für 1 und “other” für die übrigen Werte zurück.

Fazit: hilft uns die Intl API wirklich?

Die Intl API bietet einige sinnvolle Erweiterungen für den JavaScript-Sprachstandard, um internationale Applikationen zu ermöglichen. Das Thema Übersetzungen spart die Schnittstelle aber komplett aus und überlässt es zusätzlichen Bibliotheken. Aber gerade die Formatierung von Datums-, Zeit- und Zahlenwerten löst eine Reihe von Standardproblemen auf elegante und solide Art.


URL dieses Artikels:
https://www.heise.de/-6040598

Links in diesem Artikel:
[1] https://tools.ietf.org/html/bcp47

Copyright © 2021 Heise Medien

Adblock test (Why?)

✇ Developer-Blog - Tales from the Web side

Dependency Injection in JavaScript

Von heise online — 22. März 2021 um 08:54

Dependency Injection in JavaScript

Tales from the Web side Sebastian Springer

Bibliotheken und Frameworks setzen auf Dependency Injection, um mehr Flexiblität im Umgang mit Abhängigkeiten innerhalb einer Applikation zu erhalten.

In Programmiersprachen wie Java und PHP ist Dependency Injection als Entwurfsmuster seit vielen Jahren etabliert. Und es hat in den letzten Jahren auch Einzug in die JavaScript-Welt gehalten. Der wohl bekannteste Vertreter der Frameworks, der dieses Entwurfsmuster nahezu durchgängig einsetzt, ist Angular. Aber auch serverseitig wird Dependency Injection (DI) beispielsweise von Nest verwendet. Stellt sich nur die Frage, warum viele andere etablierte Bibliotheken wie React dieses Entwurfsmuster komplett links liegen lassen.

Bevor wir uns im nächsten Schritt mit der konkreten Umsetzung von DI in JavaScript beschäftigen, sehen wir uns zunächst die Theorie hinter dem Entwurfsmuster an. Bei DI geht es darum Abhängigkeiten zur Laufzeit zur Verfügung zu stellen. Im Gegensatz zum Modulsystem, bei dem die einzelnen Elemente schon zum Compile- beziehungsweise Startzeitpunkt festgelegt werden, erlaubt DI eine deutlich feinere Kontrolle über die Applikation. Außerdem erleichtert DI das Testen von Objekten, indem Abhängigkeiten für die Testumgebung ausgetauscht werden. Damit DI funktioniert, stellen DI umsetzende Bibliotheken einen Mechanismus bereit, über den die injizierbaren Klassen registriert werden. Eine Klasse in der Applikation gibt dann nur noch an, welche Abhängigkeiten sie benötigt, und die Bibliothek kümmert sich darum, dass die Klasse eine Instanz der Abhängigkeit erhält. Das klingt recht abstrakt, sehen wir uns also ein konkretes Beispiel in Nest an.

DI in Nest

Nest ist ein serverseitiges JavaScript-Framework [1], das Entwickler bei der Umsetzung von Web-Schnittstellen unterstützt. Die Funktionalität wird meist in ein oder mehrere Module aufgeteilt. Jedes Modul verfügt über Controller, die die Endpunkte der Schnittstelle enthalten. Die eigentliche Businesslogik liegt in Providern, die über DI geladen werden. Für unser DI-Beispiel setzen wir eine Schnittstelle um, die die Distanz zwischen zwei GPS-Koordinaten berechnet. Die Grundlage bildet ein fachliches Modul, das alle Belange kapselt, die mit dem Thema GPS zu tun haben, in unserem Fall ist das die Distanzberechnung. Das Modul ist im weitesten Sinn ein Container für die DI, enthält aber auch die Einstiegspunkte in das Modul. Nest arbeitet, wie auch Angular im Frontend, mit TypeScript und Decorators. Ein solcher Decorator fügt Metainformationen zu einer Klasse oder Funktion hinzu. Der nachfolgende Code zeigt das Modul, in dem bereits der Controller und der Service registriert sind:

import { Module } from '@nestjs/common';
import { DistanceController } from './distance/distance.controller';
import { DistanceService } from './distance/distance.service';

@Module({
controllers: [DistanceController],
providers: [DistanceService]
})
export class GpsModule {}

Den Einstieg stellen die Controller dar. Sie sind, wie auch Module, einfache TypeScript-Klassen, die mit Dekoratoren erweitert werden. Das folgende Listing enthält den Code des Controllers:

import { Body, Controller, Get } from '@nestjs/common';
import { Point } from 'src/point.interface';
import { DistanceService } from './distance.service';

@Controller('gps/distance')
export class DistanceController {
constructor(private readonly distanceService: DistanceService) {}

@Get() distance(@Body() coordinates: Point[]): number {
const a = coordinates[0];
const b = coordinates[1];

return this.distanceService.calculate(a, b);
}
}

Die Decorators des Controllers bestimmen das Verhalten der Schnittstelle. Der Decorator der Klasse legt den URL-Pfad fest und der Decorator der distance-Methode definiert, dass diese Methode ausgeführt wird, wenn Benutzer den Pfad gps/distance mit einem HTTP GET-Request aufrufen. Über den Body-Decorator kann die Methode direkt auf den Request-Body zugreifen. Der Controller kümmert sich lediglich um die Schnittstelle, die Extraktion der Daten aus dem Request und die Validierung der eingehenden Daten. Die eigentliche Businesslogik liegt dann nicht mehr im Controller, sondern in einem Service. Der Vorteil dieser Aufteilung ist, dass die Logik getrennt vom Request- und Response-Handling getestet werden kann.

Der Austausch des Algorithmus ist vergleichsweise einfach, da der Controller selbst nicht modifiziert werden muss, sondern lediglich die injizierte Abhängigkeit angepasst wird. Den Schlüssel bildet die sogenannte Constructor Injection. Hierbei enthält der Constructor einen Parameter, der vom Injector von Nest ausgewertet wird. Erzeugt Nest beim Start der Applikation eine Instanz des Controllers, wird der Injector tätig und generiert anhand der Konfiguration des Moduls eine Instanz des angegebenen Services. Dieser steht dann innerhalb der Controller-Klasse über die distanceService-Eigenschaft zur Verfügung. Der Code des Services selbst ist relativ unspektakulär. Er berechnet anhand zweier übergebener GPS-Koordinaten die Distanz in Metern und gibt sie zurück:

import { Injectable } from '@nestjs/common';
import { Point } from 'src/point.interface';

@Injectable()
export class DistanceService {
calculate(a: Point, b: Point): number {
const earthRadius = 6371e3;
const latARad = a.lat * Math.PI/180;
const latBRad = b.lat * Math.PI/180;
const deltaLatRad = (b.lat - a.lat) * Math.PI/180;
const deltaLonRad = (b.lon - a.lon) * Math.PI/180;

const x = Math.sin(deltaLatRad/2) * Math.sin(deltaLatRad/2) + Math.cos(latARad) * Math.cos(latBRad) * Math.sin(deltaLonRad/2) * Math.sin(deltaLonRad/2);
const c = 2 * Math.atan2(Math.sqrt(x), Math.sqrt(1-x));

return earthRadius * c;
}
}

Im einfachsten Fall sind der Name des Services, den der Injector verwendet, und der Klassenname gleich. Angenommen, der Distanz-Algorithmus soll jetzt durch eine optimierte Version ersetzt werden, kann dies entweder durch ein Ersetzen der Klasse oder durch eine Konfigurationsänderung am Modul erfolgen. Der Service wird in diesem Fall nicht mehr direkt referenziert, sondern in Form eines Objekts, wie im nachfolgenden Code zu sehen ist. In diesem Fall wird die OptimizedDistanceService-Klasse verwendet:

@Module({
controllers: [DistanceController],
providers: [{
provide: DistanceService,
useClass: OptimizedDistanceService,
}]
})
export class GpsModule {}

Neben der Flexibilität zum Austausch von Algorithmen bringt die DI gerade auch beim Testen deutliche Vorteile mit sich. Auch hier werfen wir wieder einen Blick auf die konkrete Implementierung in Nest:

import { Test, TestingModule } from '@nestjs/testing';
import { DistanceController } from './distance.controller';
import { DistanceService } from './distance.service';

const newYork = { lat: 40.6943, lon: -73.9249 };
const tokyo = { lat: 35.6897, lon: 139.6922 };
const distance = 10853705.986613212;

describe('DistanceController', () => {
let controller: DistanceController;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DistanceController],
providers: [DistanceService]
}).overrideProvider(DistanceService).useValue({
calculate() { return distance; }
}).compile();

controller = module.get<DistanceController>(DistanceController);
});

it('should calculate the distance between New York and Tokyo', () => {
const result = controller.distance([newYork, tokyo]);
expect(result).toBe(distance);
});
});

Die Nest CLI legt für alle Strukturen, die mit ihr generiert werden, standardmäßig einen Test an. So auch für den DistanceController. Die Setup-Routine, die mit der beforeEach-Funktion definiert wird, sorgt dafür, dass vor jedem Test ein neues Test-Modul erzeugt wird. Hier können Entwickler alle Strukturen registrieren, die für einen Testlauf erforderlich sind. In unserem Fall sind das der DistanceController und der DistanceService. Wobei die Tests nicht den ursprünglichen Service, sondern einen speziell für den Test definierten Mock-Service nutzen.

Die Injector-Komponente von Nest weiß aufgrund dieser Konfiguration, dass bei jeder Anforderung nach der DistanceService-Abhängigkeit der unter useValue angegebene Wert verwendet werden soll. Dieser Mock-Service gibt für die calculate-Methode immer einen konstanten Wert für die Distanz zurück, unabhängig davon, wie er aufgerufen wird. Das hat den Vorteil, dass der Controller-Test nur den Controller selbst, nicht aber den Service testet. Die Konsequenz daraus ist, dass der Test auch nur wegen Fehlern im Controller fehlschlagen kann. Der Test selbst ruft die distance-Methode des Controllers mit einem Array aus Koordinatenpunkten auf, die an die Stelle des Request-Bodys treten. Da die Rückmeldung des Controllers synchron erfolgt, sind keine weiteren Maßnahmen wie das Einfügen von async/await erforderlich. Bleibt nur noch die Überprüfung des Ergebnisses mittels Aufrufs der expect-Funktion.

Alternativen zur integrierten DI

Bisher haben wir uns mit der DI von Nest nur eine integrierte Lösung angesehen. Es gibt allerdings auch Bibliotheken, die DI unabhängig vom verwendeten Framework anbieten. Diese eignen sich vor allem für individuelle Lösungen und eigene Framework-Ansätze. Ein recht populärer Vertreter dieser Art von Bibliotheken ist Inversify [2]. Die Umsetzung ist etwas aufwendiger als bei einer integrierten Lösung wie bei Nest oder Angular:

  1. Interfaces und Typen deklarieren: Inversify folgt der Empfehlung, dass die Verweise auf Abhängigkeiten nicht auf konkrete Klassen, sondern auf Abstraktionen erfolgen sollen.
  2. Dependencies angeben: Inversify arbeitet, wie auch Nest, mit Decorators. Der @injectable-Decorator kennzeichnet Klassen, die mit DI arbeiten. Der @inject-Decorator markiert die Stellen im Code, an denen die Abhängigkeiten eigensetzt werden.
  3. DI-Container erzeugen: Die Container-Instanz bringt die abstrakten Abhängigkeiten mit den konkreten Klassen zusammen.
  4. Dependencies auflösen: Die get-Methode der Container-Instanz erlaubt das Auflösen der Abhängigkeiten und somit die Arbeit mit den konkreten Objekten.

Warum haben manche Bibliotheken und Frameworks keine DI?

React ist ein prominenter Vertreter einer verbreiteten Bibliothek, die komplett auf DI verzichtet. Im Gegensatz zu Frameworks wie Nest arbeitet React viel weniger mit Klassen. Der Großteil der Komponenten in modernen React-Applikationen sind als Funktionen umgesetzt. Diese Funktionen erhalten Informationen über die sogenannten Props in Form von Argumenten. Die Props können bei der Einbindung der Komponente beeinflusst werden, was das Testen der Komponenten einfach macht. Services in der Form, wie es sie in Nest oder Angular gibt, existieren in React nicht. In den meisten Fällen greifen Entwickler für diesen Zweck auf reine Funktionen zurück, die wiederum über die übergebenen Argumente gesteuert werden.

Beim Testen geht React auch einen anderen Weg und empfiehlt, die Komponenten mehr aus der Benutzerperspektive in Form eines Blackbox-Tests zu überprüfen. Die Testumgebung rendert die Komponenten, wie sie auch in der produktiven Applikation gerendert werden, und interagiert dann wie ein Benutzer damit, löst verschiedene Events aus und überprüft deren Auswirkungen auf die gerenderten Strukturen. Aufgaben, die typischerweise von Services abstrahiert werden, wie die Kommunikation mit einer Backend-Schnittstelle, können an den Systemgrenzen durch einen Mock des HTTP-Clients überprüft werden.

Und was lernen wir daraus?

Dependency Injection ist ein Entwurfsmuster, das sich dank Frameworks wie Angular oder Nest auch in der JavaScript-Welt etabliert hat. Es eignet sich jedoch nicht in jeder Umgebung. Wo sie in klassenorientierten Applikationen ihre Stärken sehr gut ausspielen kann, ist der Einsatz von DI in einem funktionalen Ansatz eher fragwürdig. In der passenden Umgebung verwendet, erleichtert DI das Testen einzelner Einheiten deutlich und sorgt für eine Entkopplung der Strukturen, was sich wiederum förderlich beim Austausch von einzelnen Teilen der Applikation auswirkt.


URL dieses Artikels:
https://www.heise.de/-5992734

Links in diesem Artikel:
[1] https://nestjs.com/
[2] https://inversify.io/

Copyright © 2021 Heise Medien

Adblock test (Why?)

✇ Developer-Blog - Tales from the Web side

Das Adapter-Pattern in JavaScript

Von heise online — 08. März 2021 um 11:08

Das Adapter-Pattern in JavaScript

Tales from the Web side Philip Ackermann

Zugegeben, die Relevanz einiger GoF-Entwurfsmuster für JavaScript hält sich in Grenzen, wurden diese Entwurfsmuster doch ursprünglich dafür konzipiert, Rezepte für objektorientierte Programmiersprachen beziehungsweise das objektorientierte Programmierparadigma zu definieren.

Das Command-Pattern beispielsweise dient in erster Linie dazu, in der Objektorientierung Funktionen als Objekte zu behandeln und sie beispielsweise als Parameter einer anderen Funktion (bzw. im Kontext von Objekten: Methode) übergeben zu können. Dieses "Feature" ist in funktionalen Programmiersprachen bereits Teil des Programmierparadigmas: Funktionen sind hier "First-Class Citizens" und können wie Objekte behandelt werden, lassen sich beispielsweise Variablen zuweisen, einer anderen Funktion als Parameter übergeben oder ihr als Rückgabewert dienen.

Andere Entwurfsmuster hingegen wie das im Folgenden vorgestellte Adapter-Pattern ergeben durchaus auch in JavaScript Sinn. Der Einsatz des Adapter-Patterns in JavaScript ist vor allem dann in Betracht zu ziehen, wenn man es mit externem Code, sprich mit Third-Party-Bibliotheken zu tun hat.

Es war einmal ein Entwicklerteam ...

Dazu ein kurzes fiktives Beispiel: Angenommen, wir haben eine Applikation, in der an verschiedensten Stellen HTTP-Anfragen ausgeführt werden müssen. Statt selbst einen HTTP-Client zu implementieren, machen wir uns auf die Suche nach einer entsprechenden Bibliothek. Schließlich soll man das Rad nicht immer neu erfinden. Zum Glück gibt es im JavaScript-Universum recht viele Bibliotheken beziehungsweise Packages, und wir werden schnell fündig: Die Wahl fällt auf das Package request [1] (Spoiler: Unser Beispiel beginnt 2018 und wir wissen noch nicht, dass diese Wahl eine weniger gute war).

Wann immer wir in unserer Applikation nun eine HTTP-Anfrage stellen möchten, binden wir request wie folgt ein:

request('https://www.heise.de', (error, response, body) => {
// usw.
});

Insgesamt machen wir das an 32 Stellen. Und weil wir so große Fans von HTTP-Anfragen sind, erhöht sich die Anzahl schon nach wenigen Wochen auf 128. Das geht einige Zeit gut, doch nach ein paar Monaten erhalten wir schlechte Nachrichten (es ist jetzt der 30. März 2019): Mikeal Rogers [2], der Hauptentwickler hinter request, markiert die Bibliothek als "deprecated" und verkündet, dass sie zukünftig nicht mehr aktiv weiterentwickelt werde [3]. Nach einigem Hin und Her und mehrstündigen Diskussionen im Team entscheiden wir uns, unseren Code auf eine andere Bibliothek zu migrieren.

Schnell fällt die Wahl dabei auf axios [4]. Über 82.000 Stargazer bei GitHub? Das ist schon mal ein gutes Zeichen. Dass wir jetzt 128 Stellen in der eigenen Applikation anpassen müssen, nehmen wir zähneknirschend in Kauf, sind dieses Mal aber schlauer. Einer im Team bringt kleinlaut hervor, dass wir vielleicht eines der GoF-Entwurfsmuster einsetzen könnten. Er habe da neulich was in einem Buch über JavaScript gelesen. Einige der Patterns seien wohl auch in JavaScript sinnvoll. Nach anfänglicher Skepsis hören wir uns den Vorschlag an.

Das Adapter-Pattern in JavaScript

Statt externe Bibliotheken wie request oder axios direkt zu verwenden, definieren wir uns einfach eine eigene API, gegen die wir innerhalb unserer Applikation entwickeln. Im Fall der HTTP-Client-Funktionalität könnte die API also beispielsweise wie folgt aussehen (ja, in Form einer Klasse!). Die API definiert die Methoden, die Parameter, die Rückgabewerte und in unserem Fall auch, dass es sich um eine asynchrone API auf Basis von Promises (und nicht etwa auf Basis von Callbacks) handelt. In Bezug auf das Adapter-Pattern ist diese API also die "Adapter"-Komponente, die wir innerhalb unserer Applikation (als "Client") verwenden.

class HTTPClient {

constructor() {
// ...
}

async request(url, method, headers, body, config) {
return Promise.reject('Please implement');
}

async get(url, headers, body, config) {
return this.request(url, 'GET', headers, body, config)
}

async post(url, headers, body, config) {
return this.request(url, 'POST', headers, body, config)
}

// ...
}

Neben dieser internen API kommt jetzt die zweite (externe) API ins Spiel, die durch die externe Bibliothek (jetzt: axios) vorgegeben wird. Im Kontext des Adapter-Patterns handelt es sich bei dieser API um die "Adaptee"-Komponente. Das Bindeglied zwischen den beiden APIs (also zwischen "Adapter" und "Adaptee") kommt nun in Form eines "ConcreteAdapters", also einer konkreten Implementierung von HTTPClient. Für axios als Adaptee sähe eine – wohlgemerkt nur skizzierte – Implementierung wie folgt aus:

import HTTPClient from './HTTPClient';

class AxiosAdapter extends HTTPClient {

constructor(axios) {
this._axios = axios;
}

async request(url, method, headers, body, config) {
// hier Aufruf der "axios"-Bibliothek
// Adaptation der Parameter plus
// Adaptation des Rückgabewertes
}

async get(url, headers, body, config) {
// ...
}

// ...

}

Die Erzeugung der Adapter-Klasse lagern wir dabei aus, beispielsweise mit Hilfe einer Factory-Klasse (hey, noch ein GoF-Pattern!):

import axios from 'axios';
import AxiosAdapter from './AxiosAdapter';

class HTTPClientFactory {

static createHTTPClient() {
return new AxiosAdapter(axios);
}

}

Auf diese Weise können wir HTTP-Client-Instanzen in unserer Applikation wie folgt erzeugen und haben nichts direkt mit der externen API von axios zu tun:

import HTTPClientFactory from 'my-http-client';

const client = HTTPClientFactory.createHTTPClient();

Ein Jahr später ...

Knapp ein Jahr später – wir schreiben jetzt das Jahr 2020 – können wir uns glücklich schätzen, auf unseren Kollegen gehört zu haben. Mittlerweile wird unsere Klasse nicht mehr nur an 128 Stellen verwendet, sondern an 256! Diese würden wir nun wirklich nicht mehr alle ändern wollen.

Dass der Plan mit dem Adapter-Pattern aufgeht, merken wir auch, als es wieder soweit ist, die HTTP-Client-Bibliothek auszuwechseln: Nach einem Tweet [5] von Matteo Collina [6] werden wir auf die Bibliothek undici [7] aufmerksam. Zweimal so schnell wie der native HTTP-Client von Node.js soll sie sein. Das klingt sehr gut (zumal axios intern ja genau den nativen HTTP-Client verwendet). Sollten wir vielleicht wechseln? Klar, warum nicht?

Ist ja nicht viel Aufwand. Einfach eine neue Adapter-Klasse implementieren (hier wieder nur skizziert) ...

import HTTPClient from './HTTPClient';

class UndiciAdapter extends HTTPClient {

constructor(undici) {
this._undici = undici;
}

async request(url, method, headers, body, config) {
// hier Aufruf der "undici"-Bibliothek
// Adaptation der Parameter plus
// Adaptation des Rückgabewertes
}

async get(url, headers, body, config) {
// ...
}

// ...

}

... die Factory-Klasse anpassen ...

import undici from 'undici';
import UndiciAdapter from './UndiciAdapter';

class HTTPClientFactory {

static createHTTPClient() {
return new UndiciAdapter(undici);
}

}

... fertig!

Und falls wir doch wieder zur axios-Bibliothek zurückwechseln wollen, brauchen wir nur wenige Zeilen in der Factory ändern.

... und die Moral von der Geschicht'

"Hätten wir aber auch ohne Adapter-Pattern lösen können", meint einer im Team. Ja, hätten wir. Wir hätten auch alles nur mit Funktionen funktional lösen können. Das macht die Sprache JavaScript ja gerade so vielseitig. Und ja, hinter der Klassensyntax stecken keine echten Klassen, wie man sie aus Sprachen wie Java kennt. Und ja, wegen fehlender Interfaces in JavaScript sind viele GoF-Patterns umständlich zu realisieren. Dennoch kann man nicht abstreiten, dass das objektorientierte Programmierparadigma inklusive der objektorientierten Denkweise (auch der Denkweise in Patterns) unter Entwicklern sehr verbreitet ist und sich auf diese Weise schnell ein gemeinsames Verständnis vom Code schaffen lässt.

In diesem Sinne bleibt in Bezug auf die Integration externer APIs festzuhalten:

"Objektorientiert oder funktional,
Hauptsache es passt, der Rest ist egal."

(Unbekannter Entwickler im Team)


URL dieses Artikels:
https://www.heise.de/-5073865

Links in diesem Artikel:
[1] https://github.com/request/request
[2] https://twitter.com/mikeal
[3] https://github.com/request/request/issues/3142
[4] https://github.com/axios/axios
[5] https://twitter.com/matteocollina/status/1298148085210775553
[6] https://twitter.com/matteocollina
[7] https://github.com/nodejs/undici

Copyright © 2021 Heise Medien

Adblock test (Why?)

✇ Developer-Blog - Tales from the Web side

Micro Frontends – die Microservices im Frontend

Von heise online — 17. Februar 2021 um 12:13

Micro Frontends – die Microservices im Frontend

Tales from the Web side Sebastian Springer

Micro Frontends verfolgen einen ähnlichen Ansatz wie Microservices, nur in einer völlig unterschiedlichen Umgebung. Daher ergeben sich auch andere Vor- und Nachteile sowie eine veränderte Herangehensweise bei der Konzeption und Umsetzung.

Serverseitig hat sich das Architekturmuster der Microservices gerade für umfangreiche Applikationen als Alternative zu einem monolithischen Ansatz etabliert. Einen ähnlichen Zweck verfolgen Entwickler mittlerweile auch im Frontend mit Micro Frontends. Die Idee dahinter ist, einige der Vorteile von Microservices auch im Frontend nutzbar zu machen. Doch bevor wir uns der Frage widmen, ob Micro Frontends der neue Standard für die Entwicklung von Web-Frontends werden, werfen wir einen Blick auf die Microservice-Architektur.

Microservices und was sie mit dem Frontend zu tun haben

Die Idee hinter Microservices ist, ein großes Problem in mehrere Teilprobleme zu zerlegen und sie getrennt voneinander zu bewältigen. Dieser Architekturansatz geht noch einen Schritt weiter als eine gewöhnliche Modularisierung von Applikationen, da hier nicht der Schnitt an fachlichen Grenzen innerhalb einer Applikation gemacht wird, sondern das Ganze noch weiter geht und die Bereiche in einzelne, unabhängige Systeme teilt, die durch Schnittstellen miteinander verbunden sind. Die Vorteile, die durch eine solche Microservice-Architektur entstehen, sind:

  • Skalierbarkeit: Im Gegensatz zu einem Monolithen können die einzelnen Microservices unabhängig voneinander skaliert werden. Steht ein Service unter Last, werden zusätzliche Instanzen hochgefahren, alle übrigen Services bleiben von dieser Änderung unberührt.
  • Robustheit: Der Ausfall eines Services führt nicht zwangsläufig zum Ausfall des Gesamtsystems. Da die Services sauber voneinander getrennt sind, besteht die Möglichkeit, den ausgefallenen Service wieder hochzufahren oder Alternativen anzubieten.
  • Einfachheit: Die Komplexität eines kleinen Services ist deutlich geringer als die eines Monolithen, der den gesamten Funktionsumfang einer Applikation umfasst. Entwickler können so die einzelnen Services deutlich besser überblicken, als es bei einem großen zusammenhängenden Konstrukt möglich ist.
  • Flexibilität: Nicht zu vernachlässigen ist die Möglichkeit, in einer Microservice-Architektur das jeweils beste Werkzeug für eine Aufgabe auszuwählen. Da die Services unabhängig voneinander sind, spricht nichts dagegen, einen Service in Go, einen anderen in JavaScript und einen dritten beispielsweise in Kotlin umzusetzen.

Sicherlich hat eine Microservice-Architektur nicht nur ihre Vorteile. Die Komplexität in der Kommunikation zwischen den einzelnen Services steigt deutlich an. Hinzu kommt, dass es sich hierbei um Kommunikation zwischen eigenständigen Systemen handelt. Die Entwickler müssen sich also um Problemstellungen wie potenziellen Nachrichtenverlust, synchrone- und asynchrone Kommunikation kümmern. Entwickeln die einzelnen Service-Teams unabhängig voneinander, erfordert dies einen zusätzlichen Koordinationsaufwand, wenn es um die Versionierung der Services geht. Hierbei muss sichergestellt sein, dass die Services der Applikation untereinander kompatibel bleiben.

Micro Frontends verfolgen einen ähnlichen Ansatz wie Microservices, jedoch in einer völlig anderen Umgebung. Daraus ergibt sich eine andere Bewertung der Architekturform. Wo sich eine Microservice-Architektur für eine Vielzahl von Applikationen lohnt, sind die Anwendungsfälle für Micro Frontends deutlich eingeschränkter.

Doch werfen wir zunächst einen Blick auf die wesentlichen Unterschiede zwischen Frontend und Backend und wie sie sich auf die Architektur auswirken. Der Browser führt eine Applikation als einzelne Instanz aus. Im Gegensatz dazu sind Backend-Services unabhängig voneinander (man hat also mehrere Instanzen). Das wirkt sich vor allem auf die Skalierbarkeit der Applikation aus. Bei einer Lastspitze fährt die Backend-Infrastruktur zusätzliche Instanzen der betroffenen Services hoch und verteilt die Last entsprechend. Ist der Browser unter Last, bringt es zugegebenermaßen wenig, ein weiteres Browserfenster mit einer neuen Instanz der Applikation zu öffnen. Der Browser kann nur auf Worker-Prozesse zugreifen, die allerdings vom Rendering abgekapselt sind und lediglich für Berechnungen und Serverkommunikation eingesetzt werden können.

Das Skalierungsverhalten von Front- und Backend unterscheidet sich also grundlegend. Auch beim Thema Robustheit gibt es gravierende Unterschiede: Hier wird häufig das Bild von Pets vs. Cattle aufgegriffen, also der Unterschied zwischen Haustieren und Nutztieren. In einer Microservice-Architektur sind die einzelnen Instanzen wie Nutztiere, zu denen die Betreiber der Plattform ein eher distanziertes Verhältnis pflegen sollten. Stürzt ein Service ab, wird er durch eine neue Instanz ersetzt. Im Browser lässt sich eine solche Vorgehensweise nicht realisieren, denn es gibt nur den einen Hauptprozess. Stürzt dieser ab, war's das. Die Entwickler sollten ihn also mit umfangreicher Fehlerbehandlung hegen und pflegen wie ein Haustier.

Das Thema Einfachheit betrifft sowohl Front- als auch Backend. Kleinere Einheiten, die für sich stehen, sind in beiden Welten deutlich einfacher zu handhaben als ineinander verflochtene, umfangreiche Konstrukte. Die Flexibilität, die eine Microservice-Architektur im Backend hinsichtlich der Wahl der Sprachen und Technologien bietet, muss im Frontend auch wieder differenziert betrachtet werden. Im Backend können die Entwickler die für die jeweilige Problemstellung passende Kombination aus Programmiersprache, Frameworks und Bibliotheken auswählen. Der Browser schränkt diese Auswahl im Frontend deutlich ein. Neben JavaScript und zu einem geringen Teil WebAssembly lässt er keine weiteren Programmiersprachen direkt zu. Eine Ausnahme bilden hier nur Sprachen, die in JavaScript oder WebAssembly übersetzt werden, wie es mit TypeScript, CoffeeScript oder C# in Form von Blazor der Fall ist.

Bleibt also nur noch die Auswahl an Bibliotheken und Frameworks, die in diesem Bereich zugegebenermaßen sehr umfangreich ist. Besteht eine Applikation aus mehreren kleinen Teilen und sind diese mithilfe verschiedener Bibliotheken und Frameworks umgesetzt, lädt der Browser die erforderlichen Ressourcen zur Darstellung der Applikation vom Server, was einen erheblichen Overhead im Vergleich zu einer traditionell monolithisch aufgebauten Applikation bedeuten kann.

Der Aufbau einer Micro-Frontend-Applikation

Bei einer Micro-Frontend-Architektur setzt sich die Applikation aus mehreren voneinander unabhängigen Micro Frontends zusammen. Diese einzelnen Bestandteile werden in einer Integrationsschicht zusammengefügt. Das wird nötig, da es für den Browser immer ein Dokument gibt, das er initial vom Server lädt. Beim Aufbau einer Micro-Frontend-Architektur gilt Ähnliches wie bei den Microservices im Backend: Es gibt nicht den einen richtigen Weg, sie umzusetzen, vielmehr existieren zahlreiche Facetten dieser Architekturform.

Die einfachste Möglichkeit besteht darin, die einzelnen Micro Frontends über iFrames einzubetten. Diese Variante ist zwar nicht sonderlich elegant, stellt jedoch sicher, dass die einzelnen Teile der Applikation tatsächlich unabhängig voneinander sind. Weitere Möglichkeiten sind die Kapselung der Micro Frontends in Web Components oder der Einsatz von zusätzlichen Bibliotheken wie single-spa, die die friedliche Koexistenz verschiedener Frontend-Frameworks in einer Applikation erlauben.

Ein großes Problem bei der Umsetzung von Micro Frontends mit verschiedenen Bibliotheken ist die bereits erwähnte Paketgröße, da im schlechtesten Fall mehrere vollwertige Frameworks parallel ausgeliefert werden müssen. Dieses Problem lässt sich nur auf zwei Arten lösen: Entweder werden keine größeren Bibliotheken eingesetzt oder die Entwicklerteams der einzelnen Micro Frontends einigen sich auf gewisse Konventionen. So kann die Verwendung nur eines Frameworks festgelegt werden, und die Teams entwickeln dann nicht komplett eigenständige Applikationen, sondern Module, die die Integrationsschicht zusammenführt. Der Entwicklungsprozess einer solchen Architektur ist also geprägt von Kompromissen und einer Gratwanderung zwischen völliger Flexibilität und Einschränkungen durch Konventionen. Wie die Vorgehensweise konkret aussieht, hängt stark vom Einsatzzweck und den Rahmenbedingungen der Gesamtapplikation ab.

Micro Frontends oder keine Micro Frontends, das ist hier die Frage

Wie über so ziemlich jedes Thema in der Entwicklung, werden auch über Micro Frontends leidenschaftliche Diskussionen geführt. Und wie so oft sollte jeder auch dieser Architekturform seine Daseinsberechtigung zusprechen. Natürlich sind Micro Frontends nicht die Silver Bullet, die alle Probleme in der Web-Entwicklung lösen. Aber gerade für umfangreiche Applikationen mit mehreren beteiligten Teams kann eine Micro-Frontend-Architektur eine potenzielle Lösung sein.

Ein Szenario, in dem Micro Frontends ihre Stärke wirklich ausspielen, sind Migrationen. Hat eine bestehende Applikation ihren Zenit überschritten, steigen die Kosten für die Wartung und Weiterentwicklung deutlich an und für die Entwickler ist die Arbeit an einem solchen gewachsenen Stück Software kein wirkliches Vergnügen mehr. Also steht irgendwann die Frage eines Rewrites im Raum. Das Neuschreiben einer Applikation birgt jedoch beträchtliche Risiken, die bis hin zum Scheitern des Unterfangens reichen.

Eine Migration der alten Software mithilfe einer Micro-Frontend-Architektur auf die neue Version ist eine Möglichkeit, die Risiken in den Griff zu bekommen. Hierfür schaffen die Entwickler zunächst die Integrationsschicht, in die die bestehende Software eingebettet wird, sodass sie funktioniert wie bisher. Anschließend können sie sich schrittweise daran machen, die Features durch neue Versionen zu ersetzen. Die Integrationsschicht sorgt dann dafür, dass die Benutzer die neue Version erhalten und die alte Version wird abgeschaltet.

Sobald alle Features migriert sind, kann die alte Applikation gelöscht und auch die Integrationsschicht kann, falls sie nicht mehr benötigt wird, zurückgebaut werden. Der Vorteil dieser Vorgehensweise ist, dass es keinen Big-Bang-Release gibt, die Migration schleichend erfolgt und es zu keinem Stillstand bei der Entwicklung neuer Features kommen muss.

Abschließend bleibt nur noch zu sagen, dass jedes Team die Entscheidung für die Architektur seiner Applikation selbst treffen muss. Es ist dabei allerdings hilfreich, möglichst viele verschiedene Alternativen zu kennen und die jeweils beste auszuwählen. Ob es also ein sauber modularisierter Monolith oder eine Micro-Frontend-Architektur wird, hängt von der Problemstellung und den Vorlieben der Entwickler ab.


URL dieses Artikels:
https://www.heise.de/-5057255

Copyright © 2021 Heise Medien

Adblock test (Why?)

✇ Developer-Blog - Tales from the Web side

Zugriff auf die Zwischenablage: synchron und asynchron

Von heise online — 08. Februar 2021 um 11:09

Zugriff auf die Zwischenablage: synchron und asynchron

Tales from the Web side Philip Ackermann

Für die Arbeit mit der Systemzwischenablage stellt die W3C-Spezifikation "Clipboard API and events" zwei APIs zur Verfügung: die synchrone "Clipboard Event API" und die asynchrone "Asynchronous Clipboard API".

Der programmatische Zugriff auf die Zwischenablage innerhalb einer Webanwendung hat in den letzten Jahren einen Wandel durchgemacht. Noch vor ein paar Jahren griff man gerne auf Bibliotheken wie ZeroClipboard [1] zurück, die unter der Haube Flash voraussetzten. Mit dem Verschwinden von Flash in die Bedeutungslosigkeit verloren allerdings auch diese Bibliotheken an Bedeutung. Die execCommand API [2] wiederum, über die man unter anderem programmatisch Befehle wie das Kopieren oder Einfügen ausführen kann, leidet unter Interoperabilitätsproblemen, gilt mittlerweile als veraltet [3] und wird aller Voraussicht auch nicht mehr über den Status eines "Unofficial Draft" hinauskommen. Hinzu kommt, dass hierüber Daten nur aus dem DOM gelesen und auch nur in das DOM geschrieben werden können:

// Achtung: veraltet! 

// Kopieren
const source = document.querySelector("#source");
source.select();
document.execCommand("copy");

// Und Einfügen
const target = document.querySelector("#target");
target.focus();
document.execCommand("paste");

Beim W3C liegt der Fokus der Entwicklung daher auf der Spezifikation "Clipboard API and events" [4], die aktuell als Working Draft vorliegt. Sie definiert für die Arbeit mit der Systemzwischenablage zwei verschiedene APIs: die Clipboard Event API [5] (bzw. Synchronous Clipboard API) und die relativ neue Asynchronous Clipboard API [6].

Synchroner Zugriff

Die Clipboard Event API [7] bietet zum einen die Möglichkeit, sich in die gängigen Operationen auf der Zwischenablage "einzuhaken", sprich auf Events zu reagieren, die beim Ausschneiden, Kopieren und Einfügen ausgelöst werden sowie zum anderen synchron schreibend und lesend auf die Zwischenablage zuzugreifen.

Für Ersteres definiert die API vier verschiedene Events:

  • "copy": wird ausgelöst, wenn Daten in die Zwischenablage kopiert werden.
  • "paste": wird ausgelöst, wenn Daten aus der Zwischenablage eingefügt werden.
  • "cut": wird ausgelöst, wenn Daten "ausgeschnitten" und dabei in die Zwischenablage kopiert werden.
  • "clipboardchange": wird ausgelöst, wenn der Inhalt der Zwischenablage geändert wird. Dies betrifft im Übrigen auch Änderungen, die außerhalb des Browsers ausgelöst werden. In diesem Fall wird das Event genau dann ausgelöst, wenn der Browser wieder den Fokus erhält.

Auf diese Events kann man, wie in folgendem Listing gezeigt, über entsprechende Event-Listener reagieren. Der EventListener für das copy-Event definiert, was passieren soll, wenn innerhalb der Webseite eine Kopieraktion durchgeführt wird. Über die Eigenschaft clipboardData des entsprechenden Events gelangt man an ein Objekt vom Typ DataTransfer. Zur Erinnerung: Dieses Objekt repräsentiert ein Objekt, das ursprünglich für den Datentransfer bei Drag&Drop-API-Aktionen [8] zuständig ist. Über die Methode setData() können an diesem Transferobjekt die zu übertragenden Daten definiert werden, wobei der Methode als erster Parameter das Format der Daten (in Form des MIME-Typen) und als zweiter Parameter die eigentlichen Daten übergeben werden. Auf der Gegenseite (innerhalb des Event-Listeners, der für die Einfügeoperation zuständig ist) werden analog über die Methode getData() unter Angabe des MIME-Typen die Daten aus dem Transferobjekt gelesen.

document.addEventListener('copy', (event) => {
const { clipboardData } = event;
clipboardData.setData(
'text/plain',
'Dieser Text kommt synchron in die Zwischenablage.'
);
event.preventDefault();
});

document.addEventListener('paste', (event) => {
const { clipboardData } = event;
const data = clipboardData.getData('text/plain');
console.log(data);
// => "Dieser Text kommt synchron in die Zwischenablage."
event.preventDefault();
});

Grundsätzlich kann ein Transferobjekt Daten für verschiedene Formate enthalten. Auf diese Weise lassen sich in einer einzelnen Kopier- beziehungsweise Einfügeoperation die Daten direkt in mehreren Formaten übertragen. Das ist beispielsweise dann praktisch, um abhängig vom Ziel der Einfügeoperation die Daten entweder in dem einen oder dem anderen Format zu verarbeiten beziehungsweise. einzufügen. Folgendes Listing zeigt, wie auf diese Weise gleichzeitig Daten als einfacher Text, als formatierter HTML-Text und als JSON übertragen werden können (da sich die Daten nur als Zeichenkette übergeben lassen, muss im Fall von JSON entsprechend serialisiert (JSON.stringify()) und deserialisiert (JSON.parse()) werden).

document.addEventListener('copy', (event) => {
const { clipboardData } = event;
console.log(clipboardData);
clipboardData.setData(
'text/plain',
'Dieser Text kommt synchron in die Zwischenablage.'
);
clipboardData.setData(
'text/html',
'Dieser <strong>Text</strong> kommt synchron in die Zwischenablage.'
);
clipboardData.setData(
'application/json',
JSON.stringify({
message: 'Dieser Text kommt synchron in die Zwischenablage.'
}));
event.preventDefault();
});

document.addEventListener('paste', (event) => {
const { clipboardData } = event;
const data = clipboardData.getData('text/plain');
console.log(data);
// => "Dieser Text kommt synchron in die Zwischenablage."

const dataHTML = clipboardData.getData('text/html');
console.log(dataHTML);
// => "<meta charset='utf-8'>Dieser <strong>Text</strong> \
// kommt synchron in die Zwischenablage."

const dataJSON = JSON.parse(clipboardData.getData('application/json'));
console.log(dataJSON);
// => { message: "Dieser Text kommt synchron in die Zwischenablage." }
event.preventDefault();
});

Asynchroner Zugriff

Der oben beschriebene synchrone Zugriff auf die Zwischenablage eignet sich – weil die Ausführung der Webanwendung während der Kopier- beziehungsweise Einfügeoperation blockiert wird – nur für Daten, die eine überschaubare Größe haben. Möchte man dagegen komplexere oder größere Daten wie Bilddaten in die Zwischenablage kopieren oder aus der Zwischenablage in eine Webanwendung einfügen, greift man besser auf die Asynchronous Clipboard API [9] zurück. Wie der Name vermuten lässt, können Daten hierüber asynchron in die Zwischenablage geschrieben und aus ihr gelesen werden, ohne dass die entsprechenden Operationen dabei den Browser blockieren. Allerdings müssen Nutzer den Zugriff der entsprechenden Webanwendung auf die Zwischenablage erlauben, damit der Einsatz der Asynchronous Clipboard API überhaupt möglich ist.

Der Zugriff auf die API erfolgt über das Objekt navigator.clipboard, das entsprechende Methoden für das Schreiben (write()) sowie das Lesen (read()) bereitstellt. Beide Methoden liefern als Rückgabewert ein Promise-Objekt, sodass sie sich bequem sowohl über die Promise API als auch über async/await verwenden lassen.

Beim Schreiben werden der Methode write() die zu kopierenden Daten in Form eines Arrays von ClipboardItem-Objekten übergeben. Jedem einzelnen Objekt übergibt man die Daten mit einem Konfigurationsobjekt als Schlüssel/Wert-Paare, wobei der Schlüssel das Format repräsentiert und der Wert die Daten als Blob enthält:

const copy = async () => { 
try {
const data = [
new ClipboardItem(
{
'text/plain': new Blob(
['Dieser Text kommt asynchron in die Zwischenablage.'],
{ type: 'text/plain' }
)
}) ];
await navigator.clipboard.write(data);
} catch (error) {
console.error(error);
}
}

Der lesende Zugriff geschieht über die Methode read(), über die man an die entsprechenden Daten in Form eines Arrays von ClipboardItem-Objekte gelangt. Die Eigenschaft types jedes einzelnen ClipboardItem-Objekts enthält die verschiedenen Datenformate, die mit dem Objekt übertragen wurden:


const paste = async () => {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
for (const type of item.types) {
const data = await item.getType(type);
const text = await data.text();
console.log(text);
}
}
} catch (error) {
console.error(error);
}
}

Mit write() und read() können derzeit Texte und Bilder übertragen werden. Grundsätzlich sind weitere Formate denkbar, beispielsweise solche für Audio- oder Videodaten. Diese werden momentan aber in Bezug auf die Übertragung über die Zwischenablage noch nicht von allen Browsern unterstützt.

Für die Arbeit mit Texten stehen neben write() und read() noch die beiden Convenience-Methoden writeText() und readText() zur Verfügung, über die der Code von eben etwas einfacher gestaltet werden kann, weil man sich nicht mit ClipboardItem- und Blob-Objekten herumschlagen muss:

const copyText = async () => { 
try {
await navigator.clipboard.writeText(
"Dieser Text kommt asychron in die Zwischenablage."
);
} catch (error) {
console.error(error);
}
}

const pasteText = async () => {
try {
const text = await navigator.clipboard.readText();
console.log(text);
} catch (error) {
console.error(error);
}
}

Fazit

Mit der Asynchronous Clipboard API wird der Zugriff auf die Zwischenablage um einen asynchronen Kommunikationskanal erweitert. Alle "großen" Browser bieten Support für die Clipboard API an, sowohl für die Synchronous Clipboard API [10] als auch für die Asynchronous Clipboard API, wobei der Support für Erstere auch ältere Browserversionen umschließt. Wer auf Nummer sicher gehen möchte, dass die neuere asychrone Version im jeweiligen Browser zur Verfügung steht, kann dies über Feature Detection [11] anhand des navigator.clipboard-Objekts überprüfen. Alternativ kann man auf eine Polyfill-Bibliothek wie clipboard-polyfill [12] oder auf beliebte Alternativen wie clipboard.js [13] zurückgreifen.


URL dieses Artikels:
https://www.heise.de/-5048275

Links in diesem Artikel:
[1] https://github.com/zeroclipboard/zeroclipboard
[2] https://w3c.github.io/editing/docs/execCommand/
[3] https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
[4] https://w3c.github.io/clipboard-apis
[5] https://w3c.github.io/clipboard-apis/#clipboard-event-api
[6] https://w3c.github.io/clipboard-apis/#async-clipboard-api
[7] https://w3c.github.io/clipboard-apis/#clipboard-event-api
[8] https://html.spec.whatwg.org/multipage/dnd.html#datatransfer
[9] https://w3c.github.io/clipboard-apis/#async-clipboard-api
[10] https://caniuse.com/clipboard
[11] https://en.wikipedia.org/wiki/Feature_detection_(web_development)
[12] https://github.com/lgarron/clipboard-polyfill
[13] https://github.com/zenorocha/clipboard.js

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Verschlüsselung im Web mit der Web Crypto API

Von heise online — 26. Januar 2021 um 08:05

Verschlüsselung im Web mit der Web Crypto API

Tales from the Web side Sebastian Springer

Eines der zentralen Themen in der Webentwicklung ist Sicherheit. Mit der Web Crypto API bieten moderne Browser mittlerweile ein Werkzeug, mit dem Entwickler auch clientseitig Informationen signieren und verschlüsseln können.

Sicherheit wird im Web großgeschrieben. Nicht umsonst gibt es beispielsweise die Secure-Contexts-Spezifikation des W3C, nach der der Browser bestimmte Features wie Service Workers oder die Payment Request API nur aktiviert, wenn die Applikation über eine sichere Verbindung ausgeliefert wird. Doch der Schutz der Anwender geht noch weiter. Ein Browser ermöglicht den Zugriff auf zahlreiche Schnittstellen des Systems, auf dem die Applikation ausgeführt wird. Typischerweise sind das Mikrofon oder Kamera, aber auch systemseitige Push-Mitteilungen fallen hierunter. Entwickler können über JavaScript auf diese Schnittstellen zugreifen, allerdings erst, nachdem die Anwender diesen Zugriff erlaubt haben. Es ist also nicht möglich, beispielsweise das Mikrofon im Hintergrund zu aktivieren und damit nahezu jedes Endgerät in ein Spionagegerät zu verwandeln.

Eine Problemstellung blieb jedoch lange Zeit unberührt: Verschlüsselung von Informationen im Browser. Selbst für einfache Standardaufgaben wie das Hashen von Informationen mussten entweder selbst Funktionen geschrieben oder auf eine externe Bibliothek zurückgegriffen werden. Das bringt nicht nur den Nachteil von zusätzlichem Quellcode in der Applikation mit sich, der zum Client zu transferieren ist, auch ist die Ausführungsgeschwindigkeit solcher Algorithmen in JavaScript nicht so performant, als wenn sie nativ im Browser implementiert wären. Dieses Problem geht die Web Crypto API an. Diese Schnittstelle bietet eine Reihe von Funktionen, mit denen sich beispielsweise Signaturen und Verschlüsselung clientseitig umsetzen lassen.

Unterstützung

Ein Blick auf caniuse.com [1] verrät, dass sowohl Chrome als auch Firefox, Edge und Safari die Web Crypto API vollumfänglich unterstützen. Lediglich der Internet Explorer und Opera Mini machen hier noch Probleme. Wobei der Internet Explorer 11 die Web Crypto API schon unterstützt, allerdings in einer älteren Version der Spezifikation.

Nachdem die Web Crypto API fester Bestandteil des Browsers ist, können Entwickler die Funktionen der Schnittstelle direkt, also ohne Import-Statements, verwenden. Das folgende Codebeispiel zeigt, wie im Browser ein PBKDF2-Schlüssel auf Basis eines Passworts erzeugt werden kann, den die Applikation anschließend für die Generierung eines weiteren Schlüssels für die eigentliche Verschlüsselung oder Signatur nutzen kann:

(async () => {
const enc = new TextEncoder();
const pw = 'T0p5ecret!';
const key = await crypto.subtle.importKey(
'raw',
enc.encode(pw),
'PBKDF2',
false,
['deriveKey']
);

// ... work with the key
})();

Der Zugriff auf die Web Crypto API erfolgt über das window.crypto-Objekt (bzw. abgekürzt nur crypto). Die meisten Funktionen dieser Schnittstelle sind asynchron und arbeiten mit Promises und lassen sich wie im Beispiel mit async/await verwenden.

Neben der mittlerweile guten Browserunterstützung hat die Web Crypto API auch Einzug in Node.js gehalten. Seit Version 15 ist das aktuell noch experimentelle Modul Bestandteil des Node.js-Kerns (dieses neue Model "webcrypto" sollte allerdings nicht mit dem bereits bestehenden Modul "crypto" verwechselt werden). Im Gegensatz zum Browser müssen Entwickler hier die Funktionalität zunächst wie im folgenden Codebeispiel einbinden:

import {webcrypto} from 'crypto';

const enc = new TextEncoder();
const pw = 'T0p5ecret!';
const key = await webcrypto.subtle.importKey(
'raw',
enc.encode(pw),
'PBKDF2',
false,
['deriveKey']
);

console.log(key);

Ein erster Einsatzzweck für die Web Crypto API ergibt sich aus dem relativ trivialen Problem der Erzeugung von Zufallszahlen.

Zufallszahlen

Der JavaScript-Standard sieht zur Erzeugung von Zufallszahlen die Methode Math.random() vor. Sie gibt eine zufällige Fließkommazahl zwischen 0 und 1 zurück, mit der in einer JavaScript-Applikation gearbeitet werden kann. Die genaue Implementierung dieser Methode überlässt der Standard den Browserherstellern, und so gibt es von Plattform zu Plattform unterschiedliche Implementierungen. Allen gemeinsam ist jedoch, dass keine kryptographisch sichere Variante dabei ist. Das bedeutet, dass Entwickler diese Methode nicht für sicherheitskritische Algorithmen nutzen sollten. Glücklicherweise sieht die Web Crypto API für diesen Zweck die getRandomValues()-Methode vor, die dieses Problem löst und sichere Zufallszahlen generiert. Wie diese Methode in der Praxis verwendet wird, zeigt der folgende Codeblock:

const randomNumbers = new Uint32Array(1);
crypto.getRandomValues(randomNumbers);
console.log(randomNumbers[0]);

Die getRandomValues()-Methode akzeptiert ein typisiertes Integer-Array, also ein Uint8Array, Uint16Array oder Uint32Array. Je nach Größe des Arrays werden unterschiedlich viele Zufallszahlen erzeugt. In unserem Beispiel wird eine 32 Bit große Zufallszahl generiert. Diese ist kryptografisch sicher und kann entsprechend auch zur Lösung sicherheitsrelevanter Probleme verwendet werden.

Die wirklich interessanten Features der Web Crypto API verbergen sich hinter dem SubtleCrypto Interface oder konkret hinter der subtle-Eigenschaft des crypto-Objekts. In den folgenden Abschnitten werfen wir mit Signatur und Verschlüsselung einen Blick auf zwei typische Einsatzgebiete für die Web Crypto API.

Signatur

Bei Webapplikationen setzen die Entwickler digitale Signaturen normalerweise ein, um sicherzustellen, dass keine unberechtigten Dritten eine Information manipuliert wurde, die zwischen dem Sender und Empfänger ausgetauscht wird. Die Web Crypto API unterstützt eine Reihe von Algorithmen für das Signieren von Informationen. Beispiele hierfür sind RSA-PSS, AES-GCM oder der im folgenden Beispiel verwendete HMAC. Die Web Crypto API nutzt beim Signieren Schlüssel, die die Applikation entweder selbst generiert, oder einen importierten externen Schlüssel. Je nach Variante nutzen Entwickler hier entweder die generateKey()- oder die importKey()-Methode. Damit das folgende Beispiel problemlos ausführbar ist, erzeugt der Code den HMAC-Schlüssel selbst und nutzt ihn zum Signieren und zur anschließenden Überprüfung der Signatur:

(async () =>{
const message = (new TextEncoder()).encode('Hallo Welt');

// generate the key
const key = await crypto.subtle.generateKey({
name: 'HMAC',
hash: {name: 'SHA-256'}
}, false, ['sign', 'verify']);

// sign the message
const signature = await crypto.subtle.sign(
{name: 'HMAC'},
key,
message
);

// print the signature
console.log(new Uint8Array(signature));

// verify the signature
const isValid = await crypto.subtle.verify(
{name: 'HMAC'},
key,
signature,
message
);
console.log('isValid?', isValid);
})();

Der Code zeigt zwei zentrale Merkmale der Web Crypto API sehr schön:

  • Promises: Nahezu alle Methoden der Schnittstelle sind asynchron und nutzen Promises, um mit dieser Asynchronität umzugehen. Ein Vorteil dieses Paradigmas ist, dass Entwickler im Quellcode mit async/await arbeiten können und so keine zusätzlichen Callback-Funktionen implementieren müssen, die die Lesbarkeit des Quellcodes erschweren.
  • Typisierte Arrays und ArrayBuffer: Die Web Crypto API arbeitet intern häufig mit typisierten Arrays und ArrayBuffer-Objekten zum Austausch von Informationen. So akzeptieren beispielsweise die sign()- und verify()-Methoden ein Uint8Array-Objekt, das mithilfe des TextEncoders erzeugt wird. Die Rückgabe der sign()-Methode ist ein ArrayBuffer, der, in ein typisiertes Array umgewandelt, auf der Konsole angezeigt werden kann. Der Grund für den Einsatz dieser Datenstrukturen ist, dass sie zur Verarbeitung und zum Austausch von Informationen besser geeignet sind als beispielsweise Zeichenketten.

Der Quellcode im Beispiel sorgt zunächst dafür, dass die zu verschlüsselnde Zeichenkette als Uint8Array, dem Eingabeformat für die sign()- und verify()-Methode vorliegt. Anschließend erzeugt er einen neuen Schlüssel. Die generateKey()-Methode erwartet, dass die Entwickler beim Aufruf die Einsatzzwecke des Schlüssels angeben, in diesem Fall sind dies "sign" und "verify". Nutzt eine Applikation einen Schlüssel für eine Operation, die hier nicht genannt wird, wirft die JavaScript eine DOMException, die aussagt, dass der Schlüssel für diese Art Operation nicht erlaubt ist. Mit dem Schlüssel und der codierten Zeichenkette erzeugt die sign()-Methode eine Signatur und gibt sie in Form eines ArrayBuffers zurück.

Das Gegenstück zur sign()-Methode ist die verify()-Methode. Sie akzeptiert ein Konfigurationsobjekt, das beispielsweise den Namen des verwendeten Algorithmus enthält. Außerdem müssen der Schlüssel, die Signatur und die zu überprüfende Zeichenketten übergeben werden. Das Ergebnis des sign()-Aufrufs ist ein boolscher Wert, der angibt, ob die Signatur gültig ist.

Die Signatur kann eine Applikation beispielsweise nutzen, um sensible Informationen mit einem Server auszutauschen. Beide Seiten können dann überprüfen, ob die Nachrichten auf dem Weg manipuliert wurden. Voraussetzung hierfür ist, dass beide Seiten den Schlüssel der jeweils anderen Seite kennen.

Verschlüsselung

Das Verschlüsseln von Daten mithilfe der Web Crypto API funktioniert ähnlich wie das Signieren von Daten, nur dass das Ergebnis eben die verschlüsselten Informationen statt der Signatur sind. Das folgende Codebeispiel nutzt eine Kombination von Schlüsseln. Zunächst erzeugt der Quellcode einen passwortbasierten Schlüssel mit dem Verwendungszweck deriveKey, von dem anschließend ein zweiter Schlüssel abgeleitet wird. Dies bietet zum einen die Möglichkeit, für die Ver- und Entschlüsselung ein Passwort zu nutzen, und zum anderen wird die Sicherheit durch die Kombination von zwei Verschlüsselungsmechanismen zusätzlich erhöht. Die encrypt()-Methode nutzt dann den zweiten Schlüssel, um einen Text mit dem AES-GCM-Algorithmus zu verschlüsseln. Die Entschlüsselung erfolgt anschließend mit der decrypt()-Methode und ebenfalls mit dem zweiten Schlüssel.

(async () => {
const enc = new TextEncoder();
const dec = new TextDecoder();
const pw = 'T0p5ecret!';

// create the first key
const key1 = await crypto.subtle.importKey(
'raw',
enc.encode(pw),
'PBKDF2',
false,
['deriveKey']
);

// create the second key
const key2 = await crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: crypto.getRandomValues(new Uint8Array(10)),
iterations: 250000,
hash: "SHA-256",
},
key1,
{ name: "AES-GCM", length: 256 },
false,
['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(10));
const encryptedMessage = await crypto.subtle.encrypt(
{
name: "AES-GCM",
iv,
},
key2,
new TextEncoder().encode('Hallo Welt')
);

console.log('encrypted data: ', new Uint8Array(encryptedMessage));

const decryptedBuffer = await window.crypto.subtle.decrypt(
{
name: "AES-GCM",
iv: iv,
},
key2,
encryptedMessage
);
const decryptedMessage = dec.decode(decryptedBuffer);

console.log('decrypted data: ', decryptedMessage);
})();

Die encrypt()- und decrypt()-Methoden arbeiten, wie schon sign() und verify(), mit typisierten Arrays und ArrayBuffer-Objekten, um mit den Schlüsseln sowie den Ergebnissen zu arbeiten.
Ein Einsatzzweck für die clientseitige Verschlüsselung ist beispielsweise die Absicherung von Informationen, die im Client gespeichert werden. Mit der zunehmenden Umsetzung offlinefähiger Applikationen steigt auch die Menge der Daten, die im Browser unter anderem in der IndexedDB gespeichert werden. Der Nachteil dieser Speichermechanismen ist, dass sie über die Entwicklerwerkzeuge des Browsers problemlos ausgelesen werden können. Verschaffen sich also unberechtigte Dritte Zugriff zum Browser, ist es ihnen möglich, die komplette Web Storage des Browsers auszulesen. Liegen die Daten dort jedoch verschlüsselt, wird es für die Angreifer zumindest etwas aufwändiger an die Informationen zu kommen. Voraussetzung dafür ist natürlich, dass der Schlüssel, der zum Entschlüsseln verwendet wird, nicht auch im Browser gespeichert wird.

Fazit

Die Web Crypto API ist eine mittlerweile von allen wichtigen Browserherstellern unterstützte Low-Level-API zum Umgang mit Kryptographie in JavaScript. Die Schnittstelle ist nicht nur im Client, sondern auch serverseitig in Node.js verfügbar, sodass Quellcode und auch Bibliotheken auf beiden Seiten der Kommunikationsstrecke wiederverwendet werden können. Die Web Crypto API unterstützt verschiedene Anwendungsfälle von der Erzeugung kryptografisch sicherer Werte über Signatur bis hin zur Verschlüsselung von Informationen, die in Web-Applikationen zum Einsatz kommen. Der Vorteil dieser Schnittstelle ist, dass keine zusätzlichen Bibliotheken installiert werden müssen und, nachdem die Schnittstelle nativ vom Browser implementiert wird, sie auch noch verhältnismäßig performant ist.
Die Web Crypto API ist ein weiterer Schritt in Richtung sicherer Web-Applikationen, die die Daten ihrer Nutzer vor dem Zugriff Unberechtigter schützen.


URL dieses Artikels:
https://www.heise.de/-5035591

Links in diesem Artikel:
[1] https://caniuse.com

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Wo geht die Reise mit den Single-Page-Applikationen hin?

Von heise online — 18. Januar 2021 um 10:39

Wo geht die Reise mit den Single-Page-Applikationen hin?

Tales from the Web side Sebastian Springer

Das Entwicklerteam hinter React verfolgt einen interessanten Ansatz, der dafür sorgt, dass Single-Page-Applikationen nicht mehr reine Client-Monolithen sind. Die Idee ist Client und Server wieder näher zusammenzubringen und das Beste aus dieser Kombination herauszuholen.

Da erfindet Facebook einmal wieder das Backend neu. Die Rede ist vom neuesten Vorstoß von React: Server Components. Was sich auf den ersten Blick liest wie nur eine weitere Template Engine auf Basis von JavaScript, ist nur ein weiterer Schritt auf dem Weg, den das Entwicklerteam schon vor Jahren eingeschlagen hat, um Applikationen, die mit React erzeugt werden, noch performanter für die Benutzer zu machen.

Die Idee, die das React-Team verfolgt, ist jedoch nicht exklusiv für React, sondern findet sich in verschiedenen Varianten auch in den anderen großen JavaScript-Frameworks wieder. Doch worum geht es eigentlich? Die vergangenen Jahre waren in der Webentwicklung vor allem von Single Page-Applikationen (SPA) geprägt und damit einhergehend mit dem Siegeszug "großer" JavaScript-Frameworks und Bibliotheken wie Angular, Vue oder React. Eine solche SPA hat jedoch eine architekturelle Schwachstelle: Sie besteht, wie der Name schon andeutet, aus nur einer einzelnen HTML-Seite. Das bedeutet, dass auch alle benötigten Ressourcen in der Regel bereits zum Startzeitpunkt der Applikation geladen werden müssen.

Performance – ein Problem bei SPAs

Die Benutzer im Web warten nicht gerne. Man muss sich hier nur an die eigene Nase fassen: Ich rufe eine bestimmte Website auf oder lade eine Webapplikation. In den meisten Fällen habe ich ein konkretes Ziel. Das kann das Lesen eines Artikels, ein Einkauf oder die Erledigung einer Aufgabe im Arbeitsumfeld sein. Werde ich jedoch von einer weißen Seite oder einer sich vor mir schwerfällig aufbauenden Webpräsenz begrüßt, ist der gute erste Eindruck dahin und die Gegenseite muss mich ab diesem Zeitpunkt schon mit sehr guten Argumenten überzeugen, da ich mich ansonsten sehr schnell auf die Suche nach einer Alternative begebe. Das Motto lautet also "Load fast, stay fast". Dieser Ausdruck wird unter anderem von Google mitgeprägt. Und das aus gutem Grund: Die Ergebnisse, die eine Suchmaschine präsentiert, sollen zur Zufriedenheit der Benutzer sein und eine schnelle Auslieferung der Inhalte trägt zur Zufriedenheit bei. Das bedeutet, dass eine schnell ausgelieferte Applikation bessere Chancen auf eine gute Platzierung in der Ergebnisliste hat.

Doch zurück zu den SPAs. Es gibt zwei Metriken, die im Zusammenhang mit dem Laden einer Applikation von Bedeutung sind: Die Zeit, die es braucht, bis alle erforderlichen Ressourcen geladen sind, und die Zeit, bis die Benutzer in der Lage sind, mit der Applikation zu interagieren.

Optimierungspotenzial

Eine der wichtigsten Maßzahlen bei der Entwicklung einer SPA ist die sogenannte Bundlesize. Diese Größenangabe bezieht sich auf den gebauten Code der Applikation, der initial ausgeliefert wird. In der Regel werden SPAs in reinem JavaScript oder TypeScript entwickelt und mit einem Bundler wie Webpack oder Rollup gebaut. Dieser Build-Prozess umfasst eine Vielzahl einzelner Schritte. Die wichtigsten sind der Transpile-Prozess, falls TypeScript zum Einsatz kommt, das Zusammenfügen des Quellcodes sowie die Optimierung. Für React gibt es mit Create React App ein Kommandozeilenwerkzeug, das diesen Build-Prozess vorbereitet und den Code für den Produktivbetrieb optimiert.

TypeScript

Immer mehr SPAs, auch im React-Umfeld, werden mit TypeScript umgesetzt. Die Sprache fügt allerdings nicht nur ein Typsystem in JavaScript ein, sondern ist auch in der Lage, verschiedene JavaScript-Features zu emulieren. Die Entwickler einer Applikation können über eine Konfigurationsdirektive angeben, in welcher JavaScript-Version der generierte Quellcode erzeugt werden soll. Je älter die Version, desto mehr Features von modernem JavaScript müssen emuliert werden. Das bedeutet aber auch, dass die Bundlesize negativ beeinflusst wird. Das Ziel ist also, eine möglichst neue Version zu verwenden, falls dies die Browser der Benutzer zulassen. Die TypeScript-Konfiguration gibt die Version des generierten JavaScript-Quellcodes vor.

Das Modulsystem

Eine Applikation besteht aus vielen einzelnen Dateien, die sich jeweils um bestimmte Aspekte der Applikation kümmern: Komponenten, die die Struktur und das Aussehen der Applikation bestimmen, Services, die die Businesslogik der Applikation enthalten, und Hilfsklassen und -funktionen, die helfen, bestimmte Routinen auszulagern. Das JavaScript-Modulsystem verbindet die einzelnen Bestandteile der Applikation und hilft dabei, Abhängigkeiten aufzulösen. Zwar unterstützen die meisten modernen Browser das Modulsystem mittlerweile, sodass theoretisch der Quellcode auch in Einzelteilen zum Client übertragen werden kann. Dennoch ist es praktikabler, die Applikation in wenigen großen Dateien auszuliefern, da dadurch der Verbindungs-Overhead wegfällt und die verfügbaren parallelen Verbindungen des Browsers zum Server optimal ausgenutzt werden können. Der Bundler übernimmt die Aufgabe, die Dateien zusammenzuführen. Dabei löst beispielsweise Webpack nicht nur JavaScript-Abhängigkeiten auf, sondern ist auch in der Lage, andere Ressourcen wie CSS-Dateien in das Bundle zu integrieren.

Minifier

Zur weiteren Optimierung des Ladeprozesses einer Applikation wird der Quellcode mithilfe eines Minifiers umgeschrieben. Dieser Prozessschritt sorgt dafür, dass Variablennamen verkürzt werden, unnötige Whitespaces entfernt und Kommentare gelöscht werden.

Tree Shaking

Eine weitere Möglichkeit der Optimierung bieten Bundler wie Webpack mit dem sogenannten Tree Shaking. Dabei wird ungenutzter Code im Projekt während des Build-Prozesses eliminiert, sodass nur der tatsächlich genutzte Code ausgeliefert wird. Tree Shaking ist vor allem beim Einsatz von Bibliotheken interessant, von denen nur ein Teil der Funktionalität wirklich benötigt wird.

Lazy Loading

Neben dem Tree Shaking gibt es auch innerhalb einer Applikation Bestandteile, die zwar nicht überflüssig sind, jedoch nicht initial angezeigt werden müssen. Um die initiale Bundlesize weiter zu reduzieren, können Teile der Applikation dynamisch nachgeladen werden. Diese Vorgehensweise, oft als Lazy Loading bezeichnet, sorgt dafür, dass sich der ausgelieferte Quellcode weiter verkleinert und die zusätzlichen Komponenten erst geladen werden, wenn die Benutzer sie anfragen.

Lazy Loading hat vor allem beim Route Based Lazy Loading, also dem dynamischen Laden von Komponenten basierend auf Pfaden innerhalb der Applikation, den Nachteil, dass die Benutzer warten müssen, bis der Ladevorgang abgeschlossen ist, bevor ihnen die neue Ansicht präsentiert wird. An dieser Stelle gibt es die Möglichkeit, die Ressourcen bereits vorzuladen, sodass beim Pfadwechsel bereits alle Ressourcen vorhanden sind und nur noch gerendert werden müssen. Diese Verbesserungen tragen dazu bei, dass die SPA schneller zu den Benutzern ausgeliefert werden kann und die Zeitspanne bis zur Anzeige der Applikation deutlich verkürzt wird.

Server-Side Rendering

An dieser Stelle kommt jedoch ein weiteres Problem der SPAs zum Tragen: Die Applikation wird komplett über JavaScript aufgebaut. Das bedeutet, dass der Browser zunächst ein leeres div-Element, also eine leere Seite, anzeigt und die Benutzer erst etwas von der Applikation sehen, nachdem React seine Arbeit verrichtet hat und alle Komponenten der Applikation erzeugt und in das DOM eingehängt hat. Diese Phase weist ebenfalls enormes Potenzial für Verbesserungen auf: Statt eine leere Seite auszuliefern und sie dann mittels JavaScript zu befüllen, gehen die Entwickler von SPAs einen Schritt in der Geschichte der Webentwicklung zurück und überlassen dem Webserver die Arbeit, die HTML-Struktur der Applikation vorzubereiten und bereits fertig auszuliefern. Dieser Schritt hat nur noch wenig mit den traditionellen Template-Engines zu tun.

Stattdessen bietet beispielsweise React mit dem ReactDOMServer ein Objekt, das verschiedene Methoden zur Erzeugung von HTML-Strukturen auf Serverseite enthält. Diese werden dann wie traditionelle HTML-Seiten zum Browser der Benutzer gesendet. Dort übernimmt dann das Framework die Kontrolle über die HTML-Struktur. Dieser Prozess wird in React Hydration genannt. Erst wenn dieser Prozess abgeschlossen ist, können die Benutzer vollständig mit der Applikation interagieren. Der Vorteil von diesem Server-Side Rendering genannten Prozess ist, dass den Benutzern die Informationen bereits sehr früh zur Verfügung stehen und der Hydration-Prozess vergleichsweise schnell abläuft.

Die Zukunft führt Client und Server wieder enger zusammen

Die Idee, eine Applikation komplett im Client umzusetzen, hat sich also als nicht optimal herausgestellt, und so schlagen die Entwickler von React gerade einen sehr interessanten Weg ein, indem sie versuchen, Client und Server noch enger miteinander zu verknüpfen, als es bislang schon mit Server-Side Rendering der Fall ist.

Das Ergebnis sind die Server Components. Dabei handelt es sich um reguläre React-Komponenten, die allerdings nicht zum Client gesendet werden, sondern direkt auf dem Server ausgeführt werden. Sie können auf die Ressourcen des Servers zugreifen und sich so selbst mit Daten versorgen. Außerdem können sie bestimmen, welche Komponenten für die clientseitige Darstellung gerendert werden. Durch Server Components können Entwickler schon serverseitig viel tiefer in den Aufbau einer Applikation eingreifen, sodass wirklich nur noch die Strukturen zum Client gesendet werden, die dort benötigt werden. Außerdem können hier serverseitige Caching-Mechanismen eingesetzt werden, um die Auslieferung der Applikationen noch weiter zu beschleunigen.

Noch befindet sich dieses Feature in einem sehr frühen Stadium, jedoch zeigen dieses und ähnliche Features, wo die Reise für Frontend-Entwickler hingehen könnte.


URL dieses Artikels:
https://www.heise.de/-5026838

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Blog-Reboot: Neustart mit Verstärkung

Von heise online — 04. Januar 2021 um 09:14

Blog-Reboot: Neustart mit Verstärkung

Tales from the Web side Philip Ackermann

Gemeinsam macht das Bloggen noch mehr Spaß! Ab sofort gibt es wieder neue Tales from the Web side – mit Verstärkung.

Liebe Leserinnen, liebe Leser,

in letzter Zeit war es hier auf diesem Blog merklich ruhig. Was mich hauptsächlich in der Zeit, die ich normalerweise für die Arbeit am Blog einplane, beschäftigt hat, war die Arbeit an meinem neuen Buch: "Webentwicklung – Das Handbuch für Fullstack-Entwickler" [1] steht kurz vor der Fertigstellung und erscheint Ende März nächsten Jahres beim Rheinwerk Verlag. Auf rund 650 Seiten geht es hier um alle wichtigen Themen der Webentwicklung, von den drei wichtigsten Sprachen des Web – HTML, CSS und JavaScript – über Web APIs, Web Services, Datenbanken bis hin zur Versionsverwaltung mit Git und Deployment mit Docker.

Als guten Vorsatz für das nächste Jahr soll es hier auf diesem Blog aber wieder etwas regelmäßiger Beiträge geben. Und da die Arbeit an einem Blog gemeinsam noch mehr Spaß macht, freue ich mich sehr, dass mir ab sofort Sebastian Springer [2] als Verstärkung zur Seite steht. Oder anders gesagt: ab sofort werden Sebastian und ich gemeinsam diesen Blog mit Inhalt füllen.

Sebastian dürfte den meisten ohnehin schon bekannt sein. Er ist Fachbuchautor verschiedener Bücher über Node.js und React, Speaker auf Konferenzen wie der von heise Developer organisierten enterJS [3] und schreibt darüber hinaus regelmäßig Artikel für Fachmagazine wie die iX. Sebastian und ich kennen uns bereits einige Jahre (er war unter anderem Fachgutachter meines JavaScript-Profibuches [4] und meines Node.js-Kochbuchs [5], ich umgekehrt Fachgutachter seines Node.js-Handbuchs [6] und seines React-Handbuchs [7]). Darüber hinaus schreibt Sebastian gerade ein Gastkapitel für mein weiter oben genanntes Handbuch zur Webentwicklung. Insofern sind wir was die Zusammenarbeit beim Schreiben angeht, bereits seit Jahren ein eingespieltes Team.

Thematisch werden wir der Ausrichtung des Blogs treu bleiben, das heißt in erster Linie wird es auch weiterhin um alle interessanten Themen rund um die Webentwicklung gehen – einen Blick über den Tellerrand wollen wir uns natürlich hin und wieder dennoch gestatten.

Wir freuen uns auf die Zusammenarbeit und wünschen Ihnen, liebe Leserinnen und Leser, einen guten Start ins neue Jahr. Bleiben Sie gesund!

Beste Grüße,
Philip Ackermann [8] & Sebastian Springer [9]


URL dieses Artikels:
https://www.heise.de/-5000657

Links in diesem Artikel:
[1] https://www.rheinwerk-verlag.de/webentwicklung-das-Handbuch-fuer-fullstack-entwickler/
[2] https://twitter.com/basti_springer
[3] https://enterjs.de/
[4] https://www.rheinwerk-verlag.de/professionell-entwickeln-mit-javascript-design-patterns-praxistipps/
[5] https://www.rheinwerk-verlag.de/nodejs-rezepte-und-loesungen/
[6] https://www.rheinwerk-verlag.de/nodejs-das-umfassende-handbuch/
[7] https://www.rheinwerk-verlag.de/react-das-umfassende-handbuch/
[8] https://twitter.com/cleancoderocker
[9] https://twitter.com/basti_springer

Copyright © 2021 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Features von übermorgen: Worker Threads in Node.js

Von heise online — 01. Juli 2019 um 11:30

zurück zum Artikel

Seit Version 10.5 stellt Node.js sogenannte Worker Threads als experimentelles Feature zur Verfügung. Diese Blogartikel stellt das Feature kurz vor.

JavaScript-Code wird unter Node.js bekanntermaßen innerhalb eines einzigen Threads ausgeführt. In diesem Thread läuft die sogenannte Event-Loop, eine Schleife, die kontinuierlich Anfragen aus der Event-Queue prüft und Ereignisse von Ein- und Ausgabeoperationen verarbeitet.

Stellt ein Nutzer beispielsweise eine Anfrage an einen Node.js-basierten Webserver, wird innerhalb der Event-Loop zunächst geprüft, ob die Anfrage eine blockierende Ein- oder Ausgabeoperation benötigt. Ist das der Fall, wird einer von mehreren Node.js-internen Workern angestoßen (vom Prinzip her auch Threads, aber eben Node.js-intern), der die Operation ausführt. Sobald die Ein- oder Ausgabeoperation dann abgeschlossen wurde, wird man über entsprechende Callback-Funktion darüber informiert.

Das Entscheidende ist dabei, dass während der blockierenden Ein- und Ausgabeoperationen der Haupt-Thread nicht blockiert wird: Die Event-Loop läuft ununterbrochen weiter und ist somit in der Lage, eingehende Anfragen zeitnah zu bearbeiten. So weit, so gut.

Allerdings gibt es auch Fälle, die dazu führen, dass der Haupt-Thread blockiert wird, etwa durch CPU-intensive Berechnungen wie Verschlüsselung [1] und Komprimierung [2] von Daten.

Um dem wiederum entgegenzuwirken, gibt es bislang verschiedene Ansätze:

  • Computation offloading: hierbei werden komplexe Berechnungen an andere Services delegiert, beispielsweise indem entsprechende Nachrichten an einen Messaging-Broker geschickt und von anderen am Broker registrierten Services verarbeitet werden.
  • Partitioning: hierbei werden aufwändige Berechnungen in mehrere Abschnitte unterteilt, die dann nacheinander in verschiedenen Zyklen der Event-Loop abgearbeitet werden. Üblicherweise lagert man die entsprechende Berechnung in eine Funktion aus, die man dann über die Funktion setImmediate() in die "Check Phase" der Event-Loop [3] einreiht.
  • Clustering über Hintergrund-Prozesse: hierbei wird mithilfe der Funktion fork() aus dem child_process-Package die jeweilige Berechnung an einen Kindprozess delegiert, wobei für die Kommunikation zwischen Elternprozess und Kindprozess IPC (Inter-Process Communication) zum Einsatz kommt. Packages wie worker-farm [4] vereinfachen das Erstellen und Verwalten von Unterprozessen, sodass man sich nicht selbst um Aspekte wie Process Pooling und Wiederverwendung von Unterprozessen kümmern muss. Trotzdem bleiben die grundsätzlichen Nachteile von Unterprozessen bestehen: im Vergleich zu Threads sind sie speicherintensiv und langsam.

Worker Threads

Ein weiterer Lösungsansatz steht seit Node.js 10.5 in Form der sogenannten Worker Threads zur Verfügung. Durch die Worker Threads API [5] ist es möglich, JavaScript-Code im gleichen Prozess parallel zum Haupt-Thread auszuführen.

Folgende Klassen/Objekte werden durch die API bereitgestellt:

  • Worker: repräsentiert einen Worker Thread.
  • MessageChannel: repräsentiert einen Kommunikationskanal, über den Worker Threads miteinander und mit dem Eltern-Thread kommunizieren.
  • MessagePort: repräsentiert eines der Enden eines Kommunikationskanal, ergo hat jeder Kommunikationskanal zwei dieser Message Ports.
  • workerData: Datenobjekt, das an einen Worker Thread übergeben wird und dann innerhalb dessen zur Verfügung steht.
  • parentPort: innerhalb eines Worker Threads derjenige Kommunikationskanal, über den mit dem Eltern-Thread kommuniziert werden kann.

Betrachten wir zur Veranschaulichung zunächst ein Beispiel, welches noch ohne Worker Threads auskommt. Folgendes Listing zeigt eine einfache Implementierung der Gaußschen Summenformel, die für eine gegebene Zahl n die Summe der Zahlen 1 bis n berechnet.

const n = process.argv[2] || 500;

const sum = (n) => {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
return sum;
};

async function run() {
const result = sum(n);
console.log(result);
}

setInterval(() => {
console.log('Hello world');
}, 500);

run().catch((error) => console.error(error));

Während die Funktion für verhältnismäßig kleine n noch recht schnell das Ergebnis liefert, dauert die Berechnung für ein n von beispielsweise 50.000.000 schon einige Sekunden. Da die Berechnung innerhalb des Haupt-Threads ausgeführt wird, ist der für diese Zeit blockiert. Die Folge: Die Nachricht "Hello World", die über setInterval() alle 500 Millisekunden ausgegeben werden soll, wird erst ausgegeben, wenn das Ergebnis obiger Berechnung feststeht. Die Ausgabe lautet daher:

$ node start.js 50000000
1249999975000000
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world

An diesem Problem ändert sich übrigens auch nichts, wenn man die Funktion sum() asynchron implementiert. Folgender Code führt zu gleichem Ergebnis. Auch hier beginnt die Ausgabe der "Hello World"-Nachrichten erst nachdem das Ergebnis der Berechnung feststeht.

const n = process.argv[2] || 500;

const sum = async (n) => {
return new Promise((resolve, reject) => {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
resolve(sum);
});
};

async function run() {
const result = await sum(n);
console.log(result);
}

setInterval(() => {
console.log('Hello world');
}, 500);

run().catch((error) => console.error(error));

Mit Worker Threads lassen sich komplexe Berechnungen wie die im Beispiel aus dem Haupt-Thread in einen Worker Thread auslagern. Folgendes Listing zeigt den Code, der dazu auf Seiten des Haupt-Threads für das Beispiel benötigt wird. Um einen Worker Thread zu initiieren, genügt ein Aufruf des Konstruktors Worker [6], dem man einen Pfad zu derjenigen Datei übergibt, die den im Worker Thread auszuführenden Code enthält (dazu gleich mehr). Als zweiten Parameter kann zudem ein Konfigurationsobjekt [7] übergeben werden, mit Hilfe dessen sich unter anderem über die workerData-Eigenschaft Daten an den Worker Thread übergeben lassen (im Beispiel wird auf diese Weise das n übergeben).

const { Worker } = require('worker_threads');

const n = process.argv[2] || 500;

function runService(workerData) {
return new Promise((resolve, reject) => {
const worker = new Worker('./worker.js', { workerData });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0)
reject(new Error(`Worker stopped with exit code ${code}`));
});
});
}

async function run() {
const result = await runService({
n
});
console.log(result.result);
}

setInterval(() => {
console.log('Hello world');
}, 500);

run().catch((error) => console.error(error));

Den Code für den Worker Thread zeigt folgendes Listing. Über workerData steht der übergebene Parameter n zur Verfügung, das Ergebnis der (unveränderten) Funktion sum() wird nach der Berechnung über die Methode postMessage() an den Eltern-Thread gesendet.

const { workerData, parentPort } = require('worker_threads');

const sum = async (n) => {
return new Promise((resolve, reject) => {
let sum = 0;
for (let i = 0; i < n; i++) {
sum += i;
}
resolve(sum);
});
};

const { n } = workerData;
(async () => {
const result = await sum(n);
parentPort.postMessage({ result, status: 'Done' });
})();

Wie man anhand der folgenden Ausgabe des Programms sieht, geschieht die Ausgabe der "Hello World"-Nachrichten unmittelbar, während parallel vom Worker Thread das Ergebnis der sum()-Funktion berechnet wird (zu beachten: je nach Node.js-Version muss der folgende Code unter Angabe des --experimental-worker Flags gestartet werden – unter Node.js 12 funktioniert der Code allerdings auch ohne diese Angabe).

$ node start.js 50000000
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
1249999975000000
Hello world
Hello world
Hello world
Hello world
Hello world

Fazit

Worker Threads sind ein noch experimentelles Feature von Node.js, die es ermöglichen, JavaScript-Code unabhängig vom Haupt-Thread auszuführen und damit eine Blockierung des Haupt-Threads bei komplexen Berechnungen zu verhindern.


URL dieses Artikels:
http://www.heise.de/-4354189

Links in diesem Artikel:
[1] https://nodejs.org/api/crypto.html
[2] https://nodejs.org/api/zlib.html
[3] https://nodejs.org/de/docs/guides/event-loop-timers-and-nexttick/
[4] https://www.npmjs.com/package/worker-farm
[5] https://nodejs.org/api/worker_threads.html
[6] https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options
[7] https://nodejs.org/api/worker_threads.html#worker_threads_new_worker_filename_options

Copyright © 2019 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Tools & Libraries: Kommunikation mit Web-Workern

Von heise online — 06. Juni 2018 um 07:28

zurück zum Artikel

Web Worker eignen sich hervorragend dazu, komplexe Berechnungen vom Haupt-Thread einer Webanwendung auszulagern und damit die Performance einer Anwendung zu verbessern. Eine interessante Bibliothek, die die Kommunikation zwischen Worker-Threads und Haupt-Thread vereinfacht, ist Comlink [1].

Die Kommunikation zwischen Worker-Threads und Haupt-Thread erfolgt bekanntermaßen nachrichtenbasiert mit Hilfe der Methode postMessage() (auf Seiten des Senders) und Event-Listenern für das message-Event (auf Seiten des Empfängers). Möchte man Funktionen innerhalb eines Workers aus dem Haupt-Thread heraus im RPC-Stil aufrufen, müssen eventuell noch Antwortnachrichten zu den entsprechenden Anfragenachrichten zugeordnet werden, in der Regel über das Mitschleifen einer Request-ID.

Comlink

Die Bibliothek Comlink vereinfacht diese Kommunikation zwischen Haupt-Thread und Worker-Threads durch eine zusätzliche Abstraktionsschicht: Klassen, Objekte und Funktionen, die innerhalb eines Workers definiert werden, lassen sich mit Hilfe von Comlink direkt innerhalb des Haupt-Threads importieren und wie gewohnt verwenden. Beispielsweise lassen sich Objektinstanzen direkt über die importierte Klasse erzeugen und Objektmethoden direkt aufrufen, ohne dass Entwickler mit den Details der Worker-Kommunikation in Berührung kommen. Technisch realisiert Comlink dies durch Anwendung des Proxy-Patterns: für das jeweilige Objekt (bzw. die Klasse/die Funktion), das innerhalb des Worker-Codes definiert ist, erzeugt Comlink eine Proxy-Instanz im aufrufenden Code.

Dazu stellt Comlink drei Methoden zur Verfügung:

  • Comlink.expose() dient dazu, innerhalb eines Workers das Objekt zu definieren, welches nach außen hin bereitgestellt werden soll.
  • Comlink.proxy() erzeugt für eine Objektinstanz der Typen Worker, Window oder MessagePort ein Proxy-Objekt, welches die gesamte Nachrichtenkommunikation verbirgt und die API des zuvor im Worker über Comlink.expose() definierten Objekts als asynchrone API zur Verfügung stellt.
  • Comlink.proxyValue() funktioniert genau wie Comlink.proxy(), allerdings mit einem kleinen Unterschied: Während Comlink.proxy() eine Kopie des Originalobjekts erzeugt, arbeitet Comlink.proxyValue() wirklich als Proxy: ändert man das Proxy-Objekt im Haupt-Thread, ändert sich auch das ursprüngliche Objekt im Worker-Thread.

Installation und Beispiel

Comlink kann entweder direkt von der Projekt-Website heruntergeladen oder über npm installiert werden:

npm install comlinkjs

Innerhalb des Codes für den Worker definiert man anschließend beliebige Objekte (Klassen, Funktionen, etc.) wie im folgenden Listing die Klasse Calculator (die in der Praxis natürlich etwas Speicherintensiveres als bloß die Summe zweier Zahlen berechnen würde) und übergibt sie der Methode Comlink.expose(). Dies ist notwendig, damit Comlink bei der Definition mehrerer Objekte intern weiß, welches dasjenige ist, für das später das Proxy-Objekt erstellt werden soll.

importScripts("/node_modules/comlinkjs/comlink.global.min.js");
class Calculator {
sum(x = 0, y = 0) {
return x + y;
}
}
Comlink.expose(Calculator, self);

Auf Seiten des Haupt-Threads erstellt man anschließend wie gewohnt eine Instanz von Worker, übergibt diese dann aber der Methode Comlink.proxy(). Im folgenden Beispiel wird auf diese Weise ein Proxy für die im Worker definierten Klasse Calculator erzeugt. Anschließend lassen sich über await new Calculator() neue Objektinstanzen erstellen. Das await ist dabei notwendig, da die Kommunikation zwischen Haupt-Thread und Worker-Thread asynchron abläuft. Dies gilt auch für andere Aufrufe der Proxy-API wie im Beispiel der Aufruf von sum().

<!doctype html>
<script src="/node_modules/comlinkjs/comlink.global.min.js"></script>
<script>
async function init() {
const worker = new Worker('worker.js');
const Calculator = Comlink.proxy(worker);
const calculator = await new Calculator();
const result = await calculator.sum(80, 90);
console.log(result);
};
init();
</script>

Fazit

Comlink versucht die technischen Details der Nachrichtenkommunikation mit Web-Workern zu vereinfachen und ist ein interessantes Beispiel für die Anwendung des Proxy-Patterns. Comlink kann aber nicht nur bei der Kommunikation mit Web-Workern verwendet werden, sondern auch bei der Kommunikation mit anderen Browserfenstern, Frames und generell allem, was das MessagePort-Interface der Channel Messaging API [2] implementiert.


URL dieses Artikels:
http://www.heise.de/-4063928

Links in diesem Artikel:
[1] https://github.com/GoogleChromeLabs/comlink
[2] https://html.spec.whatwg.org/multipage/web-messaging.html#channel-messaging

Copyright © 2018 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Features von übermorgen: die Payment Request API

Von heise online — 19. Oktober 2017 um 11:19

zurück zum Artikel

Das Implementieren von Bestell- beziehungsweise Bezahlprozessen innerhalb von Webanwendungen kann mitunter recht komplex sein. Die sogenannte Payment Request API [1], die momentan beim W3C ausgearbeitet wird und vor etwa einem Monat zusammen mit den Payment Method Identifiers [2] als "Candidate Recommendation" veröffentlicht wurde, soll hier Abhilfe schaffen.

Die API sieht vor, den Browser als Vermittler zwischen folgenden bei einem Bestellprozess involvierten Akteuren einzusetzen: dem Zahlungsempfänger (beispielsweise einem Online-Händler), dem Zahlungssender (also dem Käufer) und demjenigen, der die Hilfsmittel für eine Zahlung bereitstellt (Kreditkarte etc.).

Folgende Typen werden durch die API bereitgestellt:

  • PaymentRequest: repräsentiert eine Anfrage für einen Bestellprozess.
  • PaymentAddress: repräsentiert eine Rechnungsadresse.
  • PaymentResponse: repräsentiert die Antwort für einen Bestellprozess.
  • PaymentRequestUpdateEvent: Event, das ausgelöst wird, wenn sich die Details einer Bestellanfrage ändern.

Browser-Support

Ob die API vom aktuellen Browser unterstützt wird, lässt sich innerhalb einer Website wie gewohnt über Feature Detection prüfen, beispielsweise durch Überprüfung auf das PaymentRequest-Objekt:

if (window.PaymentRequest) {
// Payment Request API wird unterstützt
} else {
// Payment Request API wird nicht unterstützt
}

Momentan unterstützen lediglich Chrome, Opera und Microsoft Edge die API [3], in Firefox kann die API als experimentelles Feature über das Flag "dom.payments.request.enabled" unter "about:config" aktiviert werden.

Bezahlanfragen formulieren

Um eine Bezahlanfrage zu formulieren, erstellt man eine Instanz von PaymentRequest, wobei dem Konstruktor drei Konfigurationsobjekte als Parameter übergeben werden können:

const methodData = { /* siehe Listings unten */ };
const details = { /* siehe Listings unten */ };
const options = { /* siehe Listings unten */ };
const paymentRequest = new PaymentRequest(
methodData,
details,
options
);

Über methodData lassen sich die zur Verfügung stehenden Bezahlmethoden angeben. Der Eigenschaft supportedMethods kann dabei ein Array von Kennzeichnern für unterstützte Bezahlmethoden hinterlegt werden:

// Konfiguration der Bezahlmethoden
const methodData = [
{
supportedMethods: ['basic-card'],
data: {
supportedNetworks: ['visa', 'mastercard'],
supportedTypes: ['debit', 'credit'],
}
}
];

Nähere Angaben zu der Bestellung wie etwa Identifikationsnummer einer Bestellung, die zu bestellenden Artikel, Versandkosten etc. lassen sich über das Konfigurationsobjekt details konfigurieren:

// Konfiguration der Bestelldetails
const details = {
id: 'order-123-12312',
displayItems: [
{
label: 'Summe',
amount: { currency: 'EUR', value: '25.00' },
},
{
label: 'Mehrwertsteuer',
amount: { currency: 'EUR', value: '4.75' },
},
],
shippingOptions: [
{
id: 'standard',
label: 'Standardversand',
amount: { currency: 'EUR', value: '5.00' },
selected: true,
},
{
id: 'express',
label: 'Expressversand',
amount: { currency: 'EUR', value: '11.00' },
},
],
total: {
label: 'Gesamtsumme',
amount: { currency: 'EUR', value: '34.75' },
},
};

Über das dritte Konfigurationsobjekt options lässt sich definieren, welche Informationen der Nutzer während des Bestellvorgangs eingeben muss, beispielsweise Name, E-Mail-Adresse oder Telefonnummer:

// Konfiguration der Pflichtangaben
const options = {
requestPayerEmail: true,
requestPayerName: true,
requestPayerPhone: true,
requestShipping: true,
};

Bezahlanfragen absenden

Um eine Bezahlanfrage zu starten und damit den entsprechenden Dialog zu öffnen, verwendet man die Methode show() am PaymentRequest-Objekt. Zurück gibt die Methode ein Promise-Objekt, das erfüllt wird, wenn der Nutzer den Bezahlprozess über den Dialog abschließt. Innerhalb des darauf folgenden Callbacks lässt sich dann auf die vom Nutzer eingegebenen Daten zugreifen, üblicherweise um sie zur Überprüfung an den Server zu senden (im Folgenden durch den Aufruf von verify() symbolisiert).

// Hier normalerweise Überprüfung durch Server
const verify = (paymentResponse) => Promise.resolve(true);

Der Aufruf der Methode complete() teilt dem Browser anschließend mit, dass der Bezahlprozess abgeschlossen wurde. Als Parameter lassen sich hier die Werte "success" für eine erfolgreiche Bezahlung oder "failure" bei Auftreten eines Fehlers übergeben. Dem Browser steht es laut Spezifikation dann frei, ob er eine entsprechende Meldung anzeigt oder nicht.

paymentRequest.show()
.then((paymentResponse) => {
// Zugriff auf die vom Nutzer
// eingegebenen Daten.
const {
requestId,
methodName,
details,
shipping,
shippingOption,
payerName,
payerEmail,
payerPhone
} = paymentResponse;
// verify() als imaginäre Funktion, mit der
// die Bezahlanfrage mit der Serverseite überprüft wird
verify(paymentResponse).then((success) => {
if (success) {
console.log('Bezahlung erfolgreich durchgeführt');
return paymentResponse.complete('success');
} else {
console.error('Fehler bei Bezahlung');
return paymentResponse.complete('failure');
}
});
})
.catch((error) => {
console.error('Fehler:', error);
});

Fazit

Die Payment Request API will Bestell- bzw. Bezahlprozesse innerhalb von Webanwendungen vereinfachen und vereinheitlichen. Wer die API heute schon testen möchte, kann den oben gezeigten Code in einem der zuvor genannten Browser ausführen. Anmerkungen und Verbesserungsvorschläge zur API kann man übrigens über die Issue-Seite [4] des entsprechenden GitHub-Projekts loswerden.


URL dieses Artikels:
http://www.heise.de/-3694012

Links in diesem Artikel:
[1] https://www.w3.org/TR/payment-request/
[2] https://www.w3.org/TR/payment-method-id/
[3] http://caniuse.com/#feat=payment-request
[4] https://github.com/w3c/browser-payment-api/issues

Copyright © 2017 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Features von übermorgen: die Payment Request API

19. Oktober 2017 um 11:19

zurück zum Artikel

Das Implementieren von Bestell- beziehungsweise Bezahlprozessen innerhalb von Webanwendungen kann mitunter recht komplex sein. Die sogenannte Payment Request API[1], die momentan beim W3C ausgearbeitet wird und vor etwa einem Monat zusammen mit den Payment Method Identifiers[2] als "Candidate Recommendation" veröffentlicht wurde, soll hier Abhilfe schaffen.

Die API sieht vor, den Browser als Vermittler zwischen folgenden bei einem Bestellprozess involvierten Akteuren einzusetzen: dem Zahlungsempfänger (beispielsweise einem Online-Händler), dem Zahlungssender (also dem Käufer) und demjenigen, der die Hilfsmittel für eine Zahlung bereitstellt (Kreditkarte etc.).

Folgende Typen werden durch die API bereitgestellt:

  • PaymentRequest: repräsentiert eine Anfrage für einen Bestellprozess.
  • PaymentAddress: repräsentiert eine Rechnungsadresse.
  • PaymentResponse: repräsentiert die Antwort für einen Bestellprozess.
  • PaymentRequestUpdateEvent: Event, das ausgelöst wird, wenn sich die Details einer Bestellanfrage ändern.

Browser-Support

Ob die API vom aktuellen Browser unterstützt wird, lässt sich innerhalb einer Website wie gewohnt über Feature Detection prüfen, beispielsweise durch Überprüfung auf das PaymentRequest-Objekt:

if (window.PaymentRequest) {
// Payment Request API wird unterstützt
} else {
// Payment Request API wird nicht unterstützt
}

Momentan unterstützen lediglich Chrome, Opera und Microsoft Edge die API[3], in Firefox kann die API als experimentelles Feature über das Flag "dom.payments.request.enabled" unter "about:config" aktiviert werden.

Bezahlanfragen formulieren

Um eine Bezahlanfrage zu formulieren, erstellt man eine Instanz von PaymentRequest, wobei dem Konstruktor drei Konfigurationsobjekte als Parameter übergeben werden können:

const methodData = { /* siehe Listings unten */ };
const details = { /* siehe Listings unten */ };
const options = { /* siehe Listings unten */ };
const paymentRequest = new PaymentRequest(
methodData,
details,
options
);

Über methodData lassen sich die zur Verfügung stehenden Bezahlmethoden angeben. Der Eigenschaft supportedMethods kann dabei ein Array von Kennzeichnern für unterstützte Bezahlmethoden hinterlegt werden:

// Konfiguration der Bezahlmethoden
const methodData = [
{
supportedMethods: ['basic-card'],
data: {
supportedNetworks: ['visa', 'mastercard'],
supportedTypes: ['debit', 'credit'],
}
}
];

Nähere Angaben zu der Bestellung wie etwa Identifikationsnummer einer Bestellung, die zu bestellenden Artikel, Versandkosten etc. lassen sich über das Konfigurationsobjekt details konfigurieren:

// Konfiguration der Bestelldetails
const details = {
id: 'order-123-12312',
displayItems: [
{
label: 'Summe',
amount: { currency: 'EUR', value: '25.00' },
},
{
label: 'Mehrwertsteuer',
amount: { currency: 'EUR', value: '4.75' },
},
],
shippingOptions: [
{
id: 'standard',
label: 'Standardversand',
amount: { currency: 'EUR', value: '5.00' },
selected: true,
},
{
id: 'express',
label: 'Expressversand',
amount: { currency: 'EUR', value: '11.00' },
},
],
total: {
label: 'Gesamtsumme',
amount: { currency: 'EUR', value: '34.75' },
},
};

Über das dritte Konfigurationsobjekt options lässt sich definieren, welche Informationen der Nutzer während des Bestellvorgangs eingeben muss, beispielsweise Name, E-Mail-Adresse oder Telefonnummer:

// Konfiguration der Pflichtangaben
const options = {
requestPayerEmail: true,
requestPayerName: true,
requestPayerPhone: true,
requestShipping: true,
};

Bezahlanfragen absenden

Um eine Bezahlanfrage zu starten und damit den entsprechenden Dialog zu öffnen, verwendet man die Methode show() am PaymentRequest-Objekt. Zurück gibt die Methode ein Promise-Objekt, das erfüllt wird, wenn der Nutzer den Bezahlprozess über den Dialog abschließt. Innerhalb des darauf folgenden Callbacks lässt sich dann auf die vom Nutzer eingegebenen Daten zugreifen, üblicherweise um sie zur Überprüfung an den Server zu senden (im Folgenden durch den Aufruf von verify() symbolisiert).

// Hier normalerweise Überprüfung durch Server
const verify = (paymentResponse) => Promise.resolve(true);

Der Aufruf der Methode complete() teilt dem Browser anschließend mit, dass der Bezahlprozess abgeschlossen wurde. Als Parameter lassen sich hier die Werte "success" für eine erfolgreiche Bezahlung oder "failure" bei Auftreten eines Fehlers übergeben. Dem Browser steht es laut Spezifikation dann frei, ob er eine entsprechende Meldung anzeigt oder nicht.

paymentRequest.show()
.then((paymentResponse) => {
// Zugriff auf die vom Nutzer
// eingegebenen Daten.
const {
requestId,
methodName,
details,
shipping,
shippingOption,
payerName,
payerEmail,
payerPhone
} = paymentResponse;
// verify() als imaginäre Funktion, mit der
// die Bezahlanfrage mit der Serverseite überprüft wird
verify(paymentResponse).then((success) => {
if (success) {
console.log('Bezahlung erfolgreich durchgeführt');
return paymentResponse.complete('success');
} else {
console.error('Fehler bei Bezahlung');
return paymentResponse.complete('failure');
}
});
})
.catch((error) => {
console.error('Fehler:', error);
});

Fazit

Die Payment Request API will Bestell- bzw. Bezahlprozesse innerhalb von Webanwendungen vereinfachen und vereinheitlichen. Wer die API heute schon testen möchte, kann den oben gezeigten Code in einem der zuvor genannten Browser ausführen. Anmerkungen und Verbesserungsvorschläge zur API kann man übrigens über die Issue-Seite[4] des entsprechenden GitHub-Projekts loswerden.


URL dieses Artikels:
http://www.heise.de/-3694012

Links in diesem Artikel:
[1] https://www.w3.org/TR/payment-request/
[2] https://www.w3.org/TR/payment-method-id/
[3] http://caniuse.com/#feat=payment-request
[4] https://github.com/w3c/browser-payment-api/issues

Copyright © 2017 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Multi-Package-Repositories mit Lerna

26. Mai 2017 um 09:37

zurück zum Artikel

Ein wesentliches Design-Prinzip unter Node.js ist es, den Code getreu dem Motto "small is beautiful" in möglichst kleine, wiederverwendbare Packages zu strukturieren. Bei komplexeren Projekten kann das jedoch schnell unübersichtlich werden, wenn für jedes Package ein eigenes Git-Repository zu verwalten ist. Das Tool "Lerna" verspricht Abhilfe.

Wer unter Node.js oder allgemein in JavaScript ein komplexes Projekt entwickelt und seinen Code getreu dem vorgenannten Motto in viele kleine Packages strukturiert, landet recht schnell bei 50, 100 oder noch mehr Packages. Verwaltet man jedes davon in einem eigenen Git-Repository, artet das schnell in viel Konfigurationsarbeit aus: sei es, um Abhängigkeiten untereinander zu verwalten, Build-Prozesse zu organisieren oder das Deployment zu npm zu steuern.

Aus diesen Gründen strukturieren Entwickler bekannter Frameworks wie Angular[1], React[2], Meteor[3] und Ember[4] oder bekannter Tools wie Babel[5] und Jest[6] ihren Code mittlerweile in sogenannten Monorepositories[7], kurz Monorepos oder auch Multi-Package-Repositories. Die Idee: Statt jedes Package in einem eigenen Git-Repository vorzuhalten, bündelt man zusammengehörige Packages in einem einzelnen Repository.

Die Vorteile liegen auf der Hand: Zum einen muss man sich nicht mit mehreren Git-Repositories "herumschlagen", zum anderen lässt sich der Build-Prozess für alle Module stark vereinfachen und, wie wir gleich sehen werden, auch das Deployment zur npm-Registry. Ein Tool, das hierbei hilft, ist Lerna[8].

Installation

Ursprünglich Teil von Babel[9], ist Lerna mittlerweile ein eigenständiges Node.js-Package und kann wie gewohnt über den Package Manager npm als globale Abhängigkeit installiert werden:

$ npm install -g lerna

Anschließend kann das Tool über den Befehl lerna aufgerufen werden, wobei eine Reihe verschiedener Parameter zur Verfügung stehen:

  • init: Initialisierung eines Multi-Package-Repositories
  • bootstrap: Installation aller Abhängigkeiten der Packages
  • publish: Veröffentlichen aller Packages auf npm
  • updated: Überprüfen, welche Packages seit dem letzten Release geändert wurden
  • import: Importieren eines Package aus externem Repository
  • clean: Entfernen aller node_modules-Verzeichnisse in allen Packages
  • diff: Vergleich von Packages mit vorigem Release
  • run: Ausführen eines npm-Skripts in jedem Package
  • exec: Ausführen eines Kommandozeilenbefehls in jedem Package
  • ls: Auflisten aller Module

Initialisierung und Struktur

Ein Multi-Package-Repository definiert sich im Wesentlichen durch seine Struktur und über zwei globale Konfigurationsdateien: Zum einen über eine package.json-Datei, die Meta-Informationen für alle verwalteten Packages enthält, zum anderen über eine Konfigurationsdatei namens lerna.json, die wiederum Lerna-spezifische Meta-Informationen enthält. Die einzelnen Packages wiederum werden standardmäßig in einem Unterverzeichnis mit Namen packages einsortiert, sodass die Gesamtstruktur wie folgt aussieht:

multirepo/
node_modules/
packages/
package1/
node_modules/
src/
index.js
package.json
package2/
package3/
package4/
package5/
package6/
package.json
lerna.json

Diese Struktur kann man zwar manuell erzeugen. Alternativ steht aber auch wie oben schon erwähnt der Befehl lerna init zur Verfügung, der zumindest die beiden Konfigurationsdateien automatisch generiert:

$ git init multirepo
$ cd multirepo
$ lerna init

Dadurch wird zum einen das Modul Lerna als Abhängigkeit zu der package.json-Datei hinzugefügt (sofern dort noch nicht vorhanden) und zum anderen die Konfigurationsdatei lerna.json erzeugt. Zu Anfang sieht die Datei wie folgt aus und enthält die Versionsnummer der verwendeten Lerna-Bibliothek, eine Angabe darüber, unter welchem Verzeichnis die Packages liegen sowie eine Versionsnummer, die global für alle Packages gilt. Welche weiteren Konfigurationsmöglichkeiten es hier gibt, entnimmt man am besten der offiziellen Dokumentation[10].

{
"lerna": "2.0.0-beta.38",
"packages": [
"packages/*"
],
"version": "0.0.0"
}

Die Struktur der einzelnen Packages in einem Multi-Package-Repositories unterscheidet sich nicht von einem Package, das als Single-Package-Repository verwaltet wird. Das heißt beispielsweise, dass jedes Package weiterhin über seine eigene package.json-Datei verfügt und darüber zum Beispiel auch seine eigenen Abhängigkeiten definiert.

Für Abhängigkeiten hingegen, die nur während der Entwicklung benötigt werden (Eintrag "devDependencies" in package.json) ist es in den meisten Fällen sinnvoll, diese in der globalen package.json eines Multi-Package-Repositories anzugeben.

Das hat mehrere Vorteile: zum einen ist auf diese Weise sichergestellt, dass alle Packages die gleiche Version einer verwendeten Abhängigkeit haben. Zum anderen reduzieren sich dadurch sowohl die Installationszeit als auch der verwendete Speicherplatz für die entsprechende Abhängigkeit, da sie – logisch – nur einmal für alle Packages (und nicht einmal für jedes Package) installiert wird.

Bootstrapping und Deployment

Ein ebenfalls nützlicher Befehl ist lerna bootstrap. Dieser sorgt dafür, dass die Abhängigkeiten aller Packages installiert werden. Mit anderen Worten: Lerna ruft für jedes Package den Befehl npm install aus. Aber nicht nur das. Zusätzlich werden für alle Packages im Multi-Package-Repository, die als Abhängigkeit von einem anderen Package im Repository verwendet werden, symbolische Links erzeugt, was – Node.js-Entwickler werden das bestätigen – bei der Entwicklung enorm hilfreich ist. Zu guter Letzt wird dann noch für jedes Package der Befehl npm prepublish aufgerufen, wodurch die in den package.json-Dateien definierten "prepublish"-Skripte[11] ausgeführt werden.

Nimmt einem Lerna bis hierhin schon viel Arbeit ab, wird es bezüglich Deployment beziehungsweise Publishing noch besser. Der Befehl lerna publish sorgt dafür, dass die Versionsnummer für alle Packages, die sich seit dem letzten Release geändert haben, entsprechend hochgezählt wird. Dabei kann über einen kleinen Kommandozeilendialog ausgewählt werden, ob es sich um einen "Patch", einen "Minor Change", einen "Major Change" oder um einen "Custom Change" handelt (nett: Die jeweils neue Version wird in Klammern hinter der jeweiligen Auswahl angezeigt).

Aber nicht nur das: lerna publish sorgt weiterhin dafür, dass entsprechende Tags und Commits für die neue Version in Git erzeugt und alle Packages separat bei npm publiziert werden.

Fazit

Die Bibliothek Lerna hilft bei komplexen JavaScript-Projekten, die aus mehreren zusammengehörigen Packages bestehen, den Überblick zu behalten: Sie erleichtert die Konfiguration des Build-Prozesses, die Verwaltung über Git und das Publishing zu der npm-Registry. Als Empfehlung sollte jeder Node.js-Entwickler, der mit komplexen Projekten oder einer komplexen Package-/Repository-Struktur zu "kämpfen" hat, bei Gelegenheit mal einen Blick riskieren. Eventuell lohnt sich ja der Umstieg.


URL dieses Artikels:
http://www.heise.de/-3718109

Links in diesem Artikel:
[1] https://github.com/angular/angular/tree/master/modules
[2] https://github.com/facebook/react/tree/master/packages
[3] https://github.com/meteor/meteor/tree/devel/packages
[4] https://github.com/emberjs/ember.js/tree/master/packages
[5] https://github.com/babel/babel/tree/master/packages
[6] https://github.com/facebook/jest/tree/master/packages
[7] https://github.com/babel/babel/blob/master/doc/design/monorepo.md
[8] https://github.com/lerna/lerna
[9] https://babeljs.io/
[10] https://github.com/lerna/lerna/#lernajson
[11] https://docs.npmjs.com/misc/scripts

Copyright © 2017 Heise Medien

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

Multi-Package-Repositories mit Lerna

Von heise online — 26. Mai 2017 um 09:37
heise Developer

Multi-Package-Repositories mit Lerna

Ein wesentliches Design-Prinzip unter Node.js ist es, den Code getreu dem Motto "small is beautiful" in möglichst kleine, wiederverwendbare Packages zu strukturieren. Bei komplexeren Projekten kann das jedoch schnell unübersichtlich werden, wenn für jedes Package ein eigenes Git-Repository zu verwalten ist. Das Tool "Lerna" verspricht Abhilfe.

Wer unter Node.js oder allgemein in JavaScript ein komplexes Projekt entwickelt und seinen Code getreu dem vorgenannten Motto in viele kleine Packages strukturiert, landet recht schnell bei 50, 100 oder noch mehr Packages. Verwaltet man jedes davon in einem eigenen Git-Repository, artet das schnell in viel Konfigurationsarbeit aus: sei es, um Abhängigkeiten untereinander zu verwalten, Build-Prozesse zu organisieren oder das Deployment zu npm zu steuern.

Aus diesen Gründen strukturieren Entwickler bekannter Frameworks wie Angular[1], React[2], Meteor[3] und Ember[4] oder bekannter Tools wie Babel[5] und Jest[6] ihren Code mittlerweile in sogenannten Monorepositories[7], kurz Monorepos oder auch Multi-Package-Repositories. Die Idee: Statt jedes Package in einem eigenen Git-Repository vorzuhalten, bündelt man zusammengehörige Packages in einem einzelnen Repository.

Die Vorteile liegen auf der Hand: Zum einen muss man sich nicht mit mehreren Git-Repositories "herumschlagen", zum anderen lässt sich der Build-Prozess für alle Module stark vereinfachen und, wie wir gleich sehen werden, auch das Deployment zur npm-Registry. Ein Tool, das hierbei hilft, ist Lerna[8].

Installation

Ursprünglich Teil von Babel[9], ist Lerna mittlerweile ein eigenständiges Node.js-Package und kann wie gewohnt über den Package Manager npm als globale Abhängigkeit installiert werden:

$ npm install -g lerna

Anschließend kann das Tool über den Befehl lerna aufgerufen werden, wobei eine Reihe verschiedener Parameter zur Verfügung stehen:

  • init: Initialisierung eines Multi-Package-Repositories
  • bootstrap: Installation aller Abhängigkeiten der Packages
  • publish: Veröffentlichen aller Packages auf npm
  • updated: Überprüfen, welche Packages seit dem letzten Release geändert wurden
  • import: Importieren eines Package aus externem Repository
  • clean: Entfernen aller node_modules-Verzeichnisse in allen Packages
  • diff: Vergleich von Packages mit vorigem Release
  • run: Ausführen eines npm-Skripts in jedem Package
  • exec: Ausführen eines Kommandozeilenbefehls in jedem Package
  • ls: Auflisten aller Module

Initialisierung und Struktur

Ein Multi-Package-Repository definiert sich im Wesentlichen durch seine Struktur und über zwei globale Konfigurationsdateien: Zum einen über eine package.json-Datei, die Meta-Informationen für alle verwalteten Packages enthält, zum anderen über eine Konfigurationsdatei namens lerna.json, die wiederum Lerna-spezifische Meta-Informationen enthält. Die einzelnen Packages wiederum werden standardmäßig in einem Unterverzeichnis mit Namen packages einsortiert, sodass die Gesamtstruktur wie folgt aussieht:

multirepo/
node_modules/
packages/
package1/
node_modules/
src/
index.js
package.json
package2/
package3/
package4/
package5/
package6/
package.json
lerna.json

Diese Struktur kann man zwar manuell erzeugen. Alternativ steht aber auch wie oben schon erwähnt der Befehl lerna init zur Verfügung, der zumindest die beiden Konfigurationsdateien automatisch generiert:

$ git init multirepo
$ cd multirepo
$ lerna init

Dadurch wird zum einen das Modul Lerna als Abhängigkeit zu der package.json-Datei hinzugefügt (sofern dort noch nicht vorhanden) und zum anderen die Konfigurationsdatei lerna.json erzeugt. Zu Anfang sieht die Datei wie folgt aus und enthält die Versionsnummer der verwendeten Lerna-Bibliothek, eine Angabe darüber, unter welchem Verzeichnis die Packages liegen sowie eine Versionsnummer, die global für alle Packages gilt. Welche weiteren Konfigurationsmöglichkeiten es hier gibt, entnimmt man am besten der offiziellen Dokumentation[10].

{
"lerna": "2.0.0-beta.38",
"packages": [
"packages/*"
],
"version": "0.0.0"
}

Die Struktur der einzelnen Packages in einem Multi-Package-Repositories unterscheidet sich nicht von einem Package, das als Single-Package-Repository verwaltet wird. Das heißt beispielsweise, dass jedes Package weiterhin über seine eigene package.json-Datei verfügt und darüber zum Beispiel auch seine eigenen Abhängigkeiten definiert.

Für Abhängigkeiten hingegen, die nur während der Entwicklung benötigt werden (Eintrag "devDependencies" in package.json) ist es in den meisten Fällen sinnvoll, diese in der globalen package.json eines Multi-Package-Repositories anzugeben.

Das hat mehrere Vorteile: zum einen ist auf diese Weise sichergestellt, dass alle Packages die gleiche Version einer verwendeten Abhängigkeit haben. Zum anderen reduzieren sich dadurch sowohl die Installationszeit als auch der verwendete Speicherplatz für die entsprechende Abhängigkeit, da sie – logisch – nur einmal für alle Packages (und nicht einmal für jedes Package) installiert wird.

Bootstrapping und Deployment

Ein ebenfalls nützlicher Befehl ist lerna bootstrap. Dieser sorgt dafür, dass die Abhängigkeiten aller Packages installiert werden. Mit anderen Worten: Lerna ruft für jedes Package den Befehl npm install aus. Aber nicht nur das. Zusätzlich werden für alle Packages im Multi-Package-Repository, die als Abhängigkeit von einem anderen Package im Repository verwendet werden, symbolische Links erzeugt, was – Node.js-Entwickler werden das bestätigen – bei der Entwicklung enorm hilfreich ist. Zu guter Letzt wird dann noch für jedes Package der Befehl npm prepublish aufgerufen, wodurch die in den package.json-Dateien definierten "prepublish"-Skripte[11] ausgeführt werden.

Nimmt einem Lerna bis hierhin schon viel Arbeit ab, wird es bezüglich Deployment beziehungsweise Publishing noch besser. Der Befehl lerna publish sorgt dafür, dass die Versionsnummer für alle Packages, die sich seit dem letzten Release geändert haben, entsprechend hochgezählt wird. Dabei kann über einen kleinen Kommandozeilendialog ausgewählt werden, ob es sich um einen "Patch", einen "Minor Change", einen "Major Change" oder um einen "Custom Change" handelt (nett: Die jeweils neue Version wird in Klammern hinter der jeweiligen Auswahl angezeigt).

Aber nicht nur das: lerna publish sorgt weiterhin dafür, dass entsprechende Tags und Commits für die neue Version in Git erzeugt und alle Packages separat bei npm publiziert werden.

Fazit

Die Bibliothek Lerna hilft bei komplexen JavaScript-Projekten, die aus mehreren zusammengehörigen Packages bestehen, den Überblick zu behalten: Sie erleichtert die Konfiguration des Build-Prozesses, die Verwaltung über Git und das Publishing zu der npm-Registry. Als Empfehlung sollte jeder Node.js-Entwickler, der mit komplexen Projekten oder einer komplexen Package-/Repository-Struktur zu "kämpfen" hat, bei Gelegenheit mal einen Blick riskieren. Eventuell lohnt sich ja der Umstieg.


URL dieses Artikels:
http://www.heise.de/-3718109

Links in diesem Artikel:
[1] https://github.com/angular/angular/tree/master/modules
[2] https://github.com/facebook/react/tree/master/packages
[3] https://github.com/meteor/meteor/tree/devel/packages
[4] https://github.com/emberjs/ember.js/tree/master/packages
[5] https://github.com/babel/babel/tree/master/packages
[6] https://github.com/facebook/jest/tree/master/packages
[7] https://github.com/babel/babel/blob/master/doc/design/monorepo.md
[8] https://github.com/lerna/lerna
[9] https://babeljs.io/
[10] https://github.com/lerna/lerna/#lernajson
[11] https://docs.npmjs.com/misc/scripts

Let's block ads! (Why?)

✇ Developer-Blog - Tales from the Web side

21. März 2017 um 03:50

[unable to retrieve full-text content]

✇ Developer-Blog - Tales from the Web side

Features von übermorgen: Die Web Share API und die Web Share Target API

Von heise online — 06. Dezember 2016 um 07:02
heise Developer

Bislang gibt es für das Teilen von Inhalten zwischen mobilen Webanwendungen keinen einheitlichen Ansatz. Doch dies könnte sich bald ändern, denn bei der Web Incubator Community Group [1] macht man sich bereits Gedanken über entsprechende Web APIs: Über die Web Share API [2] soll das Teilen von Inhalten möglich sein, über die Web Share Target API [3] das Empfangen von Inhalten.

Beide genannten APIs befinden sich derzeit in einem noch frühen Stadium und werden noch in keiner offiziellen Version der großen Browser unterstützt. Allerdings lässt sich zumindest die Web Share API über den sogenannten Origin Trial von Google testen (der im Allgemeinen dem Chrome-Entwicklerteam dazu dient, Entwickler-Feedback bei der Implementierung neuer APIs zu bekommen). Registriert man sich dort, erhält man ein spezielles Token, nach dessen Einbau in die eigene Webanwendung die API auf Android-Geräten über Chrome Beta [4] geprüft werden kann:

Offiziell soll die Web Share API dann mit Version 55 Einzug in Chrome erhalten.

Die Web Share API

Wie bereits gesagt ist das Ziel der Web Share API [5], einen einheitlichen Weg zu schaffen, über den Inhalte (im folgenden "Ressourcen" genannt) zwischen mobilen Anwendungen geteilt werden können. Die API ist in seiner aktuellen Form relativ übersichtlich, zumal nur die beiden Interfaces Navigator und WorkerNavigator um eine Methode share() erweitert werden sollen. Feature Detection könnte daher beispielsweise wie folgt aussehen:

if(navigator.share === undefined) {
console.error('Web Share API wird nicht unterstützt.');
} else {
console.log('Web Share API wird unterstützt.');
}

Als Parameter erwartet die Methode share() ein Objekt vom Typ ShareData, das wiederum drei Eigenschaften haben kann:

  • title: Titel der Ressource, die geteilt wird
  • text: Text, über den sich eine zusätzliche Beschreibung der Ressource angeben lässt
  • url: URL zu der Ressource, die geteilt wird

Außerdem ist geplant, auch die Angabe von Bilddaten beziehungsweise Blobs zu ermöglichen. Alle Eigenschaften sind optional, allerdings muss mindestens eine im Objekt enthalten sein.

let shareData =
{
title: document.title,
text: 'Die erste mit der Web Share API geteilte Ressource.',
url: window.location.href
}

Als Rückgabewert erhält man von der Methode share() ein Promise-Objekt, auf dem sich wie gewohnt Callback-Funktionen definieren lassen:

navigator
.share(shareData)
.then(() => {
console.log('Erfolgreich geteilt');
})
.catch(error => {
console.log('Fehler beim Teilen: ', error)
});

Die über then() angegebene Callback-Funktion wird aufgerufen, wenn der Nutzer eine Anwendung (bzw. "Share Target") ausgewählt hat, zu der die Ressource geteilt werden soll und die Ressource von dieser Anwendung ohne Fehler akzeptiert wurde. Im Fehlerfall wird entsprechend die über catch() definierte Callback-Funktion aufgerufen, wobei Fehler beispielsweise dann auftreten, wenn

  1. die geteilte Ressource beziehungsweise im ShareData-Objekt übermittelte Daten fehlerhaft sind,
  2. keine passende Anwendung gefunden wurde, die die Ressource entgegennimmt,
  3. der Nutzer keine Anwendung auswählt beziehungsweise die Auswahl abbricht oder
  4. die Ressource nicht erfolgreich zu der ausgewählten Anwendung übermittelt werden konnte.

"Share Targets" können zum einen vorhandene Dienste wie die Zwischenablage sein, zum anderen native Anwendungen wie Facebook oder Twitter, aber auch andere Webanwendungen, und zwar solche, die sich über die Web Share Target API für das Empfangen geteilter Inhalte registriert haben.

Die Web Share Target API

Während die Web Share API es ermöglichen soll, Inhalte zu teilen, soll es über die Web Share Target API [15] möglich sein, (von anderen Anwendungen) geteilte Inhalte entgegenzunehmen. Als Voraussetzung muss der jeweilige Browser dabei sowohl die Service Worker API [16] als auch die Web App Manifest API [17] unterstützen.

Eine Anwendung, die geteilte Inhalte entgegennehmen soll (wie gesagt auch "Share Target" genannt), registriert sich entweder über die Methode addEventListener() oder über den Event-Handler onshare für das share-Event:

navigator.actions.addEventListener('share', handler);

Empfangene Events sind vom Type ShareEvent und ermöglichen wiederum den Zugriff auf das jeweilige ShareData-Objekt:

const handler = event => {
let shareData = event.data;
console.log(shareData.title);
console.log(shareData.text);
console.log(shareData.url);
}

Die Daten innerhalb des SharedData-Objekts können je nach Zielanwendung unterschiedliche Verwendung finden, beispielsweise ließe sich bei einem E-Mail-Client als "Share Target" der Inhalt der Eigenschaft title als Betreff der E-Mail und die Kombination aus text und url als Inhalt der E-Mail verwenden, während bei einem Text Messenger der Inhalt von title ignoriert und dagegen nur eine Kombination aus text und url verwendet werden könnte:

const emailHandler = event => {
let data = event.data;
let subject = data.title;
let content = `${data.text} ${data.url}`;
composeEmail(subject, content);
}
const textMessengerHandler = event => {
let content = `${data.text} ${data.url}`;
composeMessage(content);
}

Fazit

Die Web Share API und die Web Share Target API definieren Schnittstellen zum Teilen von Inhalten zwischen mobilen Anwendungen. Derzeit werden beide APIs noch von keinem Browser unterstützt, die Web Share API lässt sich aber wie geschildert in Chrome Beta unter Android testen.


URL dieses Artikels:
https://www.heise.de/developer/artikel/Features-von-uebermorgen-Die-Web-Share-API-und-die-Web-Share-Target-API-3506197.html

Links in diesem Artikel:
  [1] https://wicg.github.io/admin/charter.html
  [2] https://github.com/WICG/web-share/blob/master/docs/interface.md
  [3] https://github.com/mgiuca/web-share-target/blob/master/docs/interface.md
  [4] https://play.google.com/store/apps/details?id=com.chrome.beta
  [5] https://github.com/mgiuca/web-share
  [6] https://www.heise.de/developer/artikel/Features-von-uebermorgen-Async-Cookies-API-3357752.html
  [7] https://www.heise.de/developer/artikel/Features-von-uebermorgen-Font-Loading-API-3278867.html
  [8] https://www.heise.de/developer/artikel/Features-von-uebermorgen-die-Web-Bluetooth-API-3167796.html
  [9] https://www.heise.de/developer/artikel/Features-von-uebermorgen-ES2016-3089503.html
  [10] https://www.heise.de/developer/artikel/Features-von-uebermorgen-CSS3-und-runde-Displays-2878394.html
  [11] https://www.heise.de/developer/artikel/Features-von-uebermorgen-ES7-Observer-2777709.html
  [12] https://www.heise.de/developer/artikel/Features-von-uebermorgen-ES7-Decorators-2633730.html
  [13] https://www.heise.de/developer/artikel/Features-von-uebermorgen-Natives-MVC-in-HTML6-2585841.html
  [14] https://www.heise.de/developer/artikel/Features-von-uebermorgen-ES7-Async-Functions-2561894.html
  [15] https://github.com/WICG/web-share-target
  [16] https://www.w3.org/TR/service-workers/
  [17] https://www.w3.org/TR/appmanifest/

Let's block ads! (Why?)

❌