Entwerfen und Erstellen einer progressiven Webanwendung ohne Framework (Teil 2)
Veröffentlicht: 2022-03-10Die Daseinsberechtigung dieses Abenteuers bestand darin, Ihren bescheidenen Autor ein wenig in den Disziplinen des visuellen Designs und der JavaScript-Programmierung zu fordern. Die Funktionalität der Anwendung, für deren Erstellung ich mich entschieden hatte, war einer To-do-Anwendung nicht unähnlich. Es ist wichtig zu betonen, dass dies keine Übung in originellem Denken war. Das Ziel war weit weniger wichtig als der Weg.
Du willst wissen, wie die Bewerbung geendet ist? Richten Sie Ihren Telefonbrowser auf https://io.benfrain.com.
Hier ist eine Zusammenfassung dessen, was wir in diesem Artikel behandeln werden:
- Der Projektaufbau und warum ich mich für Gulp als Build-Tool entschieden habe;
- Anwendungsdesignmuster und was sie in der Praxis bedeuten;
- Anwendungsstatus speichern und visualisieren;
- wie CSS auf Komponenten beschränkt wurde;
- welche UI/UX-Feinheiten wurden verwendet, um die Dinge „App-ähnlicher“ zu machen;
- Wie sich der Auftrag durch Iteration verändert hat.
Beginnen wir mit den Build-Tools.
Werkzeuge bauen
Um meine grundlegenden Tools von TypeScipt und PostCSS zum Laufen zu bringen und eine anständige Entwicklungserfahrung zu schaffen, bräuchte ich ein Build-System.
In meinem Hauptberuf habe ich in den letzten fünf Jahren Schnittstellenprototypen in HTML/CSS und in geringerem Umfang in JavaScript erstellt. Bis vor kurzem habe ich Gulp mit einer beliebigen Anzahl von Plugins fast ausschließlich verwendet, um meine ziemlich bescheidenen Build-Anforderungen zu erfüllen.
Normalerweise muss ich CSS verarbeiten, JavaScript oder TypeScript in weiter unterstütztes JavaScript konvertieren und gelegentlich verwandte Aufgaben wie das Minimieren der Codeausgabe und das Optimieren von Assets ausführen. Die Verwendung von Gulp hat es mir immer ermöglicht, diese Probleme mit Souveränität zu lösen.
Für diejenigen, die nicht vertraut sind, können Sie mit Gulp JavaScript schreiben, um „etwas“ mit Dateien in Ihrem lokalen Dateisystem zu tun. Um Gulp zu verwenden, haben Sie normalerweise eine einzelne Datei (namens gulpfile.js
) im Stammverzeichnis Ihres Projekts. Mit dieser JavaScript-Datei können Sie Aufgaben als Funktionen definieren. Sie können „Plugins“ von Drittanbietern hinzufügen, die im Wesentlichen weitere JavaScript-Funktionen sind, die sich mit bestimmten Aufgaben befassen.
Eine beispielhafte Gulp-Aufgabe
Eine beispielhafte Gulp-Aufgabe könnte die Verwendung eines Plugins sein, um PostCSS für die Verarbeitung in CSS zu nutzen, wenn Sie ein Authoring-Stylesheet ändern (gulp-postcss). Oder kompilieren Sie TypeScript-Dateien in Vanilla-JavaScript (gulp-typescript), während Sie sie speichern. Hier ist ein einfaches Beispiel dafür, wie Sie eine Aufgabe in Gulp schreiben. Diese Aufgabe verwendet das Gulp-Plugin „del“, um alle Dateien in einem Ordner namens „build“ zu löschen:
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
Das require
weist das del
-Plugin einer Variablen zu. Dann wird die Methode gulp.task
aufgerufen. Wir benennen die Aufgabe mit einem String als erstem Argument („clean“) und führen dann eine Funktion aus, die in diesem Fall die „del“-Methode verwendet, um den ihr als Argument übergebenen Ordner zu löschen. Die Sternchen-Symbole dort sind „Glob“-Muster, die im Wesentlichen „jede Datei in jedem Ordner“ des Build-Ordners aussagen.
Gulp-Aufgaben können viel komplizierter werden, aber im Wesentlichen ist das die Mechanik, wie die Dinge gehandhabt werden. Die Wahrheit ist, dass Sie mit Gulp kein JavaScript-Experte sein müssen, um zurechtzukommen; Kenntnisse zum Kopieren und Einfügen der Klasse 3 sind alles, was Sie brauchen.
Ich war all die Jahre bei Gulp als meinem Standard-Build-Tool/Task-Runner geblieben, mit einer Richtlinie von „Wenn es nicht kaputt ist; versuchen Sie nicht, es zu beheben'.
Ich war jedoch besorgt, dass ich in meinen Wegen stecken blieb. Es ist eine leichte Falle, in die man tappt. Zuerst macht man jedes Jahr am selben Ort Urlaub und weigert sich dann, neue Modetrends anzunehmen, bevor man sich schließlich und standhaft weigert, neue Bauwerkzeuge auszuprobieren.
Ich hatte im Internet viel über „Webpack“ geredet und hielt es für meine Pflicht, ein Projekt mit dem neumodischen Toast der Frontend-Entwickler cool-kids auszuprobieren.
Webpaket
Ich erinnere mich genau, dass ich mit großem Interesse zur Website webpack.js.org gesprungen bin. Die erste Erklärung, was Webpack ist und tut, begann so:
import bar from './bar';
Sag was? Mit den Worten von Dr. Evil: „Wirf mir einen verdammten Knochen hierher, Scott“.
Ich weiß, dass ich damit fertig werden muss, aber ich habe eine Abneigung gegen alle Codierungserklärungen entwickelt, die „foo“, „bar“ oder „baz“ erwähnen. Das und das völlige Fehlen einer prägnanten Beschreibung, wofür Webpack eigentlich gedacht war, ließ mich vermuten, dass es vielleicht nichts für mich war.
Etwas weiter in die Webpack-Dokumentation eintauchend, wurde eine etwas weniger undurchsichtige Erklärung angeboten: „Im Kern ist Webpack ein statischer Modul-Bundler für moderne JavaScript-Anwendungen“.
Hmmm. Statischer Modulbundler. War es das, was ich wollte? Ich war nicht überzeugt. Ich las weiter, aber je mehr ich las, desto weniger klar war ich. Damals gingen mir Konzepte wie Abhängigkeitsgraphen, das Neuladen von Hot-Modulen und Einstiegspunkte im Wesentlichen verloren.
Ein paar Abende später, als ich Webpack recherchierte, gab ich jede Vorstellung auf, es zu verwenden.
Ich bin mir sicher, dass Webpack in der richtigen Situation und in erfahreneren Händen immens leistungsfähig und angemessen ist, aber für meine bescheidenen Bedürfnisse schien es ein völliger Overkill zu sein. Modulbündelung, Tree-Shaking und Hot-Module Reload klangen großartig; Ich war einfach nicht davon überzeugt, dass ich sie für meine kleine „App“ brauchte.
Also zurück zu Gulp.
In Bezug auf das Thema, Dinge nicht um der Veränderung willen zu ändern, war eine weitere Technologie, die ich evaluieren wollte, Yarn over NPM für die Verwaltung von Projektabhängigkeiten. Bis zu diesem Zeitpunkt hatte ich immer NPM verwendet und Yarn wurde als bessere und schnellere Alternative angepriesen. Ich habe nicht viel über Yarn zu sagen, außer wenn Sie derzeit NPM verwenden und alles in Ordnung ist, müssen Sie sich nicht die Mühe machen, Yarn auszuprobieren.
Ein Tool, das für mich zu spät kam, um es für diese Anwendung zu bewerten, ist Parceljs. Ohne Konfiguration und einem BrowserSync-ähnlichen Browser-Neuladen, habe ich seitdem einen großartigen Nutzen darin gefunden! Außerdem wurde mir zur Verteidigung von Webpack gesagt, dass ab Version 4 von Webpack keine Konfigurationsdatei erforderlich ist. Anekdotenhafterweise entschied sich in einer neueren Umfrage, die ich auf Twitter durchgeführt habe, von den 87 Befragten mehr als die Hälfte für Webpack gegenüber Gulp, Parcel oder Grunt.
Ich habe meine Gulp-Datei mit grundlegenden Funktionen gestartet, um sie zum Laufen zu bringen.
Eine „Standard“-Aufgabe würde die „Quell“-Ordner von Stylesheets und TypeScript-Dateien überwachen und sie zusammen mit dem grundlegenden HTML und den zugehörigen Quellkarten in einen build
-Ordner kompilieren.
Ich habe BrowserSync auch mit Gulp zum Laufen gebracht. Ich weiß vielleicht nicht, was ich mit einer Webpack-Konfigurationsdatei machen soll, aber das bedeutet nicht, dass ich eine Art Tier bin. Den Browser manuell aktualisieren zu müssen, während mit HTML/CSS iteriert wird, ist soooo 2010, und BrowserSync gibt Ihnen das kurze Feedback und die Iterationsschleife, die für die Front-End-Codierung so nützlich ist.
Hier ist die grundlegende Gulp-Datei vom 11.6.2017
Sie können sehen, wie ich das Gulpfile näher am Ende des Versands optimiert habe, indem ich eine Verkleinerung mit ugilify hinzugefügt habe:
Projektstruktur
Als Folge meiner Technologieentscheidungen definierten sich einige Elemente der Codeorganisation für die Anwendung von selbst. Eine gulpfile.js
im Stammverzeichnis des Projekts, ein node_modules
Ordner (in dem Gulp Plugin-Code speichert), ein preCSS
Ordner für die Authoring-Stylesheets, ein ts
-Ordner für die TypeScript-Dateien und ein build
-Ordner für den kompilierten Code zum Leben.
Die Idee war, eine index.html
zu haben, die die „Hülle“ der Anwendung enthält, einschließlich aller nicht-dynamischen HTML-Strukturen und dann Links zu den Stilen und der JavaScript-Datei, die die Anwendung zum Laufen bringen würden. Auf der Festplatte würde es ungefähr so aussehen:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
Das Konfigurieren von BrowserSync zum Anzeigen dieses build
-Ordners bedeutete, dass ich meinen Browser auf localhost:3000
richten konnte, und alles war gut.
Mit einem grundlegenden Build-System, einer geregelten Dateiorganisation und einigen grundlegenden Designs für den Anfang hatte ich keinen Aufschub mehr, den ich rechtmäßig verwenden konnte, um mich daran zu hindern, das Ding tatsächlich zu bauen!
Bewerbung schreiben
Das Prinzip, wie die Anwendung funktionieren würde, war folgendes. Es würde einen Datenspeicher geben. Wenn das JavaScript geladen wurde, würde es diese Daten laden, jeden Player in den Daten durchlaufen, den HTML-Code erstellen, der erforderlich ist, um jeden Player als Zeile im Layout darzustellen, und sie in den entsprechenden In/Out-Abschnitt platzieren. Dann würden Interaktionen des Benutzers einen Spieler von einem Zustand in einen anderen bewegen. Einfach.
Als es darum ging, die Anwendung tatsächlich zu schreiben, waren die zwei großen konzeptionellen Herausforderungen, die verstanden werden mussten, die folgenden:
- Wie man die Daten für eine Anwendung so darstellt, dass sie leicht erweitert und manipuliert werden können;
- Wie man die Benutzeroberfläche reagieren lässt, wenn Daten durch Benutzereingaben geändert wurden.
Eine der einfachsten Möglichkeiten, eine Datenstruktur in JavaScript darzustellen, ist die Objektnotation. Dieser Satz liest sich ein wenig nach Informatik. Einfacher gesagt ist ein „Objekt“ im JavaScript-Jargon eine praktische Möglichkeit, Daten zu speichern.
Betrachten Sie dieses JavaScript-Objekt, das einer Variablen namens ioState
(für In/Out State) zugewiesen ist:
var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }
Wenn Sie JavaScript nicht so gut kennen, können Sie wahrscheinlich zumindest verstehen, was los ist: Jede Zeile innerhalb der geschweiften Klammern ist ein Paar aus Eigenschaft (oder „Schlüssel“ im JavaScript-Jargon) und Wert. Sie können alle möglichen Dinge auf einen JavaScript-Schlüssel setzen. Beispielsweise Funktionen, Arrays anderer Daten oder verschachtelte Objekte. Hier ist ein Beispiel:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
Das Endergebnis ist, dass Sie mit dieser Art von Datenstruktur jeden Schlüssel des Objekts abrufen und festlegen können. Wenn wir beispielsweise die Anzahl des ioState-Objekts auf 7 setzen möchten:
ioState.Count = 7;
Wenn wir einen Text auf diesen Wert setzen wollen, funktioniert die Notation so:
aTextNode.textContent = ioState.Count;
Sie können sehen, dass das Abrufen von Werten und das Festlegen von Werten für dieses Zustandsobjekt auf der JavaScript-Seite der Dinge einfach ist. Die Wiedergabe dieser Änderungen in der Benutzeroberfläche ist jedoch weniger der Fall. Dies ist der Hauptbereich, in dem Frameworks und Bibliotheken versuchen, den Schmerz zu abstrahieren.
Wenn es darum geht, die Benutzeroberfläche basierend auf dem Status zu aktualisieren, ist es im Allgemeinen vorzuziehen, das Abfragen des DOM zu vermeiden, da dies im Allgemeinen als suboptimaler Ansatz angesehen wird.
Betrachten Sie die In/Out-Schnittstelle. Normalerweise wird eine Liste potenzieller Spieler für ein Spiel angezeigt. Sie sind vertikal untereinander auf der Seite aufgelistet.
Vielleicht wird jeder Player im DOM mit einem label
repräsentiert, das eine Checkbox input
umschließt. Auf diese Weise würde das Anklicken eines Players den Player auf „In“ umschalten, da das Etikett die Eingabe „markiert“ macht.
Um unsere Schnittstelle zu aktualisieren, haben wir möglicherweise einen „Listener“ für jedes Eingabeelement im JavaScript. Bei einem Klick oder einer Änderung fragt die Funktion das DOM ab und zählt, wie viele unserer Spielereingaben überprüft werden. Auf der Grundlage dieser Zählung würden wir dann etwas anderes im DOM aktualisieren, um dem Benutzer anzuzeigen, wie viele Spieler überprüft werden.
Betrachten wir die Kosten dieser grundlegenden Operation. Wir lauschen auf mehreren DOM-Knoten auf das Klicken/Prüfen einer Eingabe, fragen dann das DOM ab, um zu sehen, wie viele eines bestimmten DOM-Typs geprüft werden, und schreiben dann etwas in das DOM, um dem Benutzer, UI-weise, die Anzahl der Spieler anzuzeigen wir haben nur gezählt.
Die Alternative wäre, den Anwendungsstatus als JavaScript-Objekt im Speicher zu halten. Ein Schaltflächen-/Eingabeklick im DOM könnte lediglich das JavaScript-Objekt aktualisieren und dann, basierend auf dieser Änderung im JavaScript-Objekt, ein Single-Pass-Update aller erforderlichen Schnittstellenänderungen durchführen. Wir könnten die Abfrage des DOM überspringen, um die Spieler zu zählen, da das JavaScript-Objekt diese Informationen bereits enthalten würde.
Damit. Die Verwendung einer JavaScript-Objektstruktur für den Status schien einfach, aber flexibel genug, um den Anwendungsstatus zu jedem beliebigen Zeitpunkt zu kapseln. Die Theorie, wie dies gehandhabt werden könnte, schien auch stichhaltig genug zu sein – das muss es gewesen sein, worum es bei Phrasen wie „Datenfluss in eine Richtung“ ging? Der erste wirkliche Trick wäre jedoch, einen Code zu erstellen, der die Benutzeroberfläche basierend auf Änderungen an diesen Daten automatisch aktualisiert.
Die gute Nachricht ist, dass klügere Leute als ich dieses Zeug bereits herausgefunden haben ( Gott sei Dank! ). Menschen haben Ansätze für diese Art von Herausforderung seit den Anfängen von Anwendungen perfektioniert. Diese Kategorie von Problemen ist das A und O von „Entwurfsmustern“. Der Spitzname „Entwurfsmuster“ klang für mich zunächst esoterisch, aber nachdem ich nur ein wenig gegraben hatte, begann alles weniger nach Computerwissenschaft und mehr nach gesundem Menschenverstand zu klingen.
Designmuster
Ein Entwurfsmuster ist im Informatiklexikon ein vordefinierter und bewährter Weg, um eine allgemeine technische Herausforderung zu lösen. Stellen Sie sich Designmuster als Codierungsäquivalent eines Kochrezepts vor.
Die vielleicht bekannteste Literatur zu Entwurfsmustern ist „Entwurfsmuster: Elemente wiederverwendbarer objektorientierter Software“ aus dem Jahr 1994. Obwohl es sich hier um C++ und Smalltalk handelt, sind die Konzepte übertragbar. Für JavaScript deckt Addy Osmanis „Learning JavaScript Design Patterns“ ähnliches ab. Sie können es hier auch kostenlos online lesen.
Beobachtermuster
Typischerweise werden Entwurfsmuster in drei Gruppen eingeteilt: Kreation, Struktur und Verhalten. Ich suchte nach etwas Behavioral, das dabei hilft, Änderungen in den verschiedenen Teilen der Anwendung zu kommunizieren.
Kürzlich habe ich einen wirklich großartigen Deep-Dive über die Implementierung von Reaktivität in einer App von Gregg Pollack gesehen und gelesen. Hier finden Sie sowohl einen Blogbeitrag als auch ein Video.
Als ich die Anfangsbeschreibung des „Observer“-Musters in Learning JavaScript Design Patterns
las, war ich mir ziemlich sicher, dass es das Muster für mich war. Es wird so beschrieben:
Der Beobachter ist ein Entwurfsmuster, bei dem ein Objekt (das als Subjekt bezeichnet wird) eine Liste von Objekten verwaltet, die von ihm abhängig sind (Beobachter), und diese automatisch über Zustandsänderungen benachrichtigt.
Wenn ein Subjekt Beobachter über etwas Interessantes benachrichtigen muss, sendet es eine Benachrichtigung an die Beobachter (die spezifische Daten in Bezug auf das Thema der Benachrichtigung enthalten kann).
Der Schlüssel zu meiner Aufregung war, dass dies eine Möglichkeit zu bieten schien, Dinge bei Bedarf selbst zu aktualisieren.
Angenommen, der Benutzer hat auf eine Spielerin namens „Betty“ geklickt, um auszuwählen, dass sie für das Spiel „in“ ist. In der Benutzeroberfläche müssen möglicherweise einige Dinge passieren:
- Addiere 1 zum Spielzähler
- Entfernen Sie Betty aus dem „Out“-Pool von Spielern
- Fügen Sie Betty dem In-Pool von Spielern hinzu
Die App müsste auch die Daten aktualisieren, die die Benutzeroberfläche darstellen. Was ich unbedingt vermeiden wollte, war folgendes:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
Das Ziel war ein eleganter Datenfluss, der aktualisiert, was im DOM benötigt wird, wenn und falls die zentralen Daten geändert wurden.
Mit einem Observer-Pattern war es möglich, Updates des Zustands und damit der Benutzeroberfläche recht prägnant zu versenden. Hier ist ein Beispiel, die tatsächliche Funktion, die zum Hinzufügen eines neuen Spielers zur Liste verwendet wird:
function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }
Der für das Observer-Muster relevante Teil ist die io.notify
Methode. Da dies zeigt, dass wir den items
des Anwendungsstatus ändern, möchte ich Ihnen den Beobachter zeigen, der auf Änderungen an "Elementen" gelauscht hat:
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
Wir haben eine Benachrichtigungsmethode, die Änderungen an den Daten vornimmt, und dann Beobachter an diesen Daten, die reagieren, wenn Eigenschaften, an denen sie interessiert sind, aktualisiert werden.
Mit diesem Ansatz könnte die App Observables haben, die auf Änderungen in jeder Eigenschaft der Daten achten und eine Funktion ausführen, wenn eine Änderung auftritt.
Wenn Sie an dem Observer-Muster interessiert sind, für das ich mich entschieden habe, beschreibe ich es hier ausführlicher.
Es gab jetzt einen Ansatz, um die Benutzeroberfläche effektiv basierend auf dem Status zu aktualisieren. Pfirsichfarben. Dies ließ mich jedoch immer noch mit zwei eklatanten Problemen zurück.
Einer war, wie man den Zustand über das Neuladen/Sitzungen von Seiten hinweg speichert, und die Tatsache, dass die Benutzeroberfläche trotz funktionierender Benutzeroberfläche visuell nicht sehr „App-ähnlich“ war. Wenn beispielsweise eine Taste gedrückt wurde, änderte sich die Benutzeroberfläche sofort auf dem Bildschirm. Es war einfach nicht besonders überzeugend.
Befassen wir uns zuerst mit der Speicherseite der Dinge.
Staat retten
Mein primäres Interesse von Seiten der Entwickler, sich damit zu befassen, konzentrierte sich darauf, zu verstehen, wie App-Schnittstellen erstellt und mit JavaScript interaktiv gemacht werden können. Das Speichern und Abrufen von Daten auf einem Server oder das Angehen der Benutzerauthentifizierung und Anmeldungen war „außerhalb des Geltungsbereichs“.
Anstatt mich für die Datenspeicherung an einen Webdienst anzuschließen, habe ich mich daher dafür entschieden, alle Daten auf dem Client zu behalten. Es gibt eine Reihe von Webplattformmethoden zum Speichern von Daten auf einem Client. Ich habe mich für localStorage
entschieden.
Die API für localStorage ist unglaublich einfach. Sie setzen und erhalten Daten wie folgt:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage hat eine setItem
Methode, an die Sie zwei Strings übergeben. Der erste ist der Name des Schlüssels, mit dem Sie die Daten speichern möchten, und der zweite String ist der eigentliche String, den Sie speichern möchten. Die getItem
Methode nimmt einen String als Argument, der Ihnen alles zurückgibt, was unter diesem Schlüssel in localStorage gespeichert ist. Schön und einfach.
Einer der Gründe, localStorage nicht zu verwenden, ist jedoch die Tatsache, dass alles als „String“ gespeichert werden muss. Das bedeutet, dass Sie so etwas wie ein Array oder Objekt nicht direkt speichern können. Versuchen Sie beispielsweise, diese Befehle in Ihrer Browserkonsole auszuführen:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
Obwohl wir versucht haben, den Wert von „myArray“ als Array festzulegen; Als wir es abgerufen haben, war es als Zeichenfolge gespeichert (beachten Sie die Anführungszeichen um '1,2,3,4').
Sie können sicherlich Objekte und Arrays mit localStorage speichern, aber Sie müssen bedenken, dass sie von Strings hin und her konvertiert werden müssen.
Um also Zustandsdaten in localStorage zu schreiben, wurden sie mit der Methode JSON.stringify()
wie folgt in eine Zeichenfolge geschrieben:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
Wenn die Daten aus localStorage abgerufen werden mussten, wurde die Zeichenfolge mit der Methode JSON.parse()
wie folgt wieder in nutzbare Daten umgewandelt:
const players = JSON.parse(storage.getItem("players"));
Die Verwendung localStorage
bedeutete, dass sich alles auf dem Client befand, und das bedeutete, dass keine Dienste von Drittanbietern oder Bedenken hinsichtlich der Datenspeicherung bestehen.
Daten bestanden jetzt aus Aktualisierungen und Sitzungen – Yay! Die schlechte Nachricht war, dass localStorage es nicht überlebt, wenn ein Benutzer seine Browserdaten leert. Wenn jemand das tat, gingen alle seine In/Out-Daten verloren. Das ist ein gravierender Mangel.
Es ist nicht schwer zu verstehen, dass „localStorage“ wahrscheinlich nicht die beste Lösung für „richtige“ Anwendungen ist. Abgesehen von dem oben erwähnten Zeichenfolgenproblem ist es auch langsam für ernsthafte Arbeit, da es den "Hauptthread" blockiert. Alternativen kommen, wie KV Storage, aber machen Sie sich vorerst eine mentale Notiz, seine Verwendung aufgrund der Eignung einzuschränken.
Trotz der Fragilität, Daten lokal auf einem Benutzergerät zu speichern, wurde der Anschluss an einen Dienst oder eine Datenbank abgelehnt. Stattdessen wurde das Problem umgangen, indem eine „Laden/Speichern“-Option angeboten wurde. Dies würde es jedem Benutzer von In/Out ermöglichen, seine Daten als JSON-Datei zu speichern, die bei Bedarf wieder in die App geladen werden könnte.
Dies funktionierte gut auf Android, aber weit weniger elegant für iOS. Auf einem iPhone führte dies zu einer Fülle von Text auf dem Bildschirm wie dieser:
Wie Sie sich vorstellen können, war ich bei weitem nicht der Einzige, der Apple über WebKit wegen dieses Mankos beschimpft hat. Der relevante Fehler war hier.
Zum Zeitpunkt des Schreibens hat dieser Fehler eine Lösung und einen Patch, muss aber noch seinen Weg in iOS Safari finden. Angeblich behebt es iOS13, aber es ist in der Beta, während ich schreibe.
Also, für mein minimal lebensfähiges Produkt, war das die Speicheradressierung. Jetzt war es an der Zeit, zu versuchen, die Dinge „App-ähnlicher“ zu machen!
App-I-Ness
Es stellte sich nach vielen Diskussionen mit vielen Leuten heraus, dass es ziemlich schwierig ist, genau zu definieren, was „app like“ bedeutet.
Letztendlich entschied ich mich für „App-like“, das gleichbedeutend mit einer visuellen Raffinesse ist, die normalerweise im Web fehlt. Wenn ich an die Apps denke, die sich gut anfühlen, bieten sie alle Bewegung. Nicht umsonst, aber Bewegung, die die Geschichte Ihrer Handlungen ergänzt. Es könnten die Seitenübergänge zwischen den Bildschirmen sein, die Art und Weise, wie Menüs erscheinen. Es ist schwer mit Worten zu beschreiben, aber die meisten von uns erkennen es, wenn wir es sehen.
Das erste Stück visuellen Flairs, das benötigt wurde, war das Verschieben von Spielernamen nach oben oder unten von „In“ zu „Out“ und umgekehrt, wenn sie ausgewählt wurden. Es war einfach, einen Spieler sofort von einem Abschnitt zum anderen zu bewegen, aber sicherlich nicht „App-artig“. Eine Animation, wenn auf einen Spielernamen geklickt wird, würde hoffentlich das Ergebnis dieser Interaktion hervorheben – den Spieler, der sich von einer Kategorie in eine andere bewegt.
Wie bei vielen dieser Arten von visuellen Interaktionen täuscht ihre scheinbare Einfachheit über die Komplexität hinweg, die damit verbunden ist, dass sie tatsächlich gut funktioniert.
Es dauerte ein paar Iterationen, um die Bewegung richtig hinzubekommen, aber die grundlegende Logik war folgende:
- Sobald auf einen „Spieler“ geklickt wird, erfassen Sie, wo sich dieser Spieler geometrisch auf der Seite befindet;
- Messen Sie, wie weit die Oberseite des Bereichs entfernt ist, zu der sich der Spieler bewegen muss, wenn er nach oben geht ('In'), und wie weit der Boden entfernt ist, wenn er nach unten geht ('Out');
- Wenn Sie nach oben gehen, muss ein Raum in Höhe der Spielerreihe gelassen werden, wenn sich der Spieler nach oben bewegt, und die darüber liegenden Spieler sollten mit der gleichen Geschwindigkeit nach unten fallen, wie der Spieler braucht, um nach oben zu reisen, um in dem Raum zu landen geräumt von den bestehenden 'In'-Spielern (falls vorhanden), die herunterkommen;
- Wenn ein Spieler „aus“ geht und sich nach unten bewegt, muss alles andere bis zum verbleibenden Platz nach oben verschoben werden, und der Spieler muss unter allen aktuellen „aus“-Spielern landen.
Puh! Es war kniffliger, als ich auf Englisch dachte – geschweige denn JavaScript!
Es gab zusätzliche Komplexitäten zu berücksichtigen und zu testen, wie z. B. Übergangsgeschwindigkeiten. Ob eine konstante Bewegungsgeschwindigkeit (z. B. 20px pro 20ms) oder eine konstante Bewegungsdauer (z. B. 0,2s) besser aussehen würde, war zunächst nicht ersichtlich. Ersteres war etwas komplizierter, da die Geschwindigkeit „on the fly“ berechnet werden musste, basierend darauf, wie weit der Spieler reisen musste – eine größere Entfernung erforderte eine längere Übergangsdauer.
Es stellte sich jedoch heraus, dass eine konstante Übergangsdauer nicht nur einfacher im Code war; es erzeugte tatsächlich eine günstigere Wirkung. Der Unterschied war subtil, aber diese Art von Entscheidungen können Sie erst treffen, wenn Sie beide Optionen gesehen haben.
Bei dem Versuch, diesen Effekt festzunageln, fiel hin und wieder ein visueller Fehler auf, der jedoch nicht in Echtzeit dekonstruiert werden konnte. Ich fand, dass der beste Debugging-Prozess darin bestand, eine QuickTime-Aufzeichnung der Animation zu erstellen und sie dann Frame für Frame durchzugehen. Dies hat das Problem ausnahmslos schneller aufgedeckt als jedes codebasierte Debugging.
Wenn ich mir den Code jetzt anschaue, kann ich erkennen, dass diese Funktionalität auf etwas jenseits meiner bescheidenen App mit ziemlicher Sicherheit effektiver geschrieben werden könnte. Da die App die Anzahl der Spieler kennt und die feste Höhe der Latten kennt, sollte es durchaus möglich sein, alle Entfernungsberechnungen allein im JavaScript durchzuführen, ohne DOM-Lesen.
Es ist nicht so, dass das, was geliefert wurde, nicht funktioniert, es ist nur, dass es nicht die Art von Codelösung ist, die Sie im Internet präsentieren würden. Oh, Moment mal.
Andere "App-ähnliche" Interaktionen waren viel einfacher durchzuführen. Anstatt dass Menüs einfach mit etwas so Einfachem wie dem Umschalten einer Anzeigeeigenschaft ein- und ausgeblendet werden, wurde eine Menge Kilometer gewonnen, indem sie einfach mit etwas mehr Finesse dargestellt wurden. Es wurde immer noch einfach ausgelöst, aber CSS erledigte die ganze schwere Arbeit:
.io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }
Wenn dort das Attribut data-evswitcher-showing="true"
für ein übergeordnetes Element umgeschaltet wurde, wurde das Menü eingeblendet, in seine Standardposition zurückverwandelt und Zeigerereignisse wurden wieder aktiviert, damit das Menü Klicks erhalten konnte.
ECSS-Stylesheet-Methodik
Sie werden in diesem früheren Code feststellen, dass CSS-Überschreibungen aus Autorensicht in einem übergeordneten Selektor verschachtelt sind. Auf diese Weise bevorzuge ich es immer, UI-Stylesheets zu schreiben; eine Single Source of Truth für jeden Selektor und alle Überschreibungen für diesen Selektor, die in einem einzigen Satz geschweifter Klammern gekapselt sind. Es ist ein Muster, das die Verwendung eines CSS-Prozessors (Sass, PostCSS, LESS, Stylus usw.) erfordert, aber meiner Meinung nach ist dies der einzige positive Weg, um die Verschachtelungsfunktionalität zu nutzen.
Ich habe diesen Ansatz in meinem Buch „Enduring CSS“ zementiert, und obwohl es eine Fülle komplizierterer Methoden zum Schreiben von CSS für Schnittstellenelemente gibt, hat ECSS mir und den großen Entwicklungsteams, mit denen ich zusammenarbeite, gute Dienste geleistet, seit der Ansatz erstmals dokumentiert wurde zurück im Jahr 2014! Es erwies sich in diesem Fall als ebenso effektiv.
Partialisieren des TypeScripts
Auch ohne einen CSS-Prozessor oder eine Superset-Sprache wie Sass hatte CSS die Möglichkeit, mit der Import-Direktive eine oder mehrere CSS-Dateien in eine andere zu importieren:
@import "other-file.css";
Als ich mit JavaScript anfing, war ich überrascht, dass es kein Äquivalent gab. Immer wenn Codedateien länger als ein Bildschirm oder so hoch werden, fühlt es sich immer so an, als wäre es von Vorteil, sie in kleinere Teile aufzuteilen.
Ein weiterer Vorteil bei der Verwendung von TypeScript war, dass es eine wunderbar einfache Möglichkeit bietet, Code in Dateien aufzuteilen und diese bei Bedarf zu importieren.
Diese Funktion war älter als native JavaScript-Module und war eine großartige Komfortfunktion. Als TypeScript kompiliert wurde, wurde alles wieder in eine einzige JavaScript-Datei zusammengefügt. Dadurch war es möglich, den Anwendungscode für das Authoring einfach in überschaubare Teildateien aufzuteilen und dann einfach in die Hauptdatei zu importieren. Die Oberseite der Haupt- inout.ts
sah folgendermaßen aus:
/// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />
Diese einfache Haushalts- und Organisationsaufgabe hat enorm geholfen.
Mehrere Ereignisse
Am Anfang hatte ich das Gefühl, dass aus funktionaler Sicht ein einzelnes Event wie „Tuesday Night Football“ ausreichen würde. Wenn Sie in diesem Szenario In/Out geladen haben, haben Sie einfach Spieler hinzugefügt/entfernt oder hinein- oder herausbewegt, und das war's. Es gab keine Vorstellung von mehreren Ereignissen.
Ich entschied schnell, dass dies (selbst wenn ich mich für ein minimal lebensfähiges Produkt entschied) zu einer ziemlich begrenzten Erfahrung führen würde. Was wäre, wenn jemand zwei Spiele an verschiedenen Tagen mit unterschiedlichen Spielern organisiert hätte? Sicherlich könnte/sollte In/Out diesem Bedarf gerecht werden? Es dauerte nicht allzu lange, die Daten umzugestalten, um dies zu ermöglichen, und die zum Laden eines anderen Satzes erforderlichen Methoden zu ändern.
Zu Beginn sah der Standarddatensatz etwa so aus:
var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];
Ein Array, das ein Objekt für jeden Spieler enthält.
Nach Berücksichtigung mehrerer Ereignisse wurde es wie folgt geändert:
var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];
Die neuen Daten waren ein Array mit einem Objekt für jedes Ereignis. Dann gab es in jedem Ereignis eine EventData
-Eigenschaft, die wie zuvor ein Array mit Spielerobjekten war.
Es dauerte viel länger, um zu überdenken, wie die Schnittstelle am besten mit dieser neuen Fähigkeit umgehen könnte.
Das Design war von Anfang an sehr steril. In Anbetracht dessen, dass dies auch eine Designübung sein sollte, hatte ich das Gefühl, nicht mutig genug zu sein. Also wurde ein wenig mehr visuelles Flair hinzugefügt, beginnend mit dem Header. Dies ist, was ich in Sketch verspottet habe:
Es würde keine Preise gewinnen, aber es war sicherlich fesselnder als dort, wo es angefangen hat.
Abgesehen von der Ästhetik, erst als jemand anderes darauf hinwies, erkannte ich, dass das große Plus-Symbol in der Kopfzeile sehr verwirrend war. Die meisten Leute dachten, es sei eine Möglichkeit, ein weiteres Ereignis hinzuzufügen. In Wirklichkeit wechselte es in einen „Spieler hinzufügen“-Modus mit einem ausgefallenen Übergang, bei dem Sie den Namen des Spielers an derselben Stelle eingeben konnten, an der sich der Name des Ereignisses gerade befand.
Dies war ein weiterer Fall, in dem frische Augen von unschätzbarem Wert waren. Es war auch eine wichtige Lektion im Loslassen. Die ehrliche Wahrheit war, dass ich den Übergang des Eingabemodus in der Kopfzeile beibehalten hatte, weil ich ihn für cool und clever hielt. Fakt ist aber, dass es dem Design und damit der Anmeldung insgesamt nicht diene.
Dies wurde in der Live-Version geändert. Stattdessen befasst sich der Header nur mit Ereignissen – ein häufigeres Szenario. In der Zwischenzeit erfolgt das Hinzufügen von Spielern über ein Untermenü. Dies gibt der App eine viel verständlichere Hierarchie.
Die andere Lektion, die wir hier gelernt haben, war, dass es, wann immer möglich, von großem Vorteil ist, ehrliches Feedback von Kollegen zu erhalten. Wenn sie gute und ehrliche Leute sind, lassen sie dich nicht durchgehen!
Zusammenfassung: Mein Code stinkt
Rechts. So weit, so normaler Tech-Adventure-Rückblick; diese Dinger gibt es auf Medium für zehn Cent! Die Formel lautet in etwa so: Der Entwickler beschreibt, wie er alle Hindernisse aus dem Weg geräumt hat, um ein fein abgestimmtes Stück Software ins Internet zu bringen und dann ein Vorstellungsgespräch bei Google zu führen oder irgendwo einen Job zu bekommen. Die Wahrheit ist jedoch, dass ich ein Anfänger bei diesem App-Building-Malarkey war, sodass der Code letztendlich als „fertige“ Anwendung zum Himmel stank!
Beispielsweise funktionierte die verwendete Observer-Pattern-Implementierung sehr gut. Am Anfang war ich organisiert und methodisch, aber diese Herangehensweise ging „schief“, als ich immer verzweifelter wurde, um die Dinge zu Ende zu bringen. Wie bei einem Serien-Diäter schlichen sich alte vertraute Gewohnheiten wieder ein und die Codequalität sank anschließend.
Looking now at the code shipped, it is a less than ideal hodge-bodge of clean observer pattern and bog-standard event listeners calling functions. In the main inout.ts
file there are over 20 querySelector
method calls; hardly a poster child for modern application development!
I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.
The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.