#2757: rest.js
projectforge-webapp/src/utilities/rest.js
JavaScript / React-Dienstprogramm — zentraler REST-API-Client für das ProjectForge React-Frontend.
Quelle: projectforge-webapp/src/utilities/rest.js
88 Zeilen · 68 Code · 7 Kommentare · 13 leer
Jede React-Komponente, die mit dem ProjectForge-Backend kommuniziert, durchläuft dieses Modul. Es bietet einen dünnen Wrapper um die fetch-API mit umgebungsbewusster URL-Konstruktion, Sitzungs-Cookie-Authentifizierung (credentials: 'include'), adaptivem Debouncing für langsame Verbindungen und JSON-Anfrage-/Antwortverarbeitung. Dies ist der einzige Kontaktpunkt zwischen der React-SPA und der Spring-Boot-REST-Schicht – es gibt keinen anderen HTTP-Client im Frontend.
Architektur
Pipeline zur URL-Konstruktion
Das Modul baut REST-URLs durch eine dreistufige Pipeline auf, wobei jede Stufe einen Aspekt behandelt:
Stufe 1 – baseURL: Ermittelt den Server-Ursprung. In der Entwicklung (import.meta.env.DEV) zeigt sie auf http://localhost:8080 – den Spring-Boot-Entwicklungsserver. In der Produktion wird eine leere Zeichenkette zurückgegeben, sodass alle URLs relativ zum Seitenursprung sind (kein CORS erforderlich). Die Abfrage MODE !== 'test' verhindert, dass die Testserver-URL in Jest-Tests durchsickert.
Stufe 2 – getServiceURL / baseRestURL: Stellt das REST-Präfix voran. Wenn die Service-URL mit / beginnt (absolut vom Stammverzeichnis), wird baseURL direkt verwendet. Andernfalls wird baseRestURL verwendet, das /rs anhängt – das Standard-Präfix für ProjectForge-REST-Endpunkte. Dies ermöglicht sowohl getServiceURL('/some/path') (absolut) als auch getServiceURL('user/list') (relativ zu /rs).
Stufe 3 – evalServiceURL / createQueryParams: Hängt Abfrageparameter an. Filtert undefined-Werte heraus, kodiert jeden Wert und verwendet korrekt ? vs. &, je nachdem, ob die URL bereits eine Abfragezeichenfolge enthält.
Debounce-Strategie – Save-Data-Bewusstsein
Die Konstante
debouncedWaitTime passt sich basierend auf dem
Save-Data-Client-Hinweis an (
MDN). Wenn der Benutzer den Datensparmodus aktiviert hat (häufig auf Mobilgeräten), erhöht sich die Wartezeit von 250 ms auf 1000 ms – dies reduziert die Anforderungshäufigkeit bei getakteten Verbindungen. Dies wird von den Autovervollständigungskomponenten (Adresssuche, Benutzersuche, Aufgabensuche) verwendet, um API-Aufrufe während der Eingabe zu drosseln.
Das Design ist defensiv:
navigator && navigator.connection && navigator.connection.saveData – drei Sicherheitsabfragen, bevor auf die Eigenschaft zugegriffen wird, da
navigator.connection nicht in allen Browsern verfügbar ist (Firefox, Safari).
Exportierte API
| Export | Typ | Zweck |
debouncedWaitTime | const (250 oder 1000) | Adaptives Debounce für Autovervollständigungs-Eingaben basierend auf dem Save-Data-Header |
baseURL | const (string) | Server-Ursprung – localhost:8080 in der Entwicklung, leer in der Produktion |
baseRestURL | const (string) | baseURL + "/rs" – Standard-REST-Endpunkt-Präfix |
createQueryParams(obj) | Funktion → string | Erstellt key=val&key=val aus einem Objekt, filtert undefined-Werte heraus |
evalServiceURL(url, params) | Funktion → string | Hängt Abfrageparameter an die URL an, behandelt vorhandenes ? vs. & |
getServiceURL(url, params) | Funktion → string | Vollständiger URL-Builder – stellt Basis + REST-Präfix voran, hängt Parameter an |
handleHTTPErrors(res) | Funktion → Response | Wirft einen Fehler bei Nicht-2xx-Antworten, gibt die Antwort bei Erfolg durch |
fetchJsonGet(url, params, cb) | Funktion → Promise | GET mit JSON-Antwort, geparst und an Callback übergeben |
fetchJsonPost(url, body, cb) | Funktion → Promise | POST mit JSON-stringifiziertem Body und JSON-Antwort |
fetchGet(url, params, cb) | Funktion → Promise | Einfaches GET mit optionalem Callback (kein JSON-Parsing) |
getObjectFromQuery(str) | Funktion → object | Umkehrung von createQueryParams – parst ?key=val&... in ein Objekt |
Fehlerbehandlung
Alle drei Fetch-Funktionen verwenden dasselbe Fehlerbehandlungsmuster: .catch((error) => alert('Interner Fehler: ' + error)). Die Direktive /* eslint-disable no-alert */ am Anfang erkennt an, dass alert() normalerweise von ESLint verboten wird, hier aber absichtlich als letzte Benachrichtigungsmöglichkeit für den Benutzer verwendet wird.
Dies ist ein bewusster Kompromiss: Ein ordentliches Fehler-Toast-System (Redux-gesteuert, i18n-fähig) wäre eine bessere Benutzererfahrung, aber rest.js ist die unterste Ebene des Abhängigkeitsgraphen – es kann keine Redux- oder React-Komponenten importieren. Die Verwendung von alert() bedeutet, dass der Fehlerhandler null Abhängigkeiten hat und in jedem Kontext funktioniert (auch außerhalb des React-Lebenszyklus). Die Datei /* eslint-disable no-alert */ wurde im Commit ac30e55f7 hinzugefügt, um die Linting-Warnung zu dieser beabsichtigten Entscheidung zu unterdrücken.
Authentifizierung
Alle Fetch-Aufrufe verwenden credentials: 'include' – dies weist den Browser an, HTTP-Only-Sitzungs-Cookies mit jeder Anfrage zu senden. Dies ist der Standard-Authentifizierungsmechanismus für SPAs, die von derselben Quelle wie die API bereitgestellt werden. Das Sitzungs-Cookie wird vom Spring-Boot-Backend bei der Anmeldung gesetzt und automatisch vom Browser eingefügt. Es gibt keine Token-Verwaltung, keinen Authorization-Header und keine clientseitige Sitzungslogik – der Browser handhabt dies transparent über das credentials-Flag.
Verwendung im Frontend
Dieses Modul wird von praktisch jeder React-Komponente importiert, die Daten abruft. Typische Verwendungsmuster:
// In einer React-Komponente:
import { fetchJsonGet, fetchJsonPost } from '../utilities/rest';
// Eine Liste laden
fetchJsonGet('user/list', { search: 'kai' }, (data) => setUsers(data));
// Eine Entität speichern
fetchJsonPost('user/save', { id: 1, username: 'kai' }, (data) => console.log('gespeichert', data));
// Einfaches GET für einen Dateidownload
fetchGet('export/excel', { id: 123 }, () => setDownloading(false));
Verbraucher
Das Modul wird im gesamten React-Frontend importiert – Adressbuch, Kalender, Zeiterfassung, Benutzerverwaltung, Umfragen, Skripte und alle Plugin-Oberflächen. Zum Zeitpunkt der Erstellung dieses Textes importieren etwa 40+ React-Komponenten aus dieser Datei. Jede Änderung an der URL-Konstruktionslogik (z. B. Änderung des REST-Präfix von /rs zu etwas anderem) würde jeden API-Aufruf in der Anwendung betreffen.
Git-Verlauf
| Commit | Was sich in dieser Datei geändert hat |
bf988bc6d | React-Scripts→Vite-Migration. Die Umgebungserkennung wurde von process.env.NODE_ENV === 'development' (CRA-Konvention) auf import.meta.env.DEV && import.meta.env.MODE !== 'test' (Vite-Konvention) geändert. Die hinzugefügte Abfrage MODE !== 'test' verhindert, dass Jest während Testläufen auf localhost:8080 zeigt – ein häufiger Fehler bei der Migration von CRA zu Vite, bei dem der Testumgebungsmodus 'test' und nicht 'development' ist. |
823ef0992 | Defensive Callback-Abfrage in fetchGet. Geändert von .then(() => callback()) zu .then(() => { if (typeof callback === 'function' && callback()); }). Die Funktion fetchGet hat einen optionalen Callback – wenn sie ohne einen solchen aufgerufen wird (z. B. Fire-and-Forget-Anfragen), würde der alte Code einen TypeError: callback is not a function auslösen. Die Abfrage überspringt den Callback stillschweigend, wenn er keine Funktion ist. |
253b9f38b | Code-Formatierungsdurchlauf. Mehrzeilige Fetch-Aufrufketten wurden in einzeilige Aufrufe umgewandelt. Vorher: fetch(\n getServiceURL(...), {\n method: 'GET',\n .... Nachher: fetch(getServiceURL(...), { method: 'GET', .... Reduziertes visuelles Rauschen, keine Verhaltensänderung. |
ac30e55f7 | ESLint-Unterdrückung hinzugefügt. /* eslint-disable no-alert */ am Anfang der Datei eingefügt. Die Fehlerhandler verwenden alert() für benutzerseitige Fehler, was die empfohlene ESLint-Konfiguration verbietet. Die Unterdrückung erkennt an, dass dies beabsichtigt ist – ein benachrichtigungsmechanismus ohne Abhängigkeiten für ein Modul auf der untersten Ebene des Abhängigkeitsgraphen. |
bbd81edc3 | ESLint-gesteuerte Formatierung. Pfeilfunktionen wurden von params => ... zu (params) => ... geändert (Einzelparameter in Klammern). Der redundante Funktionskörper in getServiceURL wurde entfernt – die Funktion war zuvor mit function getServiceURL(...) { ... } definiert, und die Neuschreibung war eine reine Formatierungsänderung. |
Designentscheidungen
| Entscheidung | Warum | Kompromiss |
Kein Axios, einfaches fetch | Keine Abhängigkeiten. Das Modul hat ~80 Zeilen und deckt alle Anforderungen des Frontends ab. Das Hinzufügen von Axios (~14 KB gzippt) für Interceptoren und automatisches JSON-Parsing ist für drei HTTP-Methoden nicht gerechtfertigt. | Keine Anfrage-/Antwort-Interceptoren – eine Aktualisierung des Authentifizierungstokens würde benutzerdefinierten Code erfordern. Derzeit nicht erforderlich, da die Authentifizierung cookie-basiert ist. |
| Callback-basiert statt Promise-Rückgabe | Historisch bedingt. Als dieses Modul geschrieben wurde (2019), verwendete das Frontend Klassenkomponenten mit this.setState in Callbacks. Die Rückgabe von Promises würde in jedem Verbraucher .then(data => this.setState(...)) erfordern – das Callback-Muster war damals einfacher. | Schwerer zu verketten, schwerer mit async/await zu verwenden. Verbraucher müssen Callbacks übergeben, auch wenn sie nur die Daten möchten. Eine moderne Neufassung würde Promises zurückgeben. |
alert() für die Fehlerbehandlung | Keine Abhängigkeiten, funktioniert in jedem Kontext. Ein ordentliches Toast-System würde das Importieren von React/Redux erfordern – was ein zirkuläres Abhängigkeitsrisiko darstellt. | Unschön, nicht i18n-fähig, blockiert den UI-Thread. Ein verzögerter Fehlerereignis-Emitter wäre besser: window.dispatchEvent(new CustomEvent('rest:error', {detail: error})) – verarbeitet von einer React-Fehlergrenze oder einer Toast-Komponente. |
saveData-bewusstes Debounce | Fortschrittliche Verbesserung. Benutzer mit getakteten Verbindungen (mobil, Entwicklungsländer) erhalten ein langsameres Debounce – weniger API-Aufrufe, weniger Datennutzung. Benutzer mit unbegrenzten Verbindungen erhalten ein schnelleres Debounce – reaktionsschnellere Autovervollständigung. | Die Browserunterstützung ist begrenzt – navigator.connection ist nur für Chromium verfügbar. Firefox-/Safari-Benutzer erhalten immer den Standardwert von 250 ms. Die abwärtskompatible Fehlerbehandlung funktioniert korrekt. |