Projektowanie i budowanie progresywnej aplikacji internetowej bez frameworka (część 2)
Opublikowany: 2022-03-10Raison d'etre tej przygody było trochę popchnąć swojego skromnego autora w dyscypliny projektowania wizualnego i kodowania JavaScript. Funkcjonalność aplikacji, którą zdecydowałem się zbudować, nie różniła się od aplikacji „do zrobienia”. Należy podkreślić, że nie było to ćwiczenie z oryginalnego myślenia. Cel podróży był znacznie mniej ważny niż podróż.
Chcesz dowiedzieć się, jak zakończyła się aplikacja? Skieruj przeglądarkę telefonu na https://io.benfrain.com.
Oto podsumowanie tego, co omówimy w tym artykule:
- Konfiguracja projektu i dlaczego wybrałem Gulp jako narzędzie do budowania;
- Wzorce projektowe aplikacji i ich znaczenie w praktyce;
- Jak przechowywać i wizualizować stan aplikacji;
- jak CSS został ograniczony do komponentów;
- jakie subtelności UI/UX zostały zastosowane, aby rzeczy były bardziej „aplikacyjne”;
- Jak zmieniły się kompetencje w wyniku iteracji.
Zacznijmy od narzędzi do budowania.
Narzędzia do budowania
Aby uruchomić i uruchomić moje podstawowe narzędzia TypeScipt i PostCSS oraz stworzyć przyzwoite doświadczenie programistyczne, potrzebowałbym systemu kompilacji.
W mojej codziennej pracy od około pięciu lat buduję prototypy interfejsów w HTML/CSS i w mniejszym stopniu w JavaScript. Do niedawna używałem Gulpa z dowolną liczbą wtyczek prawie wyłącznie po to, aby spełnić moje dość skromne potrzeby w zakresie kompilacji.
Zazwyczaj muszę przetworzyć CSS, przekonwertować JavaScript lub TypeScript na szerzej obsługiwany JavaScript, a od czasu do czasu wykonać powiązane zadania, takie jak minimalizacja kodu wyjściowego i optymalizacja zasobów. Korzystanie z Gulpa zawsze pozwalało mi rozwiązywać te problemy z pewnością siebie.
Dla tych, którzy nie są zaznajomieni, Gulp pozwala pisać JavaScript, aby „coś” zrobić z plikami w lokalnym systemie plików. Aby użyć Gulp, zazwyczaj masz pojedynczy plik (o nazwie gulpfile.js
) w katalogu głównym projektu. Ten plik JavaScript umożliwia zdefiniowanie zadań jako funkcji. Możesz dodać „wtyczki” innych firm, które są zasadniczo dalszymi funkcjami JavaScript, które zajmują się określonymi zadaniami.
Przykładowe zadanie łykania
Przykładowe zadanie Gulp może polegać na użyciu wtyczki do wykorzystania PostCSS do przetwarzania w CSS podczas zmiany arkusza stylów tworzenia (gulp-postcss). Lub kompilowanie plików TypeScript do waniliowego JavaScript (gulp-typescript) podczas ich zapisywania. Oto prosty przykład, jak piszesz zadanie w Gulp. To zadanie używa wtyczki „del” gulp do usunięcia wszystkich plików w folderze o nazwie „build”:
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
require
przypisuje wtyczkę del
do zmiennej. Następnie wywoływana jest metoda gulp.task
. Nazywamy zadanie z ciągiem jako pierwszym argumentem („czysty”), a następnie uruchamiamy funkcję, która w tym przypadku wykorzystuje metodę „del”, aby usunąć folder przekazany do niej jako argument. Symbole gwiazdki są wzorcami „glob”, które zasadniczo mówią „dowolny plik w dowolnym folderze” folderu budowania.
Zadania Gulp mogą być bardziej skomplikowane, ale w istocie jest to mechanika obsługi. Prawda jest taka, że z Gulp nie musisz być czarodziejem JavaScript, aby się obejść; Umiejętności kopiowania i wklejania klasy 3 to wszystko, czego potrzebujesz.
Przez te wszystkie lata utknąłem z Gulpem jako moim domyślnym narzędziem do budowania / uruchamianiem zadań z zasadą „jeśli to nie jest zepsute; nie próbuj tego naprawiać”.
Martwiłem się jednak, że utknąłem na swoich drogach. Łatwo wpaść w pułapkę. Najpierw co roku zaczynasz spędzać wakacje w tym samym miejscu, a następnie odmawiasz przyjęcia jakichkolwiek nowych trendów w modzie, zanim ostatecznie i stanowczo odmówisz wypróbowania nowych narzędzi do budowania.
Słyszałem wiele rozmów w Internecie na temat „Webpack” i pomyślałem, że moim obowiązkiem jest wypróbowanie projektu przy użyciu nowomodnego toastu fajnych dzieciaków programistów front-end.
Pakiet internetowy
Wyraźnie pamiętam, że z dużym zainteresowaniem przeskoczyłem na stronę webpack.js.org. Pierwsze wyjaśnienie czym jest i czym jest Webpack zaczęło się tak:
import bar from './bar';
Co powiedzieć? W słowach Dr Evil: „Rzuć mi tu pieprzoną kość, Scott”.
Wiem, że to mój własny problem, z którym muszę sobie radzić, ale poczułem niechęć do wszelkich wyjaśnień dotyczących kodowania, które wspominają „foo”, „bar” lub „baz”. To plus całkowity brak zwięzłego opisania, do czego właściwie służy Webpack, sprawiło, że podejrzewałem, że być może nie jest dla mnie.
Zagłębiając się nieco w dokumentację Webpack, zaproponowano nieco mniej nieprzejrzyste wyjaśnienie: „W swej istocie webpack jest statycznym pakietem modułów dla nowoczesnych aplikacji JavaScript”.
Hmmm. Pakiet modułów statycznych. Czy tego właśnie chciałem? Nie byłem przekonany. Czytam dalej, ale im więcej czytałem, tym mniej byłem jasny. W tamtych czasach koncepcje takie jak wykresy zależności, ponowne ładowanie gorących modułów i punkty wejścia były dla mnie zasadniczo stracone.
Kilka wieczorów spędzonych na przeszukiwaniu Webpacka później zrezygnowałem z używania go.
Jestem pewien, że w odpowiedniej sytuacji i bardziej doświadczonych rękach, Webpack jest niezwykle potężny i odpowiedni, ale wydawało się, że to kompletna przesada dla moich skromnych potrzeb. Grupowanie modułów, wstrząsanie drzewami i ponowne ładowanie modułów na gorąco brzmiały świetnie; Po prostu nie byłam przekonana, że potrzebuję ich do mojej małej „aplikacji”.
Wróćmy więc do Gulpa.
W temacie niezmieniania rzeczy ze względu na zmianę, inną technologią, którą chciałem ocenić, była Yarn over NPM do zarządzania zależnościami projektu. Do tego momentu zawsze używałem NPM, a Yarn był reklamowany jako lepsza, szybsza alternatywa. Nie mam wiele do powiedzenia na temat przędzy poza tym, że obecnie używasz NPM i wszystko jest w porządku, nie musisz zawracać sobie głowy próbowaniem przędzy.
Jednym z narzędzi, które pojawiło się zbyt późno, abym mógł wycenić tę aplikację, jest Parceljs. Z zerową konfiguracją i wbudowanym mechanizmem BrowserSync, takim jak ponowne ładowanie przeglądarki, od tego czasu znalazłem w nim świetne narzędzie! Ponadto, w obronie Webpack, powiedziano mi, że od wersji 4 Webpack nie wymaga pliku konfiguracyjnego. Anegdotycznie, w nowszej ankiecie, którą przeprowadziłem na Twitterze, spośród 87 respondentów ponad połowa wybrała Webpack zamiast Gulp, Parcel lub Grunt.
Zacząłem mój plik Gulp z podstawową funkcjonalnością do uruchomienia.
Zadanie „domyślne” obejrzałoby foldery „źródłowe” arkuszy stylów i plików TypeScript i skompilowało je do folderu build
wraz z podstawowym kodem HTML i powiązanymi mapami źródłowymi.
Mam też BrowserSync współpracujący z Gulp. Mogłem nie wiedzieć, co zrobić z plikiem konfiguracyjnym Webpacka, ale to nie znaczyło, że byłem jakimś zwierzęciem. Konieczność ręcznego odświeżania przeglądarki podczas iteracji z HTML/CSS to baaardzo 2010, a BrowserSync daje ci krótką informację zwrotną i pętlę iteracji, która jest tak użyteczna przy kodowaniu front-end.
Oto podstawowy plik Gulp na dzień 11.06.2017
Możesz zobaczyć, jak poprawiłem plik Gulpfile bliżej końca wysyłki, dodając minifikację za pomocą ugilify:
Struktura projektu
W konsekwencji moich wyborów technologicznych niektóre elementy organizacji kodu aplikacji same się definiowały. Plik gulpfile.js
w katalogu głównym projektu, folder node_modules
(gdzie Gulp przechowuje kod wtyczki), folder preCSS
na arkusze stylów tworzenia, folder ts
na pliki TypeScript oraz folder build
do uruchomienia skompilowanego kodu.
Pomysł polegał na tym, aby index.html
zawierał „powłokę” aplikacji, w tym dowolną niedynamiczną strukturę HTML, a następnie linki do stylów i pliku JavaScript, które umożliwiłyby działanie aplikacji. Na dysku wyglądałoby to mniej więcej tak:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
Skonfigurowanie BrowserSync do przeglądania tego folderu build
oznaczało, że mogłem wskazać moją przeglądarkę na localhost:3000
i wszystko było w porządku.
Z podstawowym systemem kompilacji, uporządkowaną organizacją plików i kilkoma podstawowymi projektami na początek, skończyły mi się zapasy paszy na prokrastynację, której mogłem legalnie użyć, aby uniemożliwić mi faktyczne zbudowanie tego!
Pisanie aplikacji
Zasada działania aplikacji była taka. Byłby magazyn danych. Po załadowaniu JavaScript ładowałby te dane, przechodząc przez każdy odtwarzacz w danych, tworząc kod HTML potrzebny do reprezentowania każdego odtwarzacza jako wiersza w układzie i umieszczając go w odpowiedniej sekcji wejścia/wyjścia. Wtedy interakcje użytkownika przeniosłyby gracza z jednego stanu do drugiego. Prosty.
Kiedy przyszło do pisania aplikacji, dwa duże wyzwania koncepcyjne, które należało zrozumieć, to:
- Jak reprezentować dane dla aplikacji w sposób, który można łatwo rozszerzyć i manipulować;
- Jak sprawić, by interfejs użytkownika reagował, gdy dane zostały zmienione z danych wprowadzonych przez użytkownika.
Jednym z najprostszych sposobów reprezentowania struktury danych w JavaScript jest notacja obiektowa. To zdanie brzmi trochę informatycznie. Mówiąc prościej, „obiekt” w żargonie JavaScript to wygodny sposób przechowywania danych.
Rozważmy ten obiekt JavaScript przypisany do zmiennej o nazwie ioState
(dla stanu wejścia/wyjścia):
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 }
Jeśli tak naprawdę nie znasz zbyt dobrze JavaScript, prawdopodobnie możesz przynajmniej pojąć, co się dzieje: każda linia w nawiasach klamrowych jest parą właściwości (lub „klucza” w języku JavaScript) i wartości. Możesz ustawić różne rzeczy w kluczu JavaScript. Na przykład funkcje, tablice innych danych lub zagnieżdżone obiekty. Oto przykład:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
Wynik netto jest taki, że przy użyciu tego rodzaju struktury danych można uzyskać i ustawić dowolny klucz obiektu. Na przykład, jeśli chcemy ustawić licznik obiektu ioState na 7:
ioState.Count = 7;
Jeśli chcemy ustawić fragment tekstu na tę wartość, notacja działa tak:
aTextNode.textContent = ioState.Count;
Możesz zobaczyć, że pobieranie wartości i ustawianie wartości dla tego obiektu stanu jest proste po stronie JavaScript. Jednak odzwierciedlenie tych zmian w interfejsie użytkownika jest mniej widoczne. Jest to główny obszar, w którym frameworki i biblioteki starają się abstrahować od bólu.
Ogólnie rzecz biorąc, jeśli chodzi o aktualizowanie interfejsu użytkownika na podstawie stanu, lepiej jest unikać odpytywania DOM, ponieważ jest to ogólnie uważane za podejście nieoptymalne.
Rozważ interfejs wejścia/wyjścia. Zazwyczaj pokazuje listę potencjalnych graczy w grze. Są one wymienione w pionie, jeden pod drugim, w dół strony.
Być może każdy gracz jest reprezentowany w DOM z label
otaczającą pole input
. W ten sposób kliknięcie odtwarzacza spowoduje przełączenie odtwarzacza na „W” na mocy etykiety, która powoduje „zaznaczenie” danych wejściowych.
Aby zaktualizować nasz interfejs, możemy mieć „odbiornik” na każdym elemencie wejściowym w JavaScript. Po kliknięciu lub zmianie funkcja wysyła zapytanie do DOM i zlicza, ile danych wejściowych naszego odtwarzacza jest sprawdzanych. Na podstawie tej liczby zaktualizowalibyśmy coś innego w DOM, aby pokazać użytkownikowi, ilu graczy jest sprawdzonych.
Rozważmy koszt tej podstawowej operacji. Nasłuchujemy na wielu węzłach DOM pod kątem kliknięcia/sprawdzenia danych wejściowych, następnie pytamy DOM, aby zobaczyć, ile poszczególnych typów DOM jest sprawdzanych, a następnie zapisujemy coś do DOM, aby pokazać użytkownikowi, z punktu widzenia interfejsu użytkownika, liczbę graczy właśnie liczyliśmy.
Alternatywą byłoby przechowywanie stanu aplikacji jako obiektu JavaScript w pamięci. Kliknięcie przycisku/wejścia w DOM może jedynie zaktualizować obiekt JavaScript, a następnie, w oparciu o tę zmianę w obiekcie JavaScript, wykonać jednoprzebiegową aktualizację wszystkich potrzebnych zmian interfejsu. Moglibyśmy pominąć zapytanie DOM, aby zliczyć graczy, ponieważ obiekt JavaScript już zawierałby te informacje.
Więc. Użycie struktury obiektu JavaScript dla stanu wydawało się proste, ale wystarczająco elastyczne, aby zawrzeć stan aplikacji w dowolnym momencie. Teoria, w jaki sposób można sobie z tym poradzić, również wydawała się wystarczająco rozsądna – chyba o to chodziło w takich zwrotach jak „jednokierunkowy przepływ danych”? Jednak pierwszą prawdziwą sztuczką byłoby stworzenie kodu, który automatycznie aktualizowałby interfejs użytkownika na podstawie wszelkich zmian w tych danych.
Dobrą wiadomością jest to, że mądrzejsi ludzie ode mnie już to odkryli ( dzięki Bogu! ). Ludzie doskonalili podejście do tego typu wyzwań od zarania aplikacji. Ta kategoria problemów to chleb powszedni „wzorców projektowych”. Przydomek „wzorzec projektowy” początkowo brzmiał dla mnie ezoterycznie, ale po odrobinie wykopania wszystko zaczęło brzmieć mniej informatycznie, a bardziej zdrowo.
Wzorce projektowe
Wzorzec projektowy w leksykonie informatyki jest predefiniowanym i sprawdzonym sposobem rozwiązywania typowego wyzwania technicznego. Pomyśl o wzorach projektowych jako o kodowym odpowiedniku przepisu kulinarnego.
Być może najbardziej znaną literaturą na temat wzorców projektowych jest „Wzorce projektowe: elementy oprogramowania zorientowanego obiektowo do wielokrotnego użytku” z 1994 roku. Chociaż dotyczy to C++ i smalltalk, koncepcje można przenosić. W przypadku JavaScript, Addy Osmani's „Learning JavaScript Design Patterns” obejmuje podobny obszar. Możesz ją również przeczytać online za darmo tutaj.
Wzór obserwatora
Zazwyczaj wzorce projektowe są podzielone na trzy grupy: kreacyjne, strukturalne i behawioralne. Szukałem czegoś behawioralnego, które pomogłoby poradzić sobie z komunikowaniem zmian w różnych częściach aplikacji.
Niedawno widziałem i czytałem naprawdę świetne szczegółowe omówienie implementacji reaktywności w aplikacji autorstwa Gregga Pollacka. Znajdziesz tu zarówno post na blogu, jak i wideo, które zapewnią Ci przyjemność.
Czytając otwierający opis wzorca „Obserwator” w Learning JavaScript Design Patterns
, byłem prawie pewien, że był to wzorzec dla mnie. Opisuje się to w następujący sposób:
Obserwator to wzorzec projektowy, w którym obiekt (znany jako podmiot) utrzymuje listę obiektów zależnych od niego (obserwatorów), automatycznie powiadamiając ich o wszelkich zmianach stanu.
Gdy podmiot musi powiadomić obserwatorów o czymś interesującym, wysyła powiadomienie do obserwatorów (które może zawierać określone dane związane z tematem powiadomienia).
Kluczem do mojego podekscytowania było to, że wydawało się, że oferuje to sposób na aktualizowanie się w razie potrzeby.
Załóżmy, że użytkownik kliknął gracza o imieniu „Betty”, aby zaznaczyć, że bierze udział w grze. W interfejsie użytkownika może zajść kilka rzeczy:
- Dodaj 1 do licznika gier
- Usuń Betty z puli graczy „Out”
- Dodaj Betty do puli graczy „W”
Aplikacja musiałaby również zaktualizować dane reprezentujące interfejs użytkownika. To, czego bardzo chciałem uniknąć, to:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
Celem było stworzenie eleganckiego przepływu danych, który aktualizowałby to, co było potrzebne w DOM, kiedy i jeśli zmieniono centralne dane.
Dzięki wzorcowi Observer możliwe było wysyłanie aktualizacji stanu, a tym samym interfejsu użytkownika w dość zwięzły sposób. Oto przykład, właściwa funkcja służąca do dodania nowego gracza do listy:
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 }); }
Część odnosząca się do wzorca Observer, w którym znajduje się metoda io.notify
. Ponieważ pokazuje nam to, jak modyfikowaliśmy items
będące częścią stanu aplikacji, pozwólcie, że pokażę wam obserwatora, który nasłuchiwał zmian w „elementach”:
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
Mamy metodę powiadamiania, która wprowadza zmiany w danych, a następnie obserwatorów tych danych, które reagują, gdy właściwości, którymi są zainteresowani, są aktualizowane.

