Proiectarea și construirea unei aplicații web progresive fără cadru (partea a 2-a)

Publicat: 2022-03-10
Rezumat rapid ↬ În primul articol al acestei serii, autorul dumneavoastră, un începător în JavaScript, și-a propus să proiecteze și să codifice o aplicație web de bază. „Aplicația” urma să se numească „In/Out” — o aplicație pentru a organiza jocuri în echipă. În acest articol, ne vom concentra asupra modului în care a fost creată aplicația „In/Out”.

Rațiunea de a fi a acestei aventuri a fost să-ți împingi puțin autorul umil în disciplinele de design vizual și codare JavaScript. Funcționalitatea aplicației pe care am decis să o construiesc nu era diferită de o aplicație „de făcut”. Este important să subliniem că acesta nu a fost un exercițiu de gândire originală. Destinația a fost mult mai puțin importantă decât călătoria.

Doriți să aflați cum a ajuns aplicația? Îndreptați browserul telefonului către https://io.benfrain.com.

Iată un rezumat a ceea ce vom acoperi în acest articol:

  • Configurarea proiectului și de ce am optat pentru Gulp ca instrument de construcție;
  • Modele de proiectare a aplicațiilor și ce înseamnă acestea în practică;
  • Cum să stocați și să vizualizați starea aplicației;
  • modul în care CSS a fost întins la componente;
  • ce frumusețe UI/UX au fost folosite pentru a face lucrurile mai „asemănătoare unei aplicații”;
  • Cum s-a schimbat mandatul prin iterație.

Să începem cu instrumentele de construcție.

Build Tools

Pentru a pune în funcțiune instrumentele mele de bază TypeScipt și PostCSS și pentru a crea o experiență de dezvoltare decentă, aș avea nevoie de un sistem de compilare.

În munca mea de zi cu zi, în ultimii cinci ani și ceva, am construit prototipuri de interfață în HTML/CSS și, într-o măsură mai mică, JavaScript. Până de curând, am folosit Gulp cu orice număr de pluginuri aproape exclusiv pentru a-mi îndeplini nevoile destul de umile de construire.

De obicei, trebuie să procesez CSS, să convertesc JavaScript sau TypeScript în JavaScript acceptat pe scară largă și, ocazional, să efectuez sarcini conexe, cum ar fi minimizarea codului și optimizarea activelor. Folosirea Gulp mi-a permis întotdeauna să rezolv aceste probleme cu aplomb.

Pentru cei nefamiliarizați, Gulp vă permite să scrieți JavaScript pentru a face „ceva” fișierelor din sistemul dvs. de fișiere local. Pentru a utiliza Gulp, aveți de obicei un singur fișier (numit gulpfile.js ) în rădăcina proiectului. Acest fișier JavaScript vă permite să definiți sarcini ca funcții. Puteți adăuga „Plugin-uri” terță parte, care sunt în esență funcții JavaScript suplimentare, care se ocupă de sarcini specifice.

Mai multe după săritură! Continuați să citiți mai jos ↓

Un exemplu de sarcină Gulp

Un exemplu de sarcină Gulp ar putea fi utilizarea unui plugin pentru a valorifica PostCSS pentru a procesa în CSS atunci când modificați o foaie de stil de creație (gulp-postcss). Sau compilarea fișierelor TypeScript în vanilla JavaScript (gulp-typescript) pe măsură ce le salvați. Iată un exemplu simplu despre cum scrieți o sarcină în Gulp. Această sarcină folosește pluginul „del” gulp pentru a șterge toate fișierele dintr-un folder numit „build”:

 var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });

require atribuie plugin-ul del unei variabile. Apoi se apelează metoda gulp.task . Numim sarcina cu un șir ca prim argument („curat”) și apoi rulăm o funcție, care în acest caz folosește metoda „del” pentru a șterge folderul transmis ca argument. Simbolurile asterisc sunt modele „glob” care spun în esență „orice fișier din orice folder” al folderului de compilare.

Sarcinile Gulp pot deveni mult mai complicate, dar, în esență, aceasta este mecanica modului în care sunt gestionate lucrurile. Adevărul este că, cu Gulp, nu trebuie să fii un vrăjitor JavaScript pentru a te descurca; abilitățile de copiere și lipire de clasa 3 sunt tot ce ai nevoie.

