CycleTracker: JavaScript-Funktionalität
Im vorherigen Abschnitt haben wir das HTML und CSS für CycleTracker geschrieben und so eine statische Version unserer Web-App erstellt. In diesem Abschnitt werden wir das JavaScript schreiben, das benötigt wird, um aus dem statischen HTML eine voll funktionsfähige Webanwendung zu machen.
Falls Sie es noch nicht getan haben, kopieren Sie das HTML und das CSS und speichern Sie sie in Dateien mit den Namen index.html
und style.css
.
Die letzte Zeile in der HTML-Datei ruft die JavaScript-Datei app.js
auf. Dieses Skript erstellen wir in diesem Abschnitt. In dieser Lektion schreiben wir clientseitigen JavaScript-Code, um Formularübermittlungen zu erfassen, die übermittelten Daten lokal zu speichern und den Abschnitt mit den vergangenen Perioden zu füllen.
Am Ende dieser Lektion haben Sie eine voll funktionsfähige App. In zukünftigen Lektionen werden wir die App schrittweise erweitern, um eine vollständig installierbare PWA zu erstellen, die sogar dann funktioniert, wenn der Benutzer offline ist.
JavaScript-Aufgabe
Wenn ein Benutzer die Seite besucht, prüfen wir, ob bereits Daten im lokalen Speicher vorhanden sind. Beim ersten Besuch eines Benutzers auf der Seite sind keine Daten vorhanden. Wenn ein neuer Benutzer zwei Daten auswählt und das Formular übermittelt, müssen wir:
- Einen
<h2>Vergangene Perioden</h2>
-Header erstellen - Ein
<ul>
erstellen - Die
<ul>
mit einem einzigen<li>
füllen, das Informationen über diesen Zyklus enthält - Die Daten im lokalen Speicher speichern
Bei jeder weiteren Formularübermittlung müssen wir:
- Den neuen Menstruationszyklus zur aktuellen Liste hinzufügen
- Die Liste nach Datum sortieren
- Die
<ul>
mit der neuen Liste füllen, ein<li>
pro Zyklus - Die Daten zu unserem gespeicherten lokalen Speicher hinzufügen
Bestehende Benutzer haben bereits Daten im lokalen Speicher. Wenn ein Benutzer mit demselben Browser auf demselben Gerät zu unserer Webseite zurückkehrt, müssen wir:
- Die Daten aus dem lokalen Speicher abrufen
- Einen
<h2>Vergangene Perioden</h2>
-Header erstellen - Ein
<ul>
erstellen - Die
<ul>
mit einem<li>
für jeden im lokalen Speicher gespeicherten Menstruationszyklus füllen.
Dies ist eine Demonstrationsanwendung auf Anfänger-Niveau. Ziel ist es, die Grundlagen der Umwandlung einer Webanwendung in eine PWA zu vermitteln. Diese Anwendung enthält nicht notwendige Funktionen wie Formularvalidierung, Fehlerprüfung, Bearbeitungs- oder Löschfunktionen usw. Es steht Ihnen frei, die behandelten Funktionen zu erweitern und die Lektion und die Anwendungen an Ihre Lernziele und Anwendungsbedürfnisse anzupassen.
Formularübermittlung
Die Seite enthält ein <form>
mit Datumsauswahl zur Auswahl der Start- und Enddaten jedes Menstruationszyklus. Die Datumsauswahlen sind <input>
vom Typ date mit dem id
von start-date
und end-date
.
Das Formular hat keine Methode oder Aktion. Stattdessen fügen wir dem Formular einen Event-Listener mit addEventListener()
hinzu. Wenn der Benutzer versucht, das Formular abzusenden, verhindern wir die Übermittlung des Formulars, speichern den neuen Menstruationszyklus, rendern diese Periode zusammen mit den vorherigen und setzen dann das Formular zurück.
// create constants for the form and the form controls
const newPeriodFormEl = document.getElementsByTagName("form")[0];
const startDateInputEl = document.getElementById("start-date");
const endDateInputEl = document.getElementById("end-date");
// Listen to form submissions.
newPeriodFormEl.addEventListener("submit", (event) => {
// Prevent the form from submitting to the server
// since everything is client-side.
event.preventDefault();
// Get the start and end dates from the form.
const startDate = startDateInputEl.value;
const endDate = endDateInputEl.value;
// Check if the dates are invalid
if (checkDatesInvalid(startDate, endDate)) {
// If the dates are invalid, exit.
return;
}
// Store the new period in our client-side storage.
storeNewPeriod(startDate, endDate);
// Refresh the UI.
renderPastPeriods();
// Reset the form.
newPeriodFormEl.reset();
});
Nachdem wir die Übermittlung des Formulars mit preventDefault()
verhindert haben, führen wir folgende Schritte durch:
- Benutzereingaben validieren; bei ungültigen Eingaben beenden,
- Die neue Periode speichern, indem wir Daten in localStorage abrufen, analysieren, anhängen, sortieren, als String umwandeln und erneut speichern,
- Formulardaten rendern zusammen mit den Daten vorheriger Menstruationszyklen und einem Abschnittsheader und
- Das Formular mithilfe der HTMLFormElement-Methode
reset()
zurücksetzen
Benutzereingaben validieren
Wir überprüfen, ob die Daten ungültig sind. Wir führen eine minimale Fehlerüberprüfung durch. Wir stellen sicher, dass keins der Daten null ist, was das required
-Attribut verhindern sollte. Wir überprüfen auch, dass das Startdatum nicht größer ist als das Enddatum. Wenn ein Fehler auftritt, löschen wir das Formular.
function checkDatesInvalid(startDate, endDate) {
// Check that end date is after start date and neither is null.
if (!startDate || !endDate || startDate > endDate) {
// To make the validation robust we could:
// 1. add error messaging based on error type
// 2. Alert assistive technology users about the error
// 3. move focus to the error location
// instead, for now, we clear the dates if either
// or both are invalid
newPeriodFormEl.reset();
// as dates are invalid, we return true
return true;
}
// else
return false;
}
In einer robusteren Version dieser App würden wir mindestens Fehlermeldungen einfügen, die den Benutzer auf einen Fehler hinweisen. Eine gute Anwendung würde den Benutzer darüber informieren, was der Fehler ist, den Fokus auf das fehlerhafte Formularfeld legen und ARIA live regions verwenden, um Benutzer unterstützender Technologien auf den Fehler aufmerksam zu machen.
Lokaler Speicher
Wir verwenden die Web Storage API, insbesondere window.localStorage, um Start- und Enddatenpaare in einem als String umgewandelten JSON-Objekt zu speichern.
Lokalspeicherung hat einige Einschränkungen, genügt jedoch für unsere App-Anforderungen. Wir verwenden localStorage, um es einfach und ausschließlich clientseitig zu halten. Das bedeutet, dass die Daten nur auf einem Browser auf einem einzigen Gerät gespeichert werden. Wenn die Browserdaten gelöscht werden, gehen auch alle lokal gespeicherten Perioden verloren. Was für viele Anwendungen als Einschränkung erscheinen mag, kann im Falle dieser Anwendung ein Vorteil sein, da Menstruationsdatendaten persönlich sind, und der Benutzer einer solchen App sehr wohl um den Datenschutz besorgt sein könnte.
Für eine robustere Anwendung bieten andere clientseitige Speicheroptionen wie IndexedDB (IDB) und, wie später besprochen, Service Worker, eine bessere Leistung.
Beschränkungen von localStorage
umfassen:
- Begrenzter Datenspeicher:
localStorage
ist auf 5 MB Daten pro Origin beschränkt. Unser Speicherbedarf ist viel geringer. - Speichert nur Zeichenfolgen:
localStorage
speichert Daten als Zeichenfolgen-Paar für Schlüssel und Wert. Unsere Start- und Enddaten werden als JSON-Objekt gespeichert, das als Zeichenfolge geparst wird. Für komplexere Daten wäre ein robusterer Speichermechanismus wie IDB erforderlich. - Kann zu schlechter Leistung führen: Das Abrufen und Speichern von und in localStorage erfolgt synchron im Haupt-Thread. Wenn der Haupt-Thread ausgelastet ist, sind Apps nicht mehr reaktionsfähig und wirken eingefroren. Angesichts der begrenzten Natur dieser App ist dieser Blip des schlechten Benutzererlebnisses vernachlässigbar.
- Nur für den Haupt-Thread verfügbar: Neben den Leistungsproblemen durch die Auslastung des Haupt-Threads haben Service Worker keinen Zugriff auf den Haupt-Thread, was bedeutet, dass der Service Worker nicht direkt auf die lokalen Speicherdateien zugreifen kann.
Daten abrufen, anhängen, sortieren und erneut speichern
Da wir localStorage, das aus einem einzigen String besteht, verwenden, rufen wir den JSON-String der Daten aus dem lokalen Speicher ab, analysieren die JSON-Daten (falls vorhanden), fügen das neue Datumpaar dem vorhandenen Array hinzu, sortieren die Daten, wandeln das JSON-Objekt wieder in einen String um und speichern diesen String erneut in localStorage
.
Dieser Prozess erfordert die Erstellung einiger Funktionen:
// Add the storage key as an app-wide constant
const STORAGE_KEY = "period-tracker";
function storeNewPeriod(startDate, endDate) {
// Get data from storage.
const periods = getAllStoredPeriods();
// Add the new period object to the end of the array of period objects.
periods.push({ startDate, endDate });
// Sort the array so that periods are ordered by start date, from newest
// to oldest.
periods.sort((a, b) => {
return new Date(b.startDate) - new Date(a.startDate);
});
// Store the updated array back in the storage.
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(periods));
}
function getAllStoredPeriods() {
// Get the string of period data from localStorage
const data = window.localStorage.getItem(STORAGE_KEY);
// If no periods were stored, default to an empty array
// otherwise, return the stored data as parsed JSON
const periods = data ? JSON.parse(data) : [];
return periods;
}
Daten auf dem Bildschirm rendern
Der letzte Schritt unserer Anwendung besteht darin, die Liste der vergangenen Perioden zusammen mit einem Überschrift auf dem Bildschirm darzustellen.
In unserem HTML haben wir einen <section id="past-periods">
-Platzhalter hinzugefügt, um die Überschrift und die Liste der vergangenen Perioden zu enthalten.
Fügen Sie das Containerelement zur Liste der Inhalte oben im Skript hinzu.
const pastPeriodContainer = document.getElementById("past-periods");
Wir rufen den geparsten String vergangener Perioden oder ein leeres Array ab. Wenn dieser leer ist, beenden wir. Wenn vergangene Perioden existieren, löschen wir die aktuellen Inhalte aus dem vergangenen Perioden-Container. Wir erstellen eine Überschrift und eine ungeordnete Liste. Wir durchlaufen die vergangenen Perioden und fügen Listenelemente mit formatierten von- und bis-Daten hinzu.
function renderPastPeriods() {
// get the parsed string of periods, or an empty array.
const periods = getAllStoredPeriods();
// exit if there are no periods
if (periods.length === 0) {
return;
}
// Clear the list of past periods, since we're going to re-render it.
pastPeriodContainer.textContent = "";
const pastPeriodHeader = document.createElement("h2");
pastPeriodHeader.textContent = "Past periods";
const pastPeriodList = document.createElement("ul");
// Loop over all periods and render them.
periods.forEach((period) => {
const periodEl = document.createElement("li");
periodEl.textContent = `From ${formatDate(
period.startDate,
)} to ${formatDate(period.endDate)}`;
pastPeriodList.appendChild(periodEl);
});
pastPeriodContainer.appendChild(pastPeriodHeader);
pastPeriodContainer.appendChild(pastPeriodList);
}
function formatDate(dateString) {
// Convert the date string to a Date object.
const date = new Date(dateString);
// Format the date into a locale-specific string.
// include your locale for better user experience
return date.toLocaleDateString("en-US", { timeZone: "UTC" });
}
Vergangene Perioden beim Laden rendern
Wenn das aufgeschobene JavaScript beim Laden der Seite ausgeführt wird, rendern wir die vergangenen Perioden, falls vorhanden.
// Start the app by rendering the past periods.
renderPastPeriods();
Komplettes JavaScript
Ihre app.js
-Datei sollte ähnlich aussehen wie dieses JavaScript:
const newPeriodFormEl = document.getElementsByTagName("form")[0];
const startDateInputEl = document.getElementById("start-date");
const endDateInputEl = document.getElementById("end-date");
const pastPeriodContainer = document.getElementById("past-periods");
// Add the storage key as an app-wide constant
const STORAGE_KEY = "period-tracker";
// Listen to form submissions.
newPeriodFormEl.addEventListener("submit", (event) => {
event.preventDefault();
const startDate = startDateInputEl.value;
const endDate = endDateInputEl.value;
if (checkDatesInvalid(startDate, endDate)) {
return;
}
storeNewPeriod(startDate, endDate);
renderPastPeriods();
newPeriodFormEl.reset();
});
function checkDatesInvalid(startDate, endDate) {
if (!startDate || !endDate || startDate > endDate) {
newPeriodFormEl.reset();
return true;
}
return false;
}
function storeNewPeriod(startDate, endDate) {
const periods = getAllStoredPeriods();
periods.push({ startDate, endDate });
periods.sort((a, b) => {
return new Date(b.startDate) - new Date(a.startDate);
});
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(periods));
}
function getAllStoredPeriods() {
const data = window.localStorage.getItem(STORAGE_KEY);
const periods = data ? JSON.parse(data) : [];
console.dir(periods);
console.log(periods);
return periods;
}
function renderPastPeriods() {
const pastPeriodHeader = document.createElement("h2");
const pastPeriodList = document.createElement("ul");
const periods = getAllStoredPeriods();
if (periods.length === 0) {
return;
}
pastPeriodContainer.textContent = "";
pastPeriodHeader.textContent = "Past periods";
periods.forEach((period) => {
const periodEl = document.createElement("li");
periodEl.textContent = `From ${formatDate(
period.startDate,
)} to ${formatDate(period.endDate)}`;
pastPeriodList.appendChild(periodEl);
});
pastPeriodContainer.appendChild(pastPeriodHeader);
pastPeriodContainer.appendChild(pastPeriodList);
}
function formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", { timeZone: "UTC" });
}
renderPastPeriods();
Sie können die voll funktionsfähige CycleTracker-Menstruations-Tracking-Web-App ausprobieren und den Quellcode der Web-App auf GitHub ansehen. Ja, sie funktioniert, aber es ist noch keine PWA.
Als Nächstes
Im Kern ist eine PWA eine Webanwendung, die installiert und schrittweise erweitert werden kann, um offline zu arbeiten. Jetzt, da wir eine voll funktionsfähige Webanwendung haben, fügen wir die Funktionen hinzu, die zur Umwandlung in eine PWA erforderlich sind, einschließlich der Manifest-Datei, einer sicheren Verbindung und einem Service Worker.
Zuallererst erstellen wir die Manifest-Datei von CycleTracker, einschließlich Identität, Erscheinung und Ikonographie für unsere CycleTracker PWA.