Dzięki takiemu podejściu aplikacja może mieć obserwable obserwujące zmiany w dowolnej właściwości danych i uruchamiać funkcję za każdym razem, gdy nastąpi zmiana.
Jeśli interesuje Cię wybrany przeze mnie wzór Observer, to szerzej opiszę go tutaj.
Teraz pojawiło się podejście do efektywnej aktualizacji interfejsu użytkownika w oparciu o stan. Aksamitny. Jednak to wciąż pozostawiało mi dwa rażące problemy.
Jednym z nich było przechowywanie stanu podczas przeładowań/sesji strony oraz fakt, że pomimo tego, że interfejs użytkownika działał, wizualnie, po prostu nie przypominał aplikacji. Na przykład, jeśli naciśnięto przycisk, interfejs użytkownika natychmiast zmienił się na ekranie. Po prostu nie było to szczególnie przekonujące.
Zajmijmy się najpierw stroną przechowywania rzeczy.
Zapisywanie stanu
Moje główne zainteresowanie ze strony programistów skupiało się na zrozumieniu, w jaki sposób można budować interfejsy aplikacji i czynić je interaktywnymi za pomocą JavaScript. Sposób przechowywania i pobierania danych z serwera lub rozwiązywania problemów związanych z uwierzytelnianiem użytkowników i logowaniem było „poza zakresem”.
Dlatego zamiast podłączać się do usługi sieciowej w celu przechowywania danych, zdecydowałem się zachować wszystkie dane na kliencie. Istnieje wiele metod platform internetowych do przechowywania danych na kliencie. Zdecydowałem się na localStorage
.
API dla localStorage jest niezwykle proste. Ustawiasz i pobierasz dane w ten sposób:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
LocalStorage ma metodę setItem
, do której przekazujesz dwa ciągi. Pierwszy to nazwa klucza, za pomocą którego chcesz przechowywać dane, a drugi ciąg to rzeczywisty ciąg, który chcesz przechowywać. Metoda getItem
przyjmuje ciąg znaków jako argument, który zwraca to, co jest przechowywane pod tym kluczem w localStorage. Ładne i proste.
Jednak jednym z powodów, dla których nie należy korzystać z localStorage, jest fakt, że wszystko należy zapisać jako „ciąg”. Oznacza to, że nie możesz bezpośrednio przechowywać czegoś takiego jak tablica lub obiekt. Na przykład spróbuj uruchomić te polecenia w konsoli przeglądarki:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
Mimo że próbowaliśmy ustawić wartość 'myArray' jako tablicę; kiedy go pobraliśmy, był przechowywany jako ciąg (zwróć uwagę na znaki cudzysłowu wokół '1,2,3,4').
Z pewnością możesz przechowywać obiekty i tablice za pomocą localStorage, ale musisz pamiętać, że wymagają one konwersji tam iz powrotem z łańcuchów.
Tak więc, aby zapisać dane stanu do localStorage, zostały one zapisane do ciągu za pomocą metody JSON.stringify()
w następujący sposób:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
Gdy dane wymagały pobrania z localStorage, ciąg został zamieniony z powrotem w użyteczne dane za pomocą metody JSON.parse()
w następujący sposób:
const players = JSON.parse(storage.getItem("players"));
Korzystanie z localStorage
oznaczało, że wszystko było na kliencie, a to oznaczało brak usług stron trzecich lub problemów z przechowywaniem danych.
Dane były teraz odświeżane i sesje — Yay! Zła wiadomość była taka, że localStorage nie przetrwa, gdy użytkownik opróżni dane przeglądarki. Gdy ktoś to zrobi, wszystkie jego dane wejścia/wyjścia zostaną utracone. To poważna wada.
Nietrudno docenić fakt, że `localStorage` prawdopodobnie nie jest najlepszym rozwiązaniem dla 'właściwych' aplikacji. Oprócz wspomnianego wyżej problemu z ciągami, jest również powolny w przypadku poważnej pracy, ponieważ blokuje „główny wątek”. Nadchodzą alternatywy, takie jak KV Storage, ale na razie pamiętaj, aby ograniczyć jego użycie w oparciu o przydatność.
Pomimo kruchości zapisywania danych lokalnie na urządzeniu użytkownika, trudno było połączyć się z usługą lub bazą danych. Zamiast tego problem został ominięty, oferując opcję „załaduj/zapisz”. Umożliwiłoby to każdemu użytkownikowi In/Out zapisanie swoich danych jako pliku JSON, który w razie potrzeby można by ponownie załadować do aplikacji.
Działało to dobrze na Androidzie, ale znacznie mniej elegancko na iOS. Na iPhonie spowodowało to popis tekstu na ekranie:

