Hallo Aragorn,
grundsätzlich funktioniert fetch() auf Basis von Promises. Wir haben dazu im Wiki ein Tutorial, das sich schon länger im Zustand der Überarbeitung befindet.
Der eigentliche Ablauf von fetch() geschieht in drei Phasen.
- Request senden
- Header empfangen
- Inhalt empfangen
Die Übergänge von 1 nach 2 und von 2 nach 3 werden mit Promises realisiert. Promises gab es in jQuery schon bevor sie als Javascript-Kernfeature eingeführt wurden, sie heißen dort allerdings Deferred Objects und haben ein etwas anderes API. Die .done()-Methode in jQuery heißt bei Promises .then(), und JavaScript bietet mit async/await ein Pfund Syntaxzucker, um Promises besser handhaben zu können.
1. Request senden
Der Aufruf lautet fetch(url, options). Standardmäßig, d.h. ohne Options, macht fetch einen GET-Request. Du möchtest einen POST und dafür brauchst Du mindestens 2 Einträge im options-Objekt (das ist ein einfaches, flaches Objekt):
function checkLoginData(mailAddress, password) {
const loginBody = new FormData();
loginBody.set("eMail", mailAddress);
loginBody.set("passWd", password);
fetch("serverLogin.php", { method: "POST", body: loginBody });
}
Es gibt diverse Möglichkeiten, die Daten für Fetch zu übertragen. Ein FormData-Objekt kommt dem data-Objekt, das Du bei $.ajax gesehen hast, am nächsten.
Was man alles als Optionen übergeben kann, ist bei Mozilla als RequestInit-Objekt beschrieben. Vorsicht, Gefahr des mentalen Durchbrennens, da steht eine MENGE technisches Zeug. Für FormData-Übertragungen an den eigenen Server genügt meistens method und body, ggf. kannst Du noch HTTP Request-Header hinzufügen. Aber die technische Komplexität steckt im AJAX-Prinzip, jQuery oder (ganz klassisch) XMLHttpRequest bewahren Dich auch nicht davor.
fetch liefert ein Promise zurück, auf das Du nun warten musst. Insbesondere muss der checkLoginData-Aufrufer darauf warten, bevor er die Antwort auswertet. "Warten" ist etwas, das JavaScript überhaupt nicht mag, deswegen verwendet man Callback-Funktionen, die man in Promises kapselt, oder die async/await Syntax, die unter der Haube das Gleiche tut.
2. Header empfangen
Das erste Promise erfüllt sich (d.h. der erste then-Callback wird aufgerufen), sobald die Antwort des POST-Requests eingetroffen ist und die HTTP Header der Antwort bereitstehen. Der Datenteil hängt jetzt noch auf der Leitung! Der then-Callback bekommt ein Response-Objekt übergeben und kann es auswerten.
fetch("serverLogin.php", { method: "POST", body: loginBody })
.then(function(response) {
if (response.ok) // Statuscode 200-299
return response.text();
else
throw new Error(`Login-Fehler! Status: ${response.status} ${response.statusText}`)
})
Dieser erste .then()-Callback testet den Erfolg des Requests und legt fest, wie die Antwortdaten (der Inhalt der POST-Antwort) zu verarbeiten ist. In deinem Fall ist es plain text (du machst einen einfachen echo), für die Zukunft solltest Du einen JSON-String zurückgeben und im PHP application/json als Content-Type setzen. Auf JavaScript-Seite rufst Du dann response.json() statt response.text() auf, dann bekommst Du direkt das geparste Objekt.
Im Fehlerfall wirft das Beispiel ein Error-Objekt. Dieser String in Backticks ist ein Template-Literal, falls Du das noch nicht kennst.
3. Daten empfangen
Die then-Methode des fetch-Promise gibt Dir ein neues Promise zurück. Wie sich dieses erfüllt, hängt davon ab, was der then-Callback tut.
Der Aufruf von response.text() liefert seinerseits ein Promise. Dieses wird an then() zurückgegeben, und then() koppelt sich daran. D.h. sobald der Browser den Antworttext eingelesen hat, wird das text()-Promise erfüllt und damit auch das then()-Promise. Um darauf reagieren zu können, rufen wir auf dem then()-Promise noch einmal then() auf. Diesmal bekommen wir den Wert übergeben, den der vorige then-Callback zurückgegeben hat. Weil das ein Promise war, bekommen wir denjenigen Wert, den response.text() als Ergebnis im Promise hinterlegt hat.
fetch("serverLogin.php", { method: "POST", body: loginBody })
.then(function(response) {
if (response.ok) // Statuscode 200-299
return response.text();
else
throw new Error(`Login-Fehler! Status: ${response.status} ${response.statusText}`)
})
.then(function(text) {
console.log(text);
});
Die Frage ist nun, was man damit tun will. Du möchtest ja eigentlich dem Aufrufer mitteilen, ob der Login erfolgreich war oder nicht. Das eigentliche serverLogin.php sollte also schauen, ob das Passwort zur Mailadresse passt (siehe unser PHP Tutorial zu Konzepten eines Login-Systems und dann nur true oder false zurückgeben. Oder 1 und 0. Mehr nicht, man darf aus der Antwort nicht schließen können, ob die Mailadresse oder das Passwort falsch ist. Im Falle einer unbekannten Mailadresse solltest Du password_verify() trotzdem aufrufen, mit Dummydaten, damit die Requestlaufzeit gleich ist. Andernfalls könnte jemand per Laufzeitstatistik auf den Unterschied "falsche Mail" oder "falsches Passwort" zu schließen versuchen.
Du solltest also im zweiten then() die Antwort des Servers anschauen und daraus einen boolschen Wert machen, und dann das Ergebnis dieses Aufrufbandwurms an den Aufrufer zurückgeben.
function checkLoginData(mailAddress, password) {
const loginBody = new FormData();
loginBody.set("eMail", mailAddress);
loginBody.set("passWd", password);
return fetch("serverLogin.php", { method: "POST", body: loginBody })
.then(function(response) {
if (response.ok) // Statuscode 200-299
return response.text();
else
throw new Error(`Login-Fehler! Status: ${response.status} ${response.statusText}`)
})
.then(function(text) {
return (text == 'ok');
});
}
...
checkLoginData(mail, password)
.then(function(success) {
if (success) {
// was auch immer du bei erfolgreichem Login tun willst
}
else {
// was auch immer du bei nicht erfolgreichem Login tun willst
}
})
.catch(error) {
// Hier kommst Du hin, wenn etwas schiefging
}
D.h. deine checkLoginData()-Funktion gibt jetzt ihrerseits ein Promise zurück, und der Aufrufer kann mit .then() auf dieses Promise warten. Der then-Callback des Aufrufers bekommt deinen Rückgabewert. Das muss man so machen, weil checkLoginData() mit diesem Promise zurückkehrt, sobald der fetch-Request weggeschickt wurde. Das ist bei jQuery übrigens genauso, auch dort musst du den asynchronen Ablauf beachten und auf Deferred-Objekte warten.
Sobald fetch oder einer der beiden then-Callbacks mit throw abbrechen, wird das zugehörige Promise abgelehnt und weitere, durch then-Aufrufe angehängte Folgepromises, ebenfalls. Auf abgelehnte Promises reagiert man mit .catch() (in jQuery mit .fail() oder .catch()) und bekommt das geworfene Objekt als Argument. Was auch immer Du dann damit tust.
4. Alternative Schreibweise mit async und await
Das ganze lässt sich visuell von Callbacks befreien, wenn man den async/await Syntaxzucker einsetzt. Technisch befinden sich immer noch Callbacks und Promises unter der Haube, aber JavaScript baut das automatisch für dich.
async function checkLoginData(mailAddress, password) {
const loginBody = new FormData();
loginBody.set("eMail", mailAddress);
loginBody.set("passWd", password);
const response = await fetch("serverLogin.php", { method: "POST", body: loginBody });
if (response.ok) { // Statuscode 200-299
const antwort = await response.text();
return (antwort == 'ok');
// oder als Einzeiler:
return (await response.text() == 'ok');
}
else {
throw new Error(`Login-Fehler! Status: ${response.status} ${response.statusText}`);
}
)
...
// Der folgende Code muss seinerseits in einer async-Funktion stehen oder
// Top-Level Code eines ECMAScript-Moduls sein (script type="module")!
try {
if (await checkLoginData(mail, passwort)) {
// was auch immer du bei erfolgreichem Login tun willst
}
else {
// was auch immer du bei nicht erfolgreichem Login tun willst
}
}
catch(error) {
// Hier kommst Du hin, wenn etwas schiefging
}
async-Funktionen geben nicht den Wert zurück, den Du bei return angibst, sondern ein Promise, das sich erfüllt, sobald die async-Funktion zum return kommt. Das Promise wird in dem Moment zurückgegeben, wenn die async-Funktion ihren ersten eigenen await macht (siehe meine Promise-Tutorial Überarbeitung).
Wird das Promise erfüllt (d.h. die Funktion, die das Promise lieferte, war erfolgreich), bekommst Du den Erfüllungswert von await zurück. Wird es zurückgewiesen (entweder durch explizites Zurückweisen im Promise-API oder durch Werfen einer Exception), dann wirft await seinerseits eine Exception und deshalb muss da ein try/catch drumherum.
Ich hoffe, das war jetzt nicht zu unverständlich.
Rolf
sumpsi - posui - obstruxi