Computerhaus Quickborn

PWA-Tutorial: Background Sync per Service Worker​

Abbrechende Netzwerkverbindungen stellen für Progressive Web Apps kein Hindernis dar – Service Workers sei Dank. Foto: Pasuwan | shutterstock.com Progressive Web Apps (PWAs) sind ein wichtiges Konzept im Bereich Web Development, das die universelle Bereitstellung per Webbrowser mit der Feature-Vielfalt nativer Software verbindet. Eine besonders erwähnenswerte Eigenschaft progressiver Webanwendungen ist ihre Offline-Processing-Fähigkeit. Diese kommt beispielsweise zum Tragen, wenn ein Benutzer eine E-Mail übermittelt, aber keine Netzwerkverbindung verfügbar ist, um diese zu verarbeiten. In diesem Tutorial lesen Sie, wie das funktioniert – und lernen in diesem Zuge mehrere wichtige Komponenten einer PWA kennen. Nämlich: Service Worker, Synchronisierung, Sync Events sowie IndexedDB. Den Code für das PWA-Beispiel in diesem Artikel finden Sie auf GitHub. Service-Worker-Grundlagen Im Zusammenhang mit Progressive Web Apps wird jede Menge Hirnschmalz investiert, um deren Verhalten möglichst nahe an das von nativen Anwendungen anzugleichen. Dabei spielen Service Worker einer ganz wesentliche Rolle. Im Grunde handelt es sich bei einem Service Worker um eine eingeschränkte Variante eines Worker Thread, der mit dem Main Browser Thread ausschließlich über Event Messages kommuniziert – und keinen DOM-Zugriff hat. Dabei stellt der Service Worker eine Art eigener Umgebung dar (wie wir gleich sehen werden). Trotz ihrer Limitationen sind Service Worker relativ leistungsfähig, da sie einen eigenen, vom Main Thread unabhängigen Lebenszyklus haben – der eine Vielzahl von Hintergrundoperationen ermöglicht. In unserem Fall geht es dabei in erster Linie um die Sync API, die den Service Worker dazu befähigt, den Zustand der Netzwerkverbindung zu beobachten – und einen Netzwerk-Call solange zu widerholen, bis dieser erfolgreich ist. Sync-API und -Events Stellen Sie sich vor, Sie möchten einen “Retry”-Mechanismus konfigurieren, der Folgendes beinhaltet: Wenn das Netzwerk verfügbar ist, werden Request direkt übermittelt. Wenn das Netzwerk nicht verfügbar ist, erfolgt der nächste Übermittlungsversuch, sobald es wieder verfügbar ist. Sobald ein Retry fehlschlägt, kommt beim nächsten Versuch ein exponentieller Back-Off zur Anwendung und die Settings werden verworfen. Das in der Praxis umzusetzen, würde eine Menge granularer Arbeit nach sich ziehen. Glücklicherweise verfügen Service Worker für exakt diesen Zweck über ein spezielles Sync Event. Service Worker werden mit navigator.serviceWorker registriert. Dieses Objekt ist nur in einem sicheren Kontext verfügbar. Die Webseite muss also über HTTPS geladen werden. Unsere Beispielanwendung für diesen Artikel steht in der Tradition der kanonischen TODO-Sample-App. Nun werfen wir einen Blick darauf, wie sich ein neues To-Do mit Hilfe des sync-Events eines Service Workers abbilden lässt. Service-Worker-Synchronisierung einrichten Im Folgenden erörtern wir, wie Sie den gesamten Lebenszyklus einer Progressive Web App managen können, die Prozesse beinhaltet, die synchronisiert werden müssen. Dazu benötigen wir ein Full-Stack-Setup, in dem Requests für ein neues TODO dem eben beschriebenen Retry-Mechanismus folgen. Um das Frontend zu bedienen und den API Request zu händeln, nutzen wir Node und Express. Vorausgesetzt, npm ist installiert, starten Sie eine neue App mit folgendem Call: $ npm init -y Das erzeugt das Gerüst für eine neue Applikation. Im nächsten Schritt, gilt es, die express-Dependency hinzuzufügen: $ npm install express Nun können Sie einen einfachen Express-Server erstellen, der statische Dateien bereitstellt. Anschließend erstellen Sie eine neue index.js-Datei im Root-Verzeichnis und fügen dort Folgendes ein: const express = require('express');const path = require('path'); // Required for serving static filesconst app = express();const port = process.env.PORT || 3000; // default to 3000app.use(express.static(path.join(__dirname, 'public')));// Serve the main HTML file for all unmatched routes (catch-all)app.get('*', (req, res) => {res.sendFile(path.join(__dirname, 'public', 'index.html'));});app.listen(port, () => {console.log(`Server listening on port ${port}`);}); Wenn Sie diese Anwendung starten, wird sie alles bedienen, was sich in /public befindet. Um den Ausführungsprozess zu vereinfachen, öffnen Sie die package.json-Datei, die npm erstellt hat und fügen ein Startskript hinzu: "scripts": {"start": "node index.js","test": "echo "Error: no test specified" && exit 1"} Über die Befehlszeile können Sie die App nun starten: $ npm run start Bevor diese Anwendung etwas “tut”, müssen Sie eine neue index.html-Datei in das /public-Verzeichnis einfügen: PWA InfoWorldMy To-Do ListAdd Wenn Sie nun localhost:3000 aufrufen, erhalten Sie das grundlegende HTML-Layout – inklusive Titel, Input Box und Schaltfläche. Dabei ist zu beachten, dass eine Interaktion mit letztgenanntem Button den Wert eines neuen Task-Inputs übernimmt und diesen an die addTask()-Funktion übergibt. Das Hauptskript addTask() wird über script.js bereitgestellt. Sie können den Inhalt dieser Datei einfügen:if ('serviceWorker' in navigator) { // 1window.addEventListener('load', () => { // 2navigator.serviceWorker.register('sw.js') // 3.then(registration => { // 4console.log('Service Worker registered', registration); // 5}).catch(err => console.error('Service Worker registration failed', err)); // 6});}const taskChannel = new BroadcastChannel('task-channel'); // 7function addTask(task) { // 8taskChannel.postMessage({ type: 'add-task', data: task }); // 9} Diesen Code haben wir mit nummerierten Kommentaren ausgestattet, die erklären, was die einzelnen Zeilen “tun”: Überprüft, ob serviceWorker auf navigator vorhanden ist. Das ist nur der Fall, wenn ein sicherer Kontext besteht. Fügt einen Callback zum Load Observer hinzu, falls der serviceWorker vorhanden ist, damit dieser reagiert, wenn die Seite geladen wird. Nutzt die register-Methode, um die Datei sw.js als Service Worker zu laden. Nachdem sw.js geladen ist, folgt ein Callback mit dem Registrierungsobjekt. Das Registrierungsobjekt kann genutzt werden, um verschiedene Tasks auszuführen – in unserem Fall wird ausschließlich der Erfolg geloggt. Protokolliert sämtliche Fehler mit Hilfe des catch() promise-Callbacks. Erstellt einen BroadcastChannel namens “task-channel“. Das ist ein simpler Weg, um Ereignisse an den Service Worker zu übermitteln, der auf dem Codes in sw.js basiert. Die addTask()-Funktion wird von der HTML-Datei aufgerufen. Sendet eine Nachricht auf dem task-channel, definiert den Type als “add-task” sowie das Datenfeld als Task an sich. In diesem Beispiel ignorieren wir, wie das User Interface die Task Creation händeln würde. Wir könnten auch verschiedene andere Ansätze verwenden – beispielsweise einen optimistischen, bei dem wir den Task in die UI-Liste einfügen und anschließend versuchen, mit dem Backend zu synchronisieren. Alternativ wäre es auch möglich, zuerst einen Backend-Synchronisierungsversuch zu starten und im Erfolgsfall eine Nachricht an die Benutzeroberfläche zu senden, um einen Task hinzuzufügen. Der BroadcastChannel erleichtert es dabei, Nachrichten in beide Richtungen zu senden: vom Hauptthread zum Service Worker oder umgekehrt. Wie bereits eingangs erwähnt, ist im Rahmen einer Sicherheitseinschränkung eine HTTPS-Verbindung notwendig, damit serviceWorker auf navigator existieren kann. Um diese mit minimalem Aufwand herzustellen, haben wir in diesem Beispiel ngrok verwendet. Dieses praktische Befehlszeilen-Tool öffnet Ihre lokale Umgebung für die Außenwelt – ohne Konfiguration und inklusive HTTPS. Starten Sie etwa die Sample App ($ npm run start) und lassen den Befehl $ ngrok http 3000 folgen, erzeugt das einen Tunnel und sorgt dafür, dass die HTTP- und HTTPS-Endpunkte angezeigt werden. Damit können Sie die URL-Leiste Ihres Browser füttern. Zum Beispiel: Forwarding https://8041-35-223-70-178.ngrok-free.app -> http://localhost:3000 Nun können Sie die App über eine HTTPS-Verbindung unter https://8041-35-223-70-178.ngrok-free.app aufrufen. Mit dem Service Worker interagieren Die sw.js-Datei (die wir zuvor über den Browser mit serviceWorker geladen haben) dient dazu, mit dem Service Worker zu interagieren. Für das folgende Beispiel nutzen wie IndexedDB. Der Grund: Es steht dem Browser “frei”, Service-Worker-Kontexte nach Belieben zu erstellen oder zu verwerfen, um Ereignisse zu behandeln. Es gibt also keine Garantie dafür, dass derselbe Kontext verwendet wird, um den Broadcast- und den Snyc-Event zu behandeln. Lokale Variablen scheiden insofern als verlässliches Medium aus – LocalStorage ist für Service Worker nicht verfügbar. Sie könnten zwar CacheStorage verwenden (das sowohl in Main- als auch in Service Threads verfügbar ist) – das ist allerdings eigentlich dafür gedacht, Antworten auf Requests zwischenzuspeichern. Das führt uns letztlich zu IndexedDB, das in allen Service-Worker-Instanzen “lebt”. Wir nutzen die integrierte Browser-Datenbank dabei lediglich dafür, einen neuen Task zu pushen, sobald der Add-Task-Broadcast eintritt – und diese wieder zu verwerfen, wenn der Sync Event ansteht. Einen tieferen Einblick in IndexedDB bietet dieses hilfreiche Tutorial. Im Folgenden werfen wir einen Blick auf den Inhalt von sw.js. Im Anschluss werfen wir erneut einen kommentierten Blick auf die einzelnen Codezeilen. const URL = "https://8014-35-223-70-178.ngrok-free.app/"; // 1const taskChannel = new BroadcastChannel('task-channel'); // 2taskChannel.onmessage = event => { // 3persistTask(event.data.data); // 4registration.sync.register('task-sync'); // 5};let db = null; // 6let request = indexedDB.open("TaskDB", 1); // 7request.onupgradeneeded = function(event) { // 8db = event.target.result; // 9if (!db.objectStoreNames.contains("tasks")) { // 10let tasksObjectStore = db.createObjectStore("tasks", { autoIncrement: true }); // 11}};request.onsuccess = function(event) { db = event.target.result; }; // 12request.onerror = function(event) { console.log("Error in db: " + event); }; // 13persistTask = function(task){ // 14let transaction = db.transaction("tasks", "readwrite");let tasksObjectStore = transaction.objectStore("tasks");let addRequest = tasksObjectStore.add(task);addRequest.onsuccess = function(event){ console.log("Task added to DB"); };addRequest.onerror = function(event) { console.log("Error: " + event); };}self.addEventListener('sync', async function(event) { // 15if (event.tag == 'task-sync') {event.waitUntil(new Promise((res, rej) => { // 16let transaction = db.transaction("tasks", "readwrite");let tasksObjectStore = transaction.objectStore("tasks");let cursorRequest = tasksObjectStore.openCursor();cursorRequest.onsuccess = function(event) { // 17let cursor = event.target.result;if (cursor) {let task = cursor.value; // 18fetch(URL + 'todos/add', // a{ method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ "task" : task })}).then((serverResponse) => {console.log("Task saved to backend.");deleteTasks(); // bres(); // b}).catch((err) => {console.log("ERROR: " + err);rej(); //c})}}}))}})async function deleteTasks() { // 19const transaction = db.transaction("tasks", "readwrite");const tasksObjectStore = transaction.objectStore("tasks");tasksObjectStore.clear();await transaction.complete;} Folgendes spielt sich in diesem Code-Block ab: Weil alle Requests durch denselben, sicheren Tunnel laufen (den wir mit ngrok erstellt haben) wird die URL hier gespeichert. Erstellt einen neuen Broadcast-Kanal mit demselben Namen, um auf Nachrichten warten zu können. Hält nach Message Events in task-channel Ausschau. Wenn eine Antwort auf solche Ereignisse erfolgt, laufen dabei die zwei nachfolgenden Punkte ab. Ruft persistTask() auf, um den neuen Task in IndexedDB abzuspeichern. Registriert einen neuen Sync Event. Das ruft die Fähigkeit auf den Plan, intelligent vorzugehen, wenn es darum geht, Requests zu wiederholen. Der Sync-Handler ermöglicht es, ein Promise zu spezifizieren, der einen Retry triggert, sobald das Netzwerk verfügbar ist – und implementiert eine Back-Off-Strategie sowie Give-Up-Bedingungen. Erstellt eine Referenz für das Datenbankobjekt. Sorgt für den Erhalt eines “Requests”. Sämtliche Inhalte von IndexedDB werden asynchron behandelt. Löst das Event onupgradeneeded aus, sobald der Zugriff auf eine neue oder aktualisierte Datenbank erfolgt. Innerhalb von onupgradeneeded ermöglicht das globale db-Objekt Zugriff auf die Datenbank selbst. Ist die Tasks Collection nicht vorhanden, wird sie erstellt. Wird die Datenbank erfolgreich erstellt, wird sie im db-Objekt gespeichert. Schlägt der Versuch fehl, eine Datenbank zu erstellen, wird der Fehler protokolliert. Ruft die persistTask()-Funktion durch den Broadcast Event add-task (4) auf. Hiermit wird lediglich der neue Task Value in die Tasks Collection aufgenommen. Ein Call erfolgt von Broadcast Event (5) an das Sync Event. Dabei wird überprüft, ob das event.tag-Feld task-sync ist, um sicherzustellen, dass es sich das Task-Sync-Ereignis handelt. event.waitUntil() ermöglicht, dem serviceWorker mitzuteilen, dass der Prozess erst abgeschlossen ist, wenn sein inhärentes Promise verwirklicht wurde. Das hat innerhalb eines Sync Events besondere Bedeutung. a. Definiert ein neues Promise, das zunächst mit der Datenbank verbunden wird. Innerhalb des onsuccess-Callbacks der Datenbank wird ein Cursor verwendet, um den gespeicherten Task zu übernehmen. Dabei dient das Wrapping Promise dazu, mit verschachtelten asynchronen Calls umzugehen. Die Variable mit dem Wert des Broadcast Task. a. Erzeugt einen neuen fetch-Request an den Endpunkt expressJS /todos/add. b. Verläuft dieser Request erfolgreich, wird der Task aus der Datenbank gelöscht und es erfolgt ein Call an res(), um das äußere Promise aufzulösen. c. Schlägt der Request fehl, erfolgt ein Call an rej(). Dadurch wird das enthaltene Promise zurückgewiesen und die Sync API darüber “informiert”, dass die Anfrage erneut übermittelt werden muss. Die Hilfsmethode deleteTasks() löscht alle Aufgaben in der Datenbank. Das ist eine Menge Arbeit – lohnt sich am Ende aber, wenn Requests bei unzureichender Netzwerkverbindung im Hintergrund mühelos wiederholt werden können. Und zwar In-Borwser und über alle möglichen Device-Kategorien. PWA-Beispiel testen Wenn Sie nun die Beispiel-PWA starten und ein To-Do erstellen, wird dieses an das Backend übertragen und dort gespeichert. Interessant wird es nun, wenn Sie über die Dev-Tools (F12) das Netzwerk deaktivieren. Dieser Screenshot zeigt, wo die Einstellung zu finden ist, um die Offline-Funktionalität des PWA-Beispiels zu testen. Foto: Matthew Tyson | IDG Wenn Sie nun im Offline-Modus ein To-Do hinzufügen, passiert erst einmal nichts – die Sync API überwacht den Netzwerkstatus. Sobald Sie das Netzwerk wieder aktivieren, können Sie beobachten, wie der Request an das Backend übergeben wird. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox! 