M-am păstrat cu Gulp ca instrument de construcție/rulator de sarcini implicit pentru toți acești ani, cu o politică de „dacă nu s-a rupt; nu încerca să o repari”.

Cu toate acestea, eram îngrijorat că mă blocam în căile mele. Este o capcană ușor în care să cazi. În primul rând, începi să mergi în vacanță în același loc în fiecare an, apoi refuzi să adopți noi tendințe de modă înainte de a fi în cele din urmă și refuzi ferm să încerci orice instrumente noi de construcție.

Am auzit multe discuții pe internet despre „Webpack” și am crezut că era de datoria mea să încerc un proiect folosind toast-ul nou-fangled al dezvoltatorului front-end cool-kids.

Webpack

Îmi amintesc clar că am sărit peste site-ul webpack.js.org cu mare interes. Prima explicație despre ce este și ce face Webpack a început astfel:

 import bar from './bar';

Spune ce? În cuvintele Dr. Evil, „Aruncă-mi un os nenorocit aici, Scott”.

Știu că trebuie să mă ocup de problema mea, dar am dezvoltat o repulsie față de orice explicații de codare care menționează „foo”, „bar” sau „baz”. Acest lucru, plus lipsa completă de a descrie succint pentru ce era de fapt Webpack, m-au făcut să bănuiesc că poate nu era pentru mine.

Săpăind puțin mai departe în documentația Webpack, a fost oferită o explicație puțin mai puțin opacă, „În esență, webpack este un bundler de module static pentru aplicațiile JavaScript moderne”.

Hmmm. Grupare de module static. Asta mi-am dorit? Nu eram convins. Am citit mai departe, dar cu cât citeam mai mult, cu atât eram mai puțin clar. Pe atunci, concepte precum graficele de dependență, reîncărcarea modulelor fierbinți și punctele de intrare erau în esență pierdute pentru mine.

Câteva seri de cercetare Webpack mai târziu, am abandonat orice idee de a-l folosi.

Sunt sigur că în situația potrivită și pe mâini mai experimentate, Webpack este extrem de puternic și adecvat, dar mi s-a părut exagerat pentru nevoile mele umile. Gruparea modulelor, tremurarea copacilor și reîncărcarea modulului fierbinte au sunat grozav; Pur și simplu nu eram convins că am nevoie de ele pentru mica mea „aplicație”.

Deci, înapoi la Gulp atunci.

Pe tema de a nu schimba lucrurile de dragul schimbării, o altă tehnologie pe care am vrut să o evaluez a fost Yarn over NPM pentru gestionarea dependențelor proiectelor. Până în acel moment, am folosit întotdeauna NPM și Yarn a fost prezentat ca o alternativă mai bună și mai rapidă. Nu am multe de spus despre Yarn, în afară de cazul în care în prezent utilizați NPM și totul este OK, nu trebuie să vă deranjați să încercați Yarn.

Un instrument care a sosit prea târziu pentru ca eu să îl evaluez pentru această aplicație este Parceljs. Cu o configurație zero și o reîncărcare a browserului, cum ar fi BrowserSync, susținută, de atunci am găsit o mare utilitate în el! În plus, în apărarea Webpack-ului, mi s-a spus că v4-ul și după Webpack nu necesită un fișier de configurare. În mod anecdotic, într-un sondaj mai recent pe care l-am făcut pe Twitter, dintre cei 87 de respondenți, peste jumătate au ales Webpack în locul Gulp, Parcel sau Grunt.

Mi-am pornit fișierul Gulp cu funcționalitatea de bază pentru a începe și a funcționa.

O sarcină „implicit” ar urmări folderele „sursă” ale foilor de stil și fișierelor build și le-ar compila într-un folder de compilare împreună cu HTML de bază și hărțile sursă asociate.

BrowserSync funcționează și cu Gulp. S-ar putea să nu știu ce să fac cu un fișier de configurare Webpack, dar asta nu însemna că sunt un fel de animal. Trebuie să reîmprospătați manual browserul în timp ce iterați cu HTML/CSS este atât de 2010, iar BrowserSync vă oferă acel feedback scurt și bucla de iterație atât de utilă pentru codarea front-end.