Jak możesz sobie wyobrazić, nie byłem sam w krytykowaniu Apple za pośrednictwem WebKit na temat tego niedociągnięcia. Znalazł się tutaj odpowiedni błąd.
W momencie pisania tego błędu ten błąd ma rozwiązanie i łatkę, ale jeszcze nie pojawił się w iOS Safari. Podobno iOS13 naprawia to, ale to jest w wersji beta, jak piszę.
Tak więc, dla mojego minimalnego opłacalnego produktu, dotyczyło to przechowywania. Teraz nadszedł czas, aby uczynić wszystko bardziej „aplikacyjnym”!
Aplikacja-I-Ness
Okazuje się, że po wielu dyskusjach z wieloma osobami określenie dokładnie, co oznacza „app like” jest dość trudne.
Ostatecznie zdecydowałem się na to, że „aplikacja” jest synonimem wizualnej zręczności, której zwykle brakuje w sieci. Kiedy myślę o aplikacjach, z których dobrze jest korzystać, wszystkie zawierają ruch. Nie bezinteresowny, ale ruch, który wzbogaca historię twoich działań. Mogą to być przejścia stron między ekranami, sposób, w jaki pojawiają się menu. Trudno to opisać słowami, ale większość z nas wie o tym, kiedy to widzi.
Pierwszym potrzebnym elementem wizualnym było przesunięcie nazw graczy w górę lub w dół z „wchodzących” na „wychodzące” i vice versa po wybraniu. Natychmiastowe przejście gracza z jednej sekcji do drugiej było proste, ale z pewnością nie przypominało aplikacji. Miejmy nadzieję, że animacja po kliknięciu nazwy gracza podkreśli wynik tej interakcji – przejście gracza z jednej kategorii do drugiej.
Podobnie jak w przypadku wielu tego rodzaju interakcji wizualnych, ich pozorna prostota przeczy złożoności związanej z jej dobrym działaniem.
Potrzeba było kilku iteracji, aby uzyskać prawidłowy ruch, ale podstawowa logika była następująca:
- Po kliknięciu „gracza” przechwyć, gdzie ten gracz jest, geometrycznie, na stronie;
- Zmierz, jak daleko znajduje się górna część obszaru, na którą gracz musi się przemieścić, gdy jedzie w górę („W”) i jak daleko znajduje się dół, gdy schodzi („Na zewnątrz”);
- Jeśli idziesz w górę, należy pozostawić przestrzeń równą wysokości rzędu gracza, gdy gracz porusza się w górę, a gracze powyżej powinni zapaść się w dół w takim samym tempie, jak czas potrzebny graczowi na podróż w górę, aby wylądować w przestrzeni opuszczone przez istniejących graczy „w” (jeśli istnieją) schodzących;
- Jeśli gracz odchodzi i schodzi w dół, wszystko inne musi przesunąć się w górę do pozostałego miejsca, a gracz musi znaleźć się poniżej wszystkich obecnych graczy, którzy obecnie się oddalają.
Uff! To było trudniejsze niż myślałem po angielsku — nieważne JavaScript!
Istniały dodatkowe komplikacje do rozważenia i wypróbowania, takie jak prędkości przejścia. Na początku nie było oczywiste, czy stała prędkość ruchu (np. 20px na 20ms), czy stały czas trwania ruchu (np. 0,2s) będzie wyglądać lepiej. Pierwsza była nieco bardziej skomplikowana, ponieważ prędkość musiała być obliczana „w locie” na podstawie tego, jak daleko gracz musiał przebyć — większy dystans wymagał dłuższego czasu przejścia.
Okazało się jednak, że stały czas trwania przejścia nie był po prostu prostszy w kodzie; w rzeczywistości przyniosła bardziej korzystny efekt. Różnica była subtelna, ale takie wybory można określić dopiero po zapoznaniu się z obiema opcjami.
Co jakiś czas przy próbie uzyskania tego efektu rzucał się w oczy błąd wizualny, ale nie dało się go zdekonstruować w czasie rzeczywistym. Odkryłem, że najlepszym procesem debugowania było utworzenie nagrania animacji QuickTime, a następnie przeglądanie go klatka po klatce. Niezmiennie ujawniało to problem szybciej niż jakiekolwiek debugowanie oparte na kodzie.
Patrząc teraz na kod, mogę docenić, że w czymś poza moją skromną aplikacją, ta funkcjonalność prawie na pewno mogłaby zostać napisana bardziej efektywnie. Biorąc pod uwagę, że aplikacja znałaby liczbę graczy i znała stałą wysokość listew, powinno być całkowicie możliwe wykonanie wszystkich obliczeń odległości w samym JavaScript, bez żadnego odczytu DOM.
Nie chodzi o to, że to, co zostało wysłane, nie działa, po prostu nie jest to rozwiązanie w zakresie kodu, które można by zaprezentować w Internecie. Zaczekaj.
Inne interakcje „podobne do aplikacji” były znacznie łatwiejsze do wykonania. Zamiast po prostu wsuwać i wyskakiwać menu za pomocą czegoś tak prostego, jak przełączanie właściwości wyświetlania, osiągnięto wiele kilometrów, po prostu eksponując je z nieco większą finezją. Nadal był uruchamiany po prostu, ale CSS wykonywał cały ciężar:
.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; } }
Tam, gdy data-evswitcher-showing="true"
był przełączany na elemencie nadrzędnym, menu zanikało, przekształcało się z powrotem do swojej domyślnej pozycji, a zdarzenia wskaźnika byłyby ponownie włączane, aby menu mogło otrzymywać kliknięcia.
Metodologia arkusza stylów ECSS
Zauważysz w tym poprzednim kodzie, że z punktu widzenia tworzenia, nadpisania CSS są zagnieżdżane w selektorze nadrzędnym. W ten sposób zawsze wolę pisać arkusze stylów interfejsu użytkownika; pojedyncze źródło prawdy dla każdego selektora i wszelkie nadpisania dla tego selektora zawarte w jednym zestawie nawiasów klamrowych. Jest to wzorzec, który wymaga użycia procesora CSS (Sass, PostCSS, LESS, Stylus itp.), ale wydaje mi się, że jest to jedyny pozytywny sposób na wykorzystanie funkcjonalności zagnieżdżania.
Zacementowałem to podejście w mojej książce Trwały CSS i pomimo tego, że istnieje mnóstwo bardziej zaangażowanych metod pisania CSS dla elementów interfejsu, ECSS służył mi i dużym zespołom programistycznym, z którymi dobrze współpracuję, odkąd podejście to zostało po raz pierwszy udokumentowane z powrotem w 2014 roku! W tym przypadku okazało się to równie skuteczne.
Częściowanie TypeScript
Nawet bez procesora CSS lub języka superzbiorów, takiego jak Sass, CSS ma możliwość importowania jednego lub więcej plików CSS do drugiego za pomocą dyrektywy import:
@import "other-file.css";
Zaczynając od JavaScriptu byłem zaskoczony, że nie ma odpowiednika. Ilekroć pliki kodu są dłuższe niż ekran lub tak wysokie, zawsze wydaje się, że korzystne byłoby podzielenie go na mniejsze części.
Kolejną zaletą korzystania z TypeScriptu był piękny, prosty sposób dzielenia kodu na pliki i importowania go w razie potrzeby.
Ta funkcja poprzedzała natywne moduły JavaScript i była bardzo udogodnieniem. Kiedy TypeScript został skompilowany, połączył wszystko z powrotem do jednego pliku JavaScript. Oznaczało to, że możliwe było łatwe rozbicie kodu aplikacji na łatwe do zarządzania pliki częściowe w celu utworzenia i łatwego importu do głównego pliku. Góra głównego inout.ts
wyglądała tak:
/// <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" />
To proste zadanie porządkowe i porządkowe ogromnie pomogło.
Wiele wydarzeń
Na początku czułem, że z punktu widzenia funkcjonalności wystarczy jedno wydarzenie, takie jak „Wtorkowa nocna piłka nożna”. W tym scenariuszu, jeśli załadowałeś wejście/wyjście, po prostu dodałeś/usunąłeś lub przeniosłeś graczy do środka lub z niego i to było to. Nie było pojęcia o wielu wydarzeniach.
Szybko zdecydowałem, że (nawet w przypadku minimalnie opłacalnego produktu) będzie to dość ograniczone doświadczenie. Co by było, gdyby ktoś zorganizował dwie gry w różne dni, z innym składem graczy? Z pewnością wejście/wyjście mogłoby/powinno zaspokoić tę potrzebę? Zmiana kształtu danych, aby było to możliwe, i zmiana metod potrzebnych do załadowania w innym zestawie nie trwała zbyt długo.
Na początku domyślny zestaw danych wyglądał mniej więcej tak:
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 } ];
Tablica zawierająca obiekt dla każdego gracza.
Po uwzględnieniu wielu zdarzeń zmieniono go tak, aby wyglądał następująco:
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 } ] } ];
Nowe dane były tablicą z obiektem dla każdego zdarzenia. Następnie w każdym zdarzeniu znajdowała się właściwość EventData
, która była tablicą z obiektami odtwarzacza, jak poprzednio.
Dużo więcej czasu zajęło ponowne rozważenie, jak interfejs najlepiej poradzi sobie z tą nową funkcją.
Projekt od początku był bardzo sterylny. Biorąc pod uwagę, że miało to być również ćwiczenie z projektowania, nie czułem, że jestem wystarczająco odważny. Dodano więc trochę więcej wizualnego polotu, zaczynając od nagłówka. Oto, co wyśmiewałem w Sketchu:

Nie zdobył nagród, ale z pewnością był bardziej frapujący niż to, gdzie się zaczęło.
Pomijając estetykę, dopiero gdy ktoś inny to zauważył, doceniłem ikonę dużego plusa w nagłówku, która była bardzo zagmatwana. Większość ludzi myślała, że to sposób na dodanie kolejnego wydarzenia. W rzeczywistości przełączył się w tryb „Dodaj gracza” z fantazyjnym przejściem, które pozwala wpisać imię gracza w tym samym miejscu, w którym aktualnie znajdowała się nazwa wydarzenia.
To był kolejny przypadek, w którym świeże oczy były nieocenione. Była to również ważna lekcja odpuszczania. Szczerze mówiąc, trzymałem się przejścia trybu wprowadzania w nagłówku, ponieważ uważałem, że jest fajny i sprytny. Faktem jednak było, że nie służył on wzornictwu, a zatem całej aplikacji.
Zostało to zmienione w wersji na żywo. Zamiast tego nagłówek dotyczy tylko zdarzeń — co jest bardziej powszechnym scenariuszem. Tymczasem dodawanie graczy odbywa się z podmenu. Daje to aplikacji znacznie bardziej zrozumiałą hierarchię.
Kolejną lekcją, jakiej się tutaj wyciągnęliśmy, było to, że zawsze, gdy to możliwe, niezwykle korzystne jest uzyskanie szczerych informacji zwrotnych od rówieśników. Jeśli są dobrymi i uczciwymi ludźmi, nie pozwolą ci dać sobie przepustki!
Podsumowanie: Mój kod śmierdzi
Prawidłowy. Jak dotąd, normalna retrospektywa przygodowa; te rzeczy kosztują dziesięć centów na Medium! Formuła wygląda mniej więcej tak: twórca opisuje, w jaki sposób pokonali wszystkie przeszkody, aby opublikować dopracowany kawałek oprogramowania w Internecie, a następnie odebrać wywiad w Google lub zostać gdzieś zatrudniony. Prawda jest jednak taka, że po raz pierwszy przystąpiłem do tego malarskiego tworzenia aplikacji, więc kod ostatecznie został wysłany jako „ukończona” aplikacja śmierdząca do nieba!
Na przykład zastosowana implementacja wzorca Observer działała bardzo dobrze. Na początku byłem zorganizowany i metodyczny, ale to podejście „poszło na południe”, gdy byłem bardziej zdesperowany, by dokończyć sprawy. Podobnie jak w przypadku seryjnej diety, stare znajome nawyki wkradły się z powrotem, a następnie jakość kodu spadła.
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.