PWA-Tutorial: Background Sync per Service Worker​ Abbrechende Netzwerkverbindungen stellen für Progressive Web Apps kein Hindernis dar – Service Workers sei Dank. Foto: Pasuwan | shutterstock.com Progressive Web Apps (PWAs) sind ein wichtiges Konzept im Bereich Web Development, das die universelle Bereitstellung per Webbrowser mit der Feature-Vielfalt nativer Software verbindet. Eine besonders erwähnenswerte Eigenschaft progressiver Webanwendungen ist ihre Offline-Processing-Fähigkeit. Diese kommt beispielsweise zum Tragen, wenn ein Benutzer eine E-Mail übermittelt, aber keine Netzwerkverbindung verfügbar ist, um diese zu verarbeiten. In diesem Tutorial lesen Sie, wie das funktioniert – und lernen in diesem Zuge mehrere wichtige Komponenten einer PWA kennen. Nämlich: Service Worker, Synchronisierung, Sync Events sowie IndexedDB. Den Code für das PWA-Beispiel in diesem Artikel finden Sie auf GitHub. Service-Worker-Grundlagen Im Zusammenhang mit Progressive Web Apps wird jede Menge Hirnschmalz investiert, um deren Verhalten möglichst nahe an das von nativen Anwendungen anzugleichen. Dabei spielen Service Worker einer ganz wesentliche Rolle. Im Grunde handelt es sich bei einem Service Worker um eine eingeschränkte Variante eines Worker Thread, der mit dem Main Browser Thread ausschließlich über Event Messages kommuniziert – und keinen DOM-Zugriff hat. Dabei stellt der Service Worker eine Art eigener Umgebung dar (wie wir gleich sehen werden). Trotz ihrer Limitationen sind Service Worker relativ leistungsfähig, da sie einen eigenen, vom Main Thread unabhängigen Lebenszyklus haben – der eine Vielzahl von Hintergrundoperationen ermöglicht. In unserem Fall geht es dabei in erster Linie um die Sync API, die den Service Worker dazu befähigt, den Zustand der Netzwerkverbindung zu beobachten – und einen Netzwerk-Call solange zu widerholen, bis dieser erfolgreich ist. Sync-API und -Events Stellen Sie sich vor, Sie möchten einen “Retry”-Mechanismus konfigurieren, der Folgendes beinhaltet: Wenn das Netzwerk verfügbar ist, werden Request direkt übermittelt. Wenn das Netzwerk nicht verfügbar ist, erfolgt der nächste Übermittlungsversuch, sobald es wieder verfügbar ist. Sobald ein Retry fehlschlägt, kommt beim nächsten Versuch ein exponentieller Back-Off zur Anwendung und die Settings werden verworfen. Das in der Praxis umzusetzen, würde eine Menge granularer Arbeit nach sich ziehen. Glücklicherweise verfügen Service Worker für exakt diesen Zweck über ein spezielles Sync Event. Service Worker werden mit navigator.serviceWorker registriert. Dieses Objekt ist nur in einem sicheren Kontext verfügbar. Die Webseite muss also über HTTPS geladen werden. Unsere Beispielanwendung für diesen Artikel steht in der Tradition der kanonischen TODO-Sample-App. Nun werfen wir einen Blick darauf, wie sich ein neues To-Do mit Hilfe des sync-Events eines Service Workers abbilden lässt. Service-Worker-Synchronisierung einrichten Im Folgenden erörtern wir, wie Sie den gesamten Lebenszyklus einer Progressive Web App managen können, die Prozesse beinhaltet, die synchronisiert werden müssen. Dazu benötigen wir ein Full-Stack-Setup, in dem Requests für ein neues TODO dem eben beschriebenen Retry-Mechanismus folgen. Um das Frontend zu bedienen und den API Request zu händeln, nutzen wir Node und Express. Vorausgesetzt, npm ist installiert, starten Sie eine neue App mit folgendem Call: $ npm init -y Das erzeugt das Gerüst für eine neue Applikation. Im nächsten Schritt, gilt es, die express-Dependency hinzuzufügen: $ npm install express Nun können Sie einen einfachen Express-Server erstellen, der statische Dateien bereitstellt. Anschließend erstellen Sie eine neue index.js-Datei im Root-Verzeichnis und fügen dort Folgendes ein: const express = require('express');const path = require('path'); // Required for serving static filesconst app = express();const port = process.env.PORT || 3000; // default to 3000app.use(express.static(path.join(__dirname, 'public')));// Serve the main HTML file for all unmatched routes (catch-all)app.get('*', (req, res) => {res.sendFile(path.join(__dirname, 'public', 'index.html'));});app.listen(port, () => {console.log(`Server listening on port ${port}`);}); Wenn Sie diese Anwendung starten, wird sie alles bedienen, was sich in /public befindet. Um den Ausführungsprozess zu vereinfachen, öffnen Sie die package.json-Datei, die npm erstellt hat und fügen ein Startskript hinzu: "scripts": {"start": "node index.js","test": "echo "Error: no test specified" && exit 1"} Über die Befehlszeile können Sie die App nun starten: $ npm run start Bevor diese Anwendung etwas “tut”, müssen Sie eine neue index.html-Datei in das /public-Verzeichnis einfügen: PWA InfoWorldMy To-Do ListAdd Wenn Sie nun localhost:3000 aufrufen, erhalten Sie das grundlegende HTML-Layout – inklusive Titel, Input Box und Schaltfläche. Dabei ist zu beachten, dass eine Interaktion mit letztgenanntem Button den Wert eines neuen Task-Inputs übernimmt und diesen an die addTask()-Funktion übergibt. Das Hauptskript addTask() wird über script.js bereitgestellt. Sie können den Inhalt dieser Datei einfügen:if ('serviceWorker' in navigator) { // 1window.addEventListener('load', () => { // 2navigator.serviceWorker.register('sw.js') // 3.then(registration => { // 4console.log('Service Worker registered', registration); // 5}).catch(err => console.error('Service Worker registration failed', err)); // 6});}const taskChannel = new BroadcastChannel('task-channel'); // 7function addTask(task) { // 8taskChannel.postMessage({ type: 'add-task', data: task }); // 9} Diesen Code haben wir mit nummerierten Kommentaren ausgestattet, die erklären, was die einzelnen Zeilen “tun”: Überprüft, ob serviceWorker auf navigator vorhanden ist. Das ist nur der Fall, wenn ein sicherer Kontext besteht. Fügt einen Callback zum Load Observer hinzu, falls der serviceWorker vorhanden ist, damit dieser reagiert, wenn die Seite geladen wird. Nutzt die register-Methode, um die Datei sw.js als Service Worker zu laden. Nachdem sw.js geladen ist, folgt ein Callback mit dem Registrierungsobjekt. Das Registrierungsobjekt kann genutzt werden, um verschiedene Tasks auszuführen – in unserem Fall wird ausschließlich der Erfolg geloggt. Protokolliert sämtliche Fehler mit Hilfe des catch() promise-Callbacks. Erstellt einen BroadcastChannel namens “task-channel“. Das ist ein simpler Weg, um Ereignisse an den Service Worker zu übermitteln, der auf dem Codes in sw.js basiert. Die addTask()-Funktion wird von der HTML-Datei aufgerufen. Sendet eine Nachricht auf dem task-channel, definiert den Type als “add-task” sowie das Datenfeld als Task an sich. In diesem Beispiel ignorieren wir, wie das User Interface die Task Creation händeln würde. Wir könnten auch verschiedene andere Ansätze verwenden – beispielsweise einen optimistischen, bei dem wir den Task in die UI-Liste einfügen und anschließend versuchen, mit dem Backend zu synchronisieren. Alternativ wäre es auch möglich, zuerst einen Backend-Synchronisierungsversuch zu starten und im Erfolgsfall eine Nachricht an die Benutzeroberfläche zu senden, um einen Task hinzuzufügen. Der BroadcastChannel erleichtert es dabei, Nachrichten in beide Richtungen zu senden: vom Hauptthread zum Service Worker oder umgekehrt. Wie bereits eingangs erwähnt, ist im Rahmen einer Sicherheitseinschränkung eine HTTPS-Verbindung notwendig, damit serviceWorker auf navigator existieren kann. Um diese mit minimalem Aufwand herzustellen, haben wir in diesem Beispiel ngrok verwendet. Dieses praktische Befehlszeilen-Tool öffnet Ihre lokale Umgebung für die Außenwelt – ohne Konfiguration und inklusive HTTPS. Starten Sie etwa die Sample App ($ npm run start) und lassen den Befehl $ ngrok http 3000 folgen, erzeugt das einen Tunnel und sorgt dafür, dass die HTTP- und HTTPS-Endpunkte angezeigt werden. Damit können Sie die URL-Leiste Ihres Browser füttern. Zum Beispiel: Forwarding https://8041-35-223-70-178.ngrok-free.app -> http://localhost:3000 Nun können Sie die App über eine HTTPS-Verbindung unter https://8041-35-223-70-178.ngrok-free.app aufrufen. Mit dem Service Worker interagieren Die sw.js-Datei (die wir zuvor über den Browser mit serviceWorker geladen haben) dient dazu, mit dem Service Worker zu interagieren. Für das folgende Beispiel nutzen wie IndexedDB. Der Grund: Es steht dem Browser “frei”, Service-Worker-Kontexte nach Belieben zu erstellen oder zu verwerfen, um Ereignisse zu behandeln. Es gibt also keine Garantie dafür, dass derselbe Kontext verwendet wird, um den Broadcast- und den Snyc-Event zu behandeln. Lokale Variablen scheiden insofern als verlässliches Medium aus – LocalStorage ist für Service Worker nicht verfügbar. Sie könnten zwar CacheStorage verwenden (das sowohl in Main- als auch in Service Threads verfügbar ist) – das ist allerdings eigentlich dafür gedacht, Antworten auf Requests zwischenzuspeichern. Das führt uns letztlich zu IndexedDB, das in allen Service-Worker-Instanzen “lebt”. Wir nutzen die integrierte Browser-Datenbank dabei lediglich dafür, einen neuen Task zu pushen, sobald der Add-Task-Broadcast eintritt – und diese wieder zu verwerfen, wenn der Sync Event ansteht. Einen tieferen Einblick in IndexedDB bietet dieses hilfreiche Tutorial. Im Folgenden werfen wir einen Blick auf den Inhalt von sw.js. Im Anschluss werfen wir erneut einen kommentierten Blick auf die einzelnen Codezeilen. const URL = "https://8014-35-223-70-178.ngrok-free.app/"; // 1const taskChannel = new BroadcastChannel('task-channel'); // 2taskChannel.onmessage = event => { // 3persistTask(event.data.data); // 4registration.sync.register('task-sync'); // 5};let db = null; // 6let request = indexedDB.open("TaskDB", 1); // 7request.onupgradeneeded = function(event) { // 8db = event.target.result; // 9if (!db.objectStoreNames.contains("tasks")) { // 10let tasksObjectStore = db.createObjectStore("tasks", { autoIncrement: true }); // 11}};request.onsuccess = function(event) { db = event.target.result; }; // 12request.onerror = function(event) { console.log("Error in db: " + event); }; // 13persistTask = function(task){ // 14let transaction = db.transaction("tasks", "readwrite");let tasksObjectStore = transaction.objectStore("tasks");let addRequest = tasksObjectStore.add(task);addRequest.onsuccess = function(event){ console.log("Task added to DB"); };addRequest.onerror = function(event) { console.log("Error: " + event); };}self.addEventListener('sync', async function(event) { // 15if (event.tag == 'task-sync') {event.waitUntil(new Promise((res, rej) => { // 16let transaction = db.transaction("tasks", "readwrite");let tasksObjectStore = transaction.objectStore("tasks");let cursorRequest = tasksObjectStore.openCursor();cursorRequest.onsuccess = function(event) { // 17let cursor = event.target.result;if (cursor) {let task = cursor.value; // 18fetch(URL + 'todos/add', // a{ method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({ "task" : task })}).then((serverResponse) => {console.log("Task saved to backend.");deleteTasks(); // bres(); // b}).catch((err) => {console.log("ERROR: " + err);rej(); //c})}}}))}})async function deleteTasks() { // 19const transaction = db.transaction("tasks", "readwrite");const tasksObjectStore = transaction.objectStore("tasks");tasksObjectStore.clear();await transaction.complete;} Folgendes spielt sich in diesem Code-Block ab: Weil alle Requests durch denselben, sicheren Tunnel laufen (den wir mit ngrok erstellt haben) wird die URL hier gespeichert. Erstellt einen neuen Broadcast-Kanal mit demselben Namen, um auf Nachrichten warten zu können. Hält nach Message Events in task-channel Ausschau. Wenn eine Antwort auf solche Ereignisse erfolgt, laufen dabei die zwei nachfolgenden Punkte ab. Ruft persistTask() auf, um den neuen Task in IndexedDB abzuspeichern. Registriert einen neuen Sync Event. Das ruft die Fähigkeit auf den Plan, intelligent vorzugehen, wenn es darum geht, Requests zu wiederholen. Der Sync-Handler ermöglicht es, ein Promise zu spezifizieren, der einen Retry triggert, sobald das Netzwerk verfügbar ist – und implementiert eine Back-Off-Strategie sowie Give-Up-Bedingungen. Erstellt eine Referenz für das Datenbankobjekt. Sorgt für den Erhalt eines “Requests”. Sämtliche Inhalte von IndexedDB werden asynchron behandelt. Löst das Event onupgradeneeded aus, sobald der Zugriff auf eine neue oder aktualisierte Datenbank erfolgt. Innerhalb von onupgradeneeded ermöglicht das globale db-Objekt Zugriff auf die Datenbank selbst. Ist die Tasks Collection nicht vorhanden, wird sie erstellt. Wird die Datenbank erfolgreich erstellt, wird sie im db-Objekt gespeichert. Schlägt der Versuch fehl, eine Datenbank zu erstellen, wird der Fehler protokolliert. Ruft die persistTask()-Funktion durch den Broadcast Event add-task (4) auf. Hiermit wird lediglich der neue Task Value in die Tasks Collection aufgenommen. Ein Call erfolgt von Broadcast Event (5) an das Sync Event. Dabei wird überprüft, ob das event.tag-Feld task-sync ist, um sicherzustellen, dass es sich das Task-Sync-Ereignis handelt. event.waitUntil() ermöglicht, dem serviceWorker mitzuteilen, dass der Prozess erst abgeschlossen ist, wenn sein inhärentes Promise verwirklicht wurde. Das hat innerhalb eines Sync Events besondere Bedeutung. a. Definiert ein neues Promise, das zunächst mit der Datenbank verbunden wird. Innerhalb des onsuccess-Callbacks der Datenbank wird ein Cursor verwendet, um den gespeicherten Task zu übernehmen. Dabei dient das Wrapping Promise dazu, mit verschachtelten asynchronen Calls umzugehen. Die Variable mit dem Wert des Broadcast Task. a. Erzeugt einen neuen fetch-Request an den Endpunkt expressJS /todos/add. b. Verläuft dieser Request erfolgreich, wird der Task aus der Datenbank gelöscht und es erfolgt ein Call an res(), um das äußere Promise aufzulösen. c. Schlägt der Request fehl, erfolgt ein Call an rej(). Dadurch wird das enthaltene Promise zurückgewiesen und die Sync API darüber “informiert”, dass die Anfrage erneut übermittelt werden muss. Die Hilfsmethode deleteTasks() löscht alle Aufgaben in der Datenbank. Das ist eine Menge Arbeit – lohnt sich am Ende aber, wenn Requests bei unzureichender Netzwerkverbindung im Hintergrund mühelos wiederholt werden können. Und zwar In-Borwser und über alle möglichen Device-Kategorien. PWA-Beispiel testen Wenn Sie nun die Beispiel-PWA starten und ein To-Do erstellen, wird dieses an das Backend übertragen und dort gespeichert. Interessant wird es nun, wenn Sie über die Dev-Tools (F12) das Netzwerk deaktivieren. Dieser Screenshot zeigt, wo die Einstellung zu finden ist, um die Offline-Funktionalität des PWA-Beispiels zu testen. Foto: Matthew Tyson | IDG Wenn Sie nun im Offline-Modus ein To-Do hinzufügen, passiert erst einmal nichts – die Sync API überwacht den Netzwerkstatus. Sobald Sie das Netzwerk wieder aktivieren, können Sie beobachten, wie der Request an das Backend übergeben wird. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!