Iată fișierul gulp de bază din 11.6.2017

Puteți vedea cum am ajustat Gulpfile mai aproape de sfârșitul expedierii, adăugând minificare cu ugilify:

Structura proiectului

Ca urmare a alegerilor mele tehnologice, unele elemente de organizare a codului pentru aplicație se defineau singure. Un gulpfile.js în rădăcina proiectului, un folder node_modules (unde Gulp stochează codul pluginului), un folder preCSS pentru foile de stil de creație, un folder ts pentru fișierele build și un folder de compilare pentru ca codul compilat să trăiască.

Ideea a fost să existe un index.html care să conțină „shell”-ul aplicației, inclusiv orice structură HTML non-dinamică și apoi link-uri către stilurile și fișierul JavaScript care ar face ca aplicația să funcționeze. Pe disc, ar arăta cam așa:

 build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json

Configurarea BrowserSync pentru a build uita la acel folder de compilare a însemnat că îmi puteam îndrepta browserul către localhost:3000 și totul a fost bine.

Cu un sistem de construcție de bază, organizarea fișierelor stabilită și câteva modele de bază cu care să începem, am rămas fără hrană de amânare pe care o puteam folosi în mod legitim pentru a mă împiedica să construiesc lucrul!

Scrierea unei cereri

Principiul cum ar funcționa aplicația a fost acesta. Ar exista un depozit de date. Când JavaScript s-a încărcat, acesta ar încărca acele date, trece în buclă prin fiecare player din date, creând HTML-ul necesar pentru a reprezenta fiecare jucător ca un rând în aspect și plasându-le în secțiunea de intrare/ieșire corespunzătoare. Apoi interacțiunile utilizatorului ar muta un jucător dintr-o stare în alta. Simplu.

Când a fost vorba de scrierea efectivă a aplicației, cele două mari provocări conceptuale care trebuiau înțelese au fost:

  1. Cum să reprezentați datele pentru o aplicație într-o manieră care ar putea fi ușor extinsă și manipulată;
  2. Cum să faci interfața de utilizare să reacționeze atunci când datele au fost modificate de la intrarea utilizatorului.

Una dintre cele mai simple moduri de a reprezenta o structură de date în JavaScript este cu notația obiect. Acea propoziție citește puțin informatică-y. Mai simplu, un „obiect” în limbajul JavaScript este o modalitate utilă de stocare a datelor.

Luați în considerare acest obiect JavaScript atribuit unei variabile numite ioState (pentru starea In/Out):

 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 }

Dacă nu cunoașteți JavaScript atât de bine, probabil că puteți înțelege cel puțin ce se întâmplă: fiecare linie din interiorul acoladelor este o proprietate (sau „cheie” în limbajul JavaScript) și o pereche de valori. Puteți seta tot felul de lucruri la o cheie JavaScript. De exemplu, funcții, matrice de alte date sau obiecte imbricate. Iată un exemplu:

 var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }

Rezultatul net este că, folosind acest tip de structură de date, puteți obține și seta oricare dintre cheile obiectului. De exemplu, dacă vrem să setăm numărul obiectului ioState la 7:

 ioState.Count = 7;

Dacă vrem să setăm o bucată de text la acea valoare, notația funcționează astfel:

 aTextNode.textContent = ioState.Count;

Puteți vedea că obținerea de valori și setarea de valori pentru acel obiect de stare este simplă în partea JavaScript. Cu toate acestea, reflectarea acestor modificări în interfața cu utilizatorul este mai puțin. Aceasta este zona principală în care cadrele și bibliotecile încearcă să abstragă durerea.

În termeni generali, când vine vorba de actualizarea interfeței cu utilizatorul în funcție de stare, este de preferat să evitați interogarea DOM, deoarece aceasta este în general considerată o abordare sub-optimă.

Luați în considerare interfața In/Out. De obicei, arată o listă de jucători potențiali pentru un joc. Sunt listate vertical, unul sub celălalt, în josul paginii.

Poate că fiecare jucător este reprezentat în DOM cu o label include o casetă de input . În acest fel, făcând clic pe un jucător ar comuta jucătorul la „În”, în virtutea etichetei care face intrarea „verificată”.

Pentru a actualiza interfața, este posibil să avem un „ascultător” pentru fiecare element de intrare din JavaScript. La un clic sau modificare, funcția interogează DOM și numără câte dintre intrările jucătorului nostru sunt verificate. Pe baza acestui număr, vom actualiza apoi altceva în DOM pentru a arăta utilizatorului câți jucători sunt verificați.

Să luăm în considerare costul acestei operațiuni de bază. Ascultăm pe mai multe noduri DOM pentru clic/verificarea unei intrări, apoi interogăm DOM pentru a vedea câte dintre un anumit tip DOM sunt verificate, apoi scriem ceva în DOM pentru a arăta utilizatorului, în ceea ce privește UI, numărul de jucători. doar am numărat.

Alternativa ar fi să păstrați starea aplicației ca obiect JavaScript în memorie. Un clic pe buton/intrare în DOM ar putea doar să actualizeze obiectul JavaScript și apoi, pe baza acelei modificări a obiectului JavaScript, să facă o actualizare cu o singură trecere a tuturor modificărilor de interfață care sunt necesare. Am putea sări peste interogarea DOM pentru a număra jucătorii, deoarece obiectul JavaScript ar deține deja acele informații.

Asa de. Utilizarea unei structuri de obiect JavaScript pentru stare părea simplă, dar suficient de flexibilă pentru a încapsula starea aplicației la un moment dat. Teoria despre cum ar putea fi gestionat acest lucru părea și ea suficient de solidă – trebuie să fie despre ce au fost expresii precum „flux de date unidirecționale”? Cu toate acestea, primul truc adevărat ar fi crearea unui cod care să actualizeze automat interfața de utilizare pe baza oricăror modificări aduse acestor date.

Vestea bună este că oamenii mai deștepți decât mine și-au dat seama deja de aceste lucruri ( Slavă Domnului! ). Oamenii au perfecționat abordări pentru acest tip de provocare încă de la începutul aplicațiilor. Această categorie de probleme este „modelele de design”. Denumirea „model de design” mi-a sunat ezoteric la început, dar după ce am săpat puțin, totul a început să sune mai puțin informatică și mai mult bun simț.

Modele de design

Un model de design, în lexicul informatic, este o modalitate predefinită și dovedită de a rezolva o provocare tehnică comună. Gândiți-vă la modelele de design ca la echivalentul de codificare al unei rețete de gătit.

Poate cea mai faimoasă literatură despre modelele de design este „Design Patterns: Elements of Reusable Object-Oriented Software” din 1994. Deși se referă la C++ și smalltalk, conceptele sunt transferabile. Pentru JavaScript, „Learning JavaScript Design Patterns” de la Addy Osmani acoperă un teren similar. De asemenea, îl puteți citi online gratuit aici.

Model de observator

De obicei, modelele de design sunt împărțite în trei grupuri: creaționale, structurale și comportamentale. Căutam ceva comportamental care să ajute să fac față schimbărilor de comunicare în jurul diferitelor părți ale aplicației.

Mai recent, am văzut și am citit o experiență cu adevărat grozavă despre implementarea reactivității într-o aplicație de către Gregg Pollack. Există atât o postare pe blog, cât și un videoclip pentru a vă bucura aici.

Când am citit descrierea de deschidere a modelului „Observator” din Learning JavaScript Design Patterns , am fost destul de sigur că acesta este modelul pentru mine. Este descris astfel:

Observatorul este un model de design în care un obiect (cunoscut ca subiect) menține o listă de obiecte în funcție de el (observatori), notificându-i automat despre orice modificare a stării.

Când un subiect trebuie să notifice observatorii despre ceva interesant care se întâmplă, acesta transmite o notificare către observatori (care poate include date specifice legate de subiectul notificării).

Cheia entuziasmului meu a fost că acest lucru părea să ofere o modalitate de actualizare a lucrurilor atunci când era nevoie.

Să presupunem că utilizatorul a făcut clic pe un jucător numit „Betty” pentru a selecta că a fost „In” pentru joc. S-ar putea să fie nevoie să se întâmple câteva lucruri în interfața de utilizare:

  1. Adăugați 1 la numărul de joc
  2. Eliminați-o pe Betty din grupul de jucători „În afara”.
  3. Adaugă Betty în grupul de jucători „în”