PWA-Tutorial: Background Sync per Service Worker​

Abbrechende Netzwerkverbindungen stellen für Progressive Web Apps kein Hindernis dar – Service Workers sei Dank. Foto: Pasuwan | shutterstock.com Progressive Web Apps (PWAs) sind ein wichtiges Konzept im Bereich Web Development, das die universelle Bereitstellung per Webbrowser mit der Feature-Vielfalt nativer Software verbindet. Eine besonders erwähnenswerte Eigenschaft progressiver Webanwendungen ist ihre Offline-Processing-Fähigkeit. Diese kommt beispielsweise zum Tragen, wenn ein Benutzer eine E-Mail übermittelt, aber keine Netzwerkverbindung verfügbar ist, um diese zu verarbeiten. In diesem Tutorial lesen Sie, wie das funktioniert – und lernen in diesem Zuge mehrere wichtige Komponenten einer PWA kennen. Nämlich: Service Worker, Synchronisierung, Sync Events sowie IndexedDB. Den Code für das PWA-Beispiel in diesem Artikel finden Sie auf GitHub. Service-Worker-Grundlagen Im Zusammenhang mit Progressive Web Apps wird jede Menge Hirnschmalz investiert, um deren Verhalten möglichst nahe an das von nativen Anwendungen anzugleichen. Dabei spielen Service Worker einer ganz wesentliche Rolle. Im Grunde handelt es sich bei einem Service Worker um eine eingeschränkte Variante eines Worker Thread, der mit dem Main Browser Thread ausschließlich über Event Messages kommuniziert – und keinen DOM-Zugriff hat. Dabei stellt der Service Worker eine Art eigener Umgebung dar (wie wir gleich sehen werden). Trotz ihrer Limitationen sind Service Worker relativ leistungsfähig, da sie einen eigenen, vom Main Thread unabhängigen Lebenszyklus haben – der eine Vielzahl von Hintergrundoperationen ermöglicht. In unserem Fall geht es dabei in erster Linie um die Sync API, die den Service Worker dazu befähigt, den Zustand der Netzwerkverbindung zu beobachten – und einen Netzwerk-Call solange zu widerholen, bis dieser erfolgreich ist. Sync-API und -Events Stellen Sie sich vor, Sie möchten einen “Retry”-Mechanismus konfigurieren, der Folgendes beinhaltet: Wenn das Netzwerk verfügbar ist, werden Request direkt übermittelt. Wenn das Netzwerk nicht verfügbar ist, erfolgt der nächste Übermittlungsversuch, sobald es wieder verfügbar ist. Sobald ein Retry fehlschlägt, kommt beim nächsten Versuch ein exponentieller Back-Off zur Anwendung und die Settings werden verworfen. Das in der Praxis umzusetzen, würde eine Menge granularer Arbeit nach sich ziehen. Glücklicherweise verfügen Service Worker für exakt diesen Zweck über ein spezielles Sync Event. Service Worker werden mit navigator.serviceWorker registriert. Dieses Objekt ist nur in einem sicheren Kontext verfügbar. Die Webseite muss also über HTTPS geladen werden. Unsere Beispielanwendung für diesen Artikel steht in der Tradition der kanonischen TODO-Sample-App. Nun werfen wir einen Blick darauf, wie sich ein neues To-Do mit Hilfe des sync-Events eines Service Workers abbilden lässt. Service-Worker-Synchronisierung einrichten Im Folgenden erörtern wir, wie Sie den gesamten Lebenszyklus einer Progressive Web App managen können, die Prozesse beinhaltet, die synchronisiert werden müssen. Dazu benötigen wir ein Full-Stack-Setup, in dem Requests für ein neues TODO dem eben beschriebenen Retry-Mechanismus folgen. Um das Frontend zu bedienen und den API Request zu händeln, nutzen wir Node und Express. Vorausgesetzt, npm ist installiert, starten Sie eine neue App mit folgendem Call: $ npm init -y Das erzeugt das Gerüst für eine neue Applikation. Im nächsten Schritt, gilt es, die express-Dependency hinzuzufügen: $ npm install express Nun können Sie einen einfachen Express-Server erstellen, der statische Dateien bereitstellt. Anschließend erstellen Sie eine neue index.js-Datei im Root-Verzeichnis und fügen dort Folgendes ein: const express = require(‘express’);const path = require(‘path’); // Required for serving static filesconst app = express();const port = process.env.PORT || 3000; // default to 3000app.use(express.static(path.join(__dirname, ‘public’)));// Serve the main HTML file for all unmatched routes (catch-all)app.get(‘*’, (req, res) => {res.sendFile(path.join(__dirname, ‘public’, ‘index.html’));});app.listen(port, () => {console.log(`Server listening on port ${port}`);}); Wenn Sie diese Anwendung starten, wird sie alles bedienen, was sich in /public befindet. Um den Ausführungsprozess zu vereinfachen, öffnen Sie die package.json-Datei, die npm erstellt hat und fügen ein Startskript hinzu: “scripts”: {“start”: “node index.js”,”test”: “echo “Error: no test specified” && exit 1″} Über die Befehlszeile können Sie die App nun starten: $ npm run start Bevor diese Anwendung etwas “tut”, müssen Sie eine neue index.html-Datei in das /public-Verzeichnis einfügen: PWA InfoWorldMy To-Do ListAdd Wenn Sie nun localhost:3000 aufrufen, erhalten Sie das grundlegende HTML-Layout – inklusive Titel, Input Box und Schaltfläche. Dabei ist zu beachten, dass eine Interaktion mit letztgenanntem Button den Wert eines neuen Task-Inputs übernimmt und diesen an die addTask()-Funktion übergibt. Das Hauptskript addTask() wird über script.js bereitgestellt. Sie können den Inhalt dieser Datei einfügen:if (‘serviceWorker’ in navigator) { // 1window.addEventListener(‘load’, () => { // 2navigator.serviceWorker.register(‘sw.js’) // 3.then(registration => { // 4console.log(‘Service Worker registered’, registration); // 5}).catch(err => console.error(‘Service Worker registration failed’, err)); // 6});}const taskChannel = new BroadcastChannel(‘task-channel’); // 7function addTask(task) { // 8taskChannel.postMessage({ type: ‘add-task’, data: task }); // 9} Diesen Code haben wir mit nummerierten Kommentaren ausgestattet, die erklären, was die einzelnen Zeilen “tun”: Überprüft, ob serviceWorker auf navigator vorhanden ist. Das ist nur der Fall, wenn ein sicherer Kontext besteht. Fügt einen Callback zum Load Observer hinzu, falls der serviceWorker vorhanden ist, damit dieser reagiert, wenn die Seite geladen wird. Nutzt die register-Methode, um die Datei sw.js als Service Worker zu laden. Nachdem sw.js geladen ist, folgt ein Callback mit dem Registrierungsobjekt. Das Registrierungsobjekt kann genutzt werden, um verschiedene Tasks auszuführen – in unserem Fall wird ausschließlich der Erfolg geloggt. Protokolliert sämtliche Fehler mit Hilfe des catch() promise-Callbacks. Erstellt einen BroadcastChannel namens “task-channel“. Das ist ein simpler Weg, um Ereignisse an den Service Worker zu übermitteln, der auf dem Codes in sw.js basiert. Die addTask()-Funktion wird von der HTML-Datei aufgerufen. Sendet eine Nachricht auf dem task-channel, definiert den Type als “add-task” sowie das Datenfeld als Task an sich. In diesem Beispiel ignorieren wir, wie das User Interface die Task Creation händeln würde. Wir könnten auch verschiedene andere Ansätze verwenden – beispielsweise einen optimistischen, bei dem wir den Task in die UI-Liste einfügen und anschließend versuchen, mit dem Backend zu synchronisieren. Alternativ wäre es auch möglich, zuerst einen Backend-Synchronisierungsversuch zu starten und im Erfolgsfall eine Nachricht an die Benutzeroberfläche zu senden, um einen Task hinzuzufügen. Der BroadcastChannel erleichtert es dabei, Nachrichten in beide Richtungen zu senden: vom Hauptthread zum Service Worker oder umgekehrt. Wie bereits eingangs erwähnt, ist im Rahmen einer Sicherheitseinschränkung eine HTTPS-Verbindung notwendig, damit serviceWorker auf navigator existieren kann. Um diese mit minimalem Aufwand herzustellen, haben wir in diesem Beispiel ngrok verwendet. Dieses praktische Befehlszeilen-Tool öffnet Ihre lokale Umgebung für die Außenwelt – ohne Konfiguration und inklusive HTTPS. Starten Sie etwa die Sample App ($ npm run start) und lassen den Befehl $ ngrok http 3000 folgen, erzeugt das einen Tunnel und sorgt dafür, dass die HTTP- und HTTPS-Endpunkte angezeigt werden. Damit können Sie die URL-Leiste Ihres Browser füttern. Zum Beispiel: Forwarding https://8041-35-223-70-178.ngrok-free.app -> http://localhost:3000 Nun können Sie die App über eine HTTPS-Verbindung unter https://8041-35-223-70-178.ngrok-free.app aufrufen. Mit dem Service Worker interagieren Die sw.js-Datei (die wir zuvor über den Browser mit serviceWorker geladen haben) dient dazu, mit dem Service Worker zu interagieren. Für das folgende Beispiel nutzen wie IndexedDB. Der Grund: Es steht dem Browser “frei”, Service-Worker-Kontexte nach Belieben zu erstellen oder zu verwerfen, um Ereignisse zu behandeln. Es gibt also keine Garantie dafür, dass derselbe Kontext verwendet wird, um den Broadcast- und den Snyc-Event zu behandeln. Lokale Variablen scheiden insofern als verlässliches Medium aus – LocalStorage ist für Service Worker nicht verfügbar. Sie könnten zwar CacheStorage verwenden (das sowohl in Main- als auch in Service Threads verfügbar ist) – das ist allerdings eigentlich dafür gedacht, Antworten auf Requests zwischenzuspeichern. Das führt uns letztlich zu IndexedDB, das in allen Service-Worker-Instanzen “lebt”. Wir nutzen die integrierte Browser-Datenbank dabei lediglich dafür, einen neuen Task zu pushen, sobald der Add-Task-Broadcast eintritt – und diese wieder zu verwerfen, wenn der Sync Event ansteht. Einen tieferen Einblick in IndexedDB bietet dieses hilfreiche Tutorial. Im Folgenden werfen wir einen Blick auf den Inhalt von sw.js. Im Anschluss werfen wir erneut einen kommentierten Blick auf die einzelnen Codezeilen. const URL = “https://8014-35-223-70-178.ngrok-free.app/”; // 1const taskChannel = new BroadcastChannel(‘task-channel’); // 2taskChannel.onmessage = event => { // 3persistTask(event.data.data); // 4registration.sync.register(‘task-sync’); // 5};let db = null; // 6let request = indexedDB.open(“TaskDB”, 1); // 7request.onupgradeneeded = function(event) { // 8db = event.target.result; // 9if (!db.objectStoreNames.contains(“tasks”)) { // 10let tasksObjectStore = db.createObjectStore(“tasks”, { autoIncrement: true }); // 11}};request.onsuccess = function(event) { db = event.target.result; }; // 12request.onerror = function(event) { console.log(“Error in db: ” + event); }; // 13persistTask = function(task){ // 14let transaction = db.transaction(“tasks”, “readwrite”);let tasksObjectStore = transaction.objectStore(“tasks”);let addRequest = tasksObjectStore.add(task);addRequest.onsuccess = function(event){ console.log(“Task added to DB”); };addRequest.onerror = function(event) { console.log(“Error: ” + event); };}self.addEventListener(‘sync’, async function(event) { // 15if (event.tag == ‘task-sync’) {event.waitUntil(new Promise((res, rej) => { // 16let transaction = db.transaction(“tasks”, “readwrite”);let tasksObjectStore = transaction.objectStore(“tasks”);let cursorRequest = tasksObjectStore.openCursor();cursorRequest.onsuccess = function(event) { // 17let cursor = event.target.result;if (cursor) {let task = cursor.value; // 18fetch(URL + ‘todos/add’, // a{ method: ‘POST’,headers: { ‘Content-Type’: ‘application/json’ },body: JSON.stringify({ “task” : task })}).then((serverResponse) => {console.log(“Task saved to backend.”);deleteTasks(); // bres(); // b}).catch((err) => {console.log(“ERROR: ” + err);rej(); //c})}}}))}})async function deleteTasks() { // 19const transaction = db.transaction(“tasks”, “readwrite”);const tasksObjectStore = transaction.objectStore(“tasks”);tasksObjectStore.clear();await transaction.complete;} Folgendes spielt sich in diesem Code-Block ab: Weil alle Requests durch denselben, sicheren Tunnel laufen (den wir mit ngrok erstellt haben) wird die URL hier gespeichert. Erstellt einen neuen Broadcast-Kanal mit demselben Namen, um auf Nachrichten warten zu können. Hält nach Message Events in task-channel Ausschau. Wenn eine Antwort auf solche Ereignisse erfolgt, laufen dabei die zwei nachfolgenden Punkte ab. Ruft persistTask() auf, um den neuen Task in IndexedDB abzuspeichern. Registriert einen neuen Sync Event. Das ruft die Fähigkeit auf den Plan, intelligent vorzugehen, wenn es darum geht, Requests zu wiederholen. Der Sync-Handler ermöglicht es, ein Promise zu spezifizieren, der einen Retry triggert, sobald das Netzwerk verfügbar ist – und implementiert eine Back-Off-Strategie sowie Give-Up-Bedingungen. Erstellt eine Referenz für das Datenbankobjekt. Sorgt für den Erhalt eines “Requests”. Sämtliche Inhalte von IndexedDB werden asynchron behandelt. Löst das Event onupgradeneeded aus, sobald der Zugriff auf eine neue oder aktualisierte Datenbank erfolgt. Innerhalb von onupgradeneeded ermöglicht das globale db-Objekt Zugriff auf die Datenbank selbst. Ist die Tasks Collection nicht vorhanden, wird sie erstellt. Wird die Datenbank erfolgreich erstellt, wird sie im db-Objekt gespeichert. Schlägt der Versuch fehl, eine Datenbank zu erstellen, wird der Fehler protokolliert. Ruft die persistTask()-Funktion durch den Broadcast Event add-task (4) auf. Hiermit wird lediglich der neue Task Value in die Tasks Collection aufgenommen. Ein Call erfolgt von Broadcast Event (5) an das Sync Event. Dabei wird überprüft, ob das event.tag-Feld task-sync ist, um sicherzustellen, dass es sich das Task-Sync-Ereignis handelt. event.waitUntil() ermöglicht, dem serviceWorker mitzuteilen, dass der Prozess erst abgeschlossen ist, wenn sein inhärentes Promise verwirklicht wurde. Das hat innerhalb eines Sync Events besondere Bedeutung. a. Definiert ein neues Promise, das zunächst mit der Datenbank verbunden wird. Innerhalb des onsuccess-Callbacks der Datenbank wird ein Cursor verwendet, um den gespeicherten Task zu übernehmen. Dabei dient das Wrapping Promise dazu, mit verschachtelten asynchronen Calls umzugehen. Die Variable mit dem Wert des Broadcast Task. a. Erzeugt einen neuen fetch-Request an den Endpunkt expressJS /todos/add. b. Verläuft dieser Request erfolgreich, wird der Task aus der Datenbank gelöscht und es erfolgt ein Call an res(), um das äußere Promise aufzulösen. c. Schlägt der Request fehl, erfolgt ein Call an rej(). Dadurch wird das enthaltene Promise zurückgewiesen und die Sync API darüber “informiert”, dass die Anfrage erneut übermittelt werden muss. Die Hilfsmethode deleteTasks() löscht alle Aufgaben in der Datenbank. Das ist eine Menge Arbeit – lohnt sich am Ende aber, wenn Requests bei unzureichender Netzwerkverbindung im Hintergrund mühelos wiederholt werden können. Und zwar In-Borwser und über alle möglichen Device-Kategorien. PWA-Beispiel testen Wenn Sie nun die Beispiel-PWA starten und ein To-Do erstellen, wird dieses an das Backend übertragen und dort gespeichert. Interessant wird es nun, wenn Sie über die Dev-Tools (F12) das Netzwerk deaktivieren. Dieser Screenshot zeigt, wo die Einstellung zu finden ist, um die Offline-Funktionalität des PWA-Beispiels zu testen. Foto: Matthew Tyson | IDG Wenn Sie nun im Offline-Modus ein To-Do hinzufügen, passiert erst einmal nichts – die Sync API überwacht den Netzwerkstatus. Sobald Sie das Netzwerk wieder aktivieren, können Sie beobachten, wie der Request an das Backend übergeben wird. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox! 

Nach oben scrollen