De asemenea, aplicația ar trebui să actualizeze datele care reprezentau interfața de utilizare. Ceea ce am vrut foarte mult să evit a fost următorul:

 playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }

Scopul a fost acela de a avea un flux de date elegant care să actualizeze ceea ce era necesar în DOM când și dacă datele centrale au fost modificate.

Cu un model Observer, a fost posibil să se trimită actualizări la stat și, prin urmare, la interfața cu utilizatorul destul de succint. Iată un exemplu, funcția reală folosită pentru a adăuga un jucător nou la listă:

 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 }); }

Partea relevantă pentru modelul Observer există metoda io.notify . Deoarece asta ne arată că modificăm items din starea aplicației, permiteți-mi să vă arăt observatorul care a ascultat modificările la „articole”:

 io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });

Avem o metodă de notificare care face modificări datelor și apoi observatorii la acele date care răspund atunci când proprietățile de care sunt interesați sunt actualizate.

Cu această abordare, aplicația ar putea avea observabili care urmăresc modificări ale oricărei proprietăți a datelor și să ruleze o funcție ori de câte ori a avut loc o modificare.

Dacă sunteți interesat de modelul Observer pentru care am optat, îl descriu mai detaliat aici.

A existat acum o abordare pentru actualizarea efectivă a interfeței de utilizare, bazată pe stare. piersici. Cu toate acestea, acest lucru mi-a lăsat încă două probleme flagrante.

Una a fost cum să stocați starea în reîncărcările/sesiunile de pagină și faptul că, în ciuda faptului că interfața de utilizare funcționează, din punct de vedere vizual, pur și simplu nu a fost foarte „ca aplicația”. De exemplu, dacă a fost apăsat un buton, interfața de utilizare se schimba instantaneu pe ecran. Pur și simplu nu a fost deosebit de convingător.

Să ne ocupăm mai întâi de partea de stocare a lucrurilor.

Starea de salvare

Interesul meu principal din partea dezvoltării s-a concentrat pe înțelegerea modului în care interfețele aplicațiilor ar putea fi construite și făcute interactive cu JavaScript. Cum să stocați și să preluați date de pe un server sau să abordați autentificarea utilizatorului și autentificarea a fost „în afara domeniului de aplicare”.

Prin urmare, în loc să mă conectez la un serviciu web pentru nevoile de stocare a datelor, am optat să păstrez toate datele pe client. Există o serie de metode ale platformei web de stocare a datelor pe un client. Am optat pentru localStorage .

API-ul pentru localStorage este incredibil de simplu. Setați și obțineți date astfel:

 // Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");

LocalStorage are o metodă setItem căreia îi treceți două șiruri. Primul este numele cheii cu care doriți să stocați datele, iar al doilea șir este șirul real pe care doriți să îl stocați. Metoda getItem ia un șir ca argument care vă întoarce orice este stocat sub acea cheie în localStorage. Frumos și simplu.

Cu toate acestea, printre motivele pentru a nu folosi localStorage este faptul că totul trebuie salvat ca „șir”. Aceasta înseamnă că nu puteți stoca direct ceva precum o matrice sau un obiect. De exemplu, încercați să rulați aceste comenzi în consola browserului dvs.:

 // Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"

Chiar dacă am încercat să setăm valoarea lui „myArray” ca matrice; când l-am preluat, fusese stocat ca șir (rețineți ghilimelele din jurul „1,2,3,4”).

Cu siguranță puteți stoca obiecte și matrice cu localStorage, dar trebuie să aveți în vedere faptul că acestea au nevoie de conversie înainte și înapoi din șiruri.

Deci, pentru a scrie datele de stare în localStorage, a fost scris într-un șir cu metoda JSON.stringify() astfel:

 const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));

Când datele trebuiau preluate din localStorage, șirul a fost transformat înapoi în date utilizabile cu metoda JSON.parse() astfel:

 const players = JSON.parse(storage.getItem("players"));

Folosirea localStorage însemna că totul era pe client și asta însemna că nu există servicii terțe sau probleme de stocare a datelor.

Acum datele persistau reîmprospătări și sesiuni - Da! Vestea proastă a fost că localStorage nu supraviețuiește unui utilizator care își golește datele browserului. Când cineva făcea asta, toate datele lui In/Out se pierdeau. Acesta este un defect grav.

Nu este greu de apreciat că „localStorage” probabil nu este cea mai bună soluție pentru aplicațiile „adecvate”. Pe lângă problema de șir menționată mai sus, este, de asemenea, lent pentru munca serioasă, deoarece blochează „firul principal”. Vin alternative, cum ar fi KV Storage, dar deocamdată, luați o notă mentală pentru a avertiza utilizarea sa în funcție de adecvare.

În ciuda fragilității salvării datelor la nivel local pe dispozitivul utilizatorului, conectarea la un serviciu sau la o bază de date a fost rezistată. În schimb, problema a fost ocolită prin oferirea unei opțiuni de „încărcare/salvare”. Acest lucru ar permite oricărui utilizator de In/Out să-și salveze datele ca fișier JSON, care ar putea fi încărcat înapoi în aplicație dacă este necesar.

Acest lucru a funcționat bine pe Android, dar mult mai puțin elegant pentru iOS. Pe un iPhone, a avut ca rezultat o stropire de text pe ecran ca acesta:

Pe un iPhone, a avut ca rezultat o stropire de text pe ecran
(Previzualizare mare)

După cum vă puteți imagina, am fost departe de a fi singurul care a reproșat Apple prin WebKit cu privire la acest neajuns. Bug-ul relevant a fost aici.

La momentul scrierii, acest bug are o soluție și un patch, dar încă nu a făcut loc în iOS Safari. Se presupune că iOS13 îl rezolvă, dar este în versiune beta în timp ce scriu.

Deci, pentru produsul meu minim viabil, s-a adresat stocării. Acum era timpul să încercăm să facem lucrurile mai „asemănătoare unei aplicații”!

App-I-Ness

Se pare că, după multe discuții cu mulți oameni, este destul de dificil să definiți exact ce înseamnă „app like”.

În cele din urmă, m-am hotărât ca „ca o aplicație” să fie sinonim cu o slickness vizuală care de obicei lipsește de pe web. Când mă gândesc la aplicațiile care se simt bine să le folosești, toate au mișcare. Nu gratuit, ci o mișcare care se adaugă la povestea acțiunilor tale. Ar putea fi tranzițiile de pagină între ecrane, modul în care apar meniurile. Este greu de descris în cuvinte, dar cei mai mulți dintre noi îl știm când îl vedem.

Prima piesă de fler vizual necesară a fost schimbarea numelor jucătorilor în sus sau în jos de la „In” la „Out” și viceversa atunci când este selectat. A face un jucător să se mute instantaneu de la o secțiune la alta a fost simplu, dar cu siguranță nu „ca o aplicație”. Se speră că pe o animație pe care sa făcut clic pe numele unui jucător ar sublinia rezultatul acelei interacțiuni – jucătorul care trece de la o categorie la alta.

La fel ca multe dintre aceste tipuri de interacțiuni vizuale, simplitatea lor aparentă contrazice complexitatea pe care o implică să funcționeze bine.

A fost nevoie de câteva iterații pentru a obține mișcarea corectă, dar logica de bază a fost următoarea:

  • Odată ce faceți clic pe un „jucător”, capturați unde se află acel jucător, geometric, pe pagină;
  • Măsurați cât de departe este partea de sus a zonei la care trebuie să se deplaseze jucătorul dacă merge în sus („In”) și cât de departe este partea de jos, dacă coboară („Out”);
  • Dacă merge în sus, trebuie lăsat un spațiu egal cu înălțimea rândului jucătorului pe măsură ce jucătorul se mișcă în sus, iar jucătorii de deasupra ar trebui să se prăbușească în jos la aceeași rată cu timpul necesar jucătorului pentru a ajunge până la aterizare în spațiu. eliberat de jucătorii „In” existenți (dacă există) care se prăbușesc;
  • Dacă un jucător iese „În afara” și se mișcă în jos, totul trebuie să se mute în sus în spațiul din stânga și jucătorul trebuie să ajungă sub orice jucători „Ieșiți” actuali.

Pf! A fost mai complicat decât credeam în engleză – nu contează JavaScript!

Au existat complexități suplimentare de luat în considerare și de încercat, cum ar fi vitezele de tranziție. La început, nu era evident dacă o viteză constantă de mișcare (de ex. 20px la 20ms) sau o durată constantă pentru mișcare (de ex. 0.2s) ar arăta mai bine. Prima a fost puțin mai complicată, deoarece viteza trebuia calculată „din zbor”, în funcție de cât de departe trebuia să parcurgă jucătorul - distanță mai mare necesitând o durată mai lungă de tranziție.

Cu toate acestea, sa dovedit că o durată constantă de tranziție nu era doar mai simplă în cod; a produs de fapt un efect mai favorabil. Diferența a fost subtilă, dar acestea sunt genul de alegeri pe care le puteți determina numai după ce ați văzut ambele opțiuni.

Din când în când, în timp ce încercam să obțineți acest efect, o defecțiune vizuală atrage atenția, dar era imposibil de deconstruit în timp real. Am descoperit că cel mai bun proces de depanare a fost crearea unei înregistrări QuickTime a animației și apoi parcurgerea acesteia câte un cadru. În mod invariabil, aceasta a dezvăluit problema mai rapid decât orice depanare bazată pe cod.

Privind codul acum, pot aprecia că pe ceva dincolo de aplicația mea umilă, această funcționalitate ar putea fi aproape sigur scrisă mai eficient. Având în vedere că aplicația ar cunoaște numărul de jucători și ar cunoaște înălțimea fixă ​​a șipcilor, ar trebui să fie posibil să se facă toate calculele de distanță numai în JavaScript, fără nicio citire DOM.

Nu este că ceea ce a fost livrat nu funcționează, ci doar că nu este genul de soluție de cod pe care ai prezenta-o pe Internet. Oh, așteptați.

Alte interacțiuni „cum ar fi aplicația” au fost mult mai ușor de realizat. În loc ca meniurile pur și simplu să intre și să iasă cu ceva la fel de simplu precum comutarea unei proprietăți de afișare, s-a câștigat mult kilometraj prin simpla expunere a acestora cu puțin mai multă finețe. A fost încă declanșat simplu, dar CSS făcea toate sarcinile grele:

 .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; } }

Acolo, atunci când data-evswitcher-showing="true" era comutat pe un element părinte, meniul s-a estompat, se transforma înapoi în poziția implicită și evenimentele pointer ar fi reactivate, astfel încât meniul să poată primi clicuri.

Metodologia foii de stil ECSS

Veți observa în acel cod anterior că, din punct de vedere al creației, suprascrierile CSS sunt imbricate într-un selector părinte. Acesta este modul în care prefer întotdeauna scrierea foilor de stil UI; o singură sursă de adevăr pentru fiecare selector și orice suprascrieri pentru acel selector, încapsulate într-un singur set de acolade. Este un model care necesită utilizarea unui procesor CSS (Sass, PostCSS, LESS, Stylus și colab.), dar cred că este singura modalitate pozitivă de a folosi funcționalitatea de imbricare.

Am cimentat această abordare în cartea mea, Enduring CSS și, în ciuda faptului că există o multitudine de metode mai implicate disponibile pentru a scrie CSS pentru elementele de interfață, ECSS mi-a fost de folos mie și echipelor mari de dezvoltare cu care lucrez de când abordarea a fost documentată pentru prima dată. inapoi in 2014! S-a dovedit la fel de eficient în acest caz.

Partializarea TypeScript

Chiar și fără un procesor CSS sau un limbaj superset precum Sass, CSS a avut capacitatea de a importa unul sau mai multe fișiere CSS în altul cu directiva de import:

 @import "other-file.css";

Când am început cu JavaScript, am fost surprins că nu există echivalent. Ori de câte ori fișierele de cod devin mai lungi decât un ecran sau atât de înalte, se simte întotdeauna că împărțirea lor în bucăți mai mici ar fi benefică.

Un alt avantaj la utilizarea TypeScript a fost că are o modalitate foarte simplă de a împărți codul în fișiere și de a le importa atunci când este necesar.

Această capacitate a predatat modulele native JavaScript și a fost o caracteristică de mare comoditate. Când TypeScript a fost compilat, a împletit totul într-un singur fișier JavaScript. Însemna că a fost posibil să divizați cu ușurință codul aplicației în fișiere parțiale gestionabile pentru creare și importare, apoi în fișierul principal cu ușurință. Partea de sus a principalelor inout.ts arăta astfel:

 /// <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" />

Această sarcină simplă de întreținere și organizare a ajutat enorm.

Evenimente multiple

La început, am simțit că din punct de vedere al funcționalității ar fi suficient un singur eveniment, precum „Fotbalul de marți seara”. În acel scenariu, dacă ați încărcat In/Out, doar ați adăugat/eliminat sau mutați jucători înăuntru sau în afara și asta a fost tot. Nu exista nicio noțiune de evenimente multiple.

Am decis rapid că (chiar și pentru un produs minim viabil) acest lucru ar face o experiență destul de limitată. Ce se întâmplă dacă cineva ar organiza două jocuri în zile diferite, cu o listă diferită de jucători? Cu siguranță In/Out ar putea/ar trebui să satisfacă această nevoie? Nu a durat prea mult pentru a remodela datele pentru a face acest lucru posibil și pentru a modifica metodele necesare pentru a încărca într-un set diferit.

La început, setul de date implicit arăta cam așa:

 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 } ];

O matrice care conține un obiect pentru fiecare jucător.

După luarea în considerare a mai multor evenimente, a fost modificat astfel încât să arate astfel:

 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 } ] } ];

Noile date erau o matrice cu un obiect pentru fiecare eveniment. Apoi, în fiecare eveniment a fost o proprietate EventData care a fost o matrice cu obiecte jucător ca înainte.

A durat mult mai mult să se reconsidere modul în care interfața ar putea face față cel mai bine acestei noi capacități.

De la început, designul a fost întotdeauna foarte steril. Având în vedere că acesta trebuia să fie și un exercițiu de design, nu am simțit că sunt suficient de curajos. Așa că a fost adăugat puțin mai mult fler vizual, începând cu antetul. Iată ce am batjocorit în Sketch:

O machetă a designului revizuit al aplicației
Machetă de design revizuită. (Previzualizare mare)

Nu avea să câștige premii, dar cu siguranță a fost mai captivant decât de unde a început.

Lăsând deoparte estetica, abia când altcineva a subliniat-o, am apreciat că pictograma mare plus din antet era foarte confuză. Majoritatea oamenilor au crezut că este o modalitate de a adăuga un alt eveniment. În realitate, a trecut la un mod „Adăugați jucător” cu o tranziție elegantă care vă permite să introduceți numele jucătorului în același loc în care era numele evenimentului.

Acesta a fost un alt exemplu în care ochii proaspeți au fost de neprețuit. A fost, de asemenea, o lecție importantă de a da drumul. Adevărul sincer a fost că am păstrat tranziția modului de intrare în antet pentru că am simțit că este cool și inteligent. Cu toate acestea, adevărul a fost că nu a servit designului și, prin urmare, aplicației în ansamblu.

Acest lucru a fost schimbat în versiunea live. În schimb, antetul se ocupă doar de evenimente - un scenariu mai frecvent. Între timp, adăugarea jucătorilor se face dintr-un submeniu. Acest lucru oferă aplicației o ierarhie mult mai ușor de înțeles.

Cealaltă lecție învățată aici a fost că, ori de câte ori este posibil, este extrem de benefic să obțineți feedback sincer de la colegi. Dacă sunt oameni buni și cinstiți, nu vă vor lăsa să vă dați un permis!

Rezumat: My Code Stinks

Dreapta. Până acum, piesa retrospectivă tech-aventură normală; aceste lucruri sunt zece un ban pe Medium! Formula este cam așa: dezvoltatorul detaliază modul în care au doborât toate obstacolele pentru a lansa o bucată de software fin reglată pe internet și apoi a preluat un interviu la Google sau a fost angajat undeva. Cu toate acestea, adevărul este că am fost un începător la acest malarkey care construiește aplicații, așa că codul a fost livrat în cele din urmă, deoarece aplicația „terminată” a puțit în ceruri!

De exemplu, implementarea modelului Observer folosit a funcționat foarte bine. Am fost organizat și metodic de la început, dar acea abordare „a mers spre sud”, pe măsură ce am devenit mai disperat să termin lucrurile. Asemenea unui dieta în serie, vechile obiceiuri familiare s-au strecurat înapoi și calitatea codului a scăzut ulterior.

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.