Progettazione e creazione di un'applicazione Web progressiva senza un framework (parte 2)

Pubblicato: 2022-03-10
Riassunto veloce ↬ Nel primo articolo di questa serie, il tuo autore, un principiante di JavaScript, si era prefissato l'obiettivo di progettare e codificare un'applicazione web di base. L '"app" doveva essere chiamata "In/Out", un'applicazione per organizzare giochi di squadra. In questo articolo, ci concentreremo su come è stata effettivamente realizzata l'applicazione 'In/Out'.

La ragion d'essere di questa avventura era di spingere un po' il tuo umile autore nelle discipline del visual design e della codifica JavaScript. La funzionalità dell'applicazione che avevo deciso di creare non era dissimile da un'applicazione "da fare". È importante sottolineare che questo non era un esercizio di pensiero originale. La destinazione era molto meno importante del viaggio.

Vuoi scoprire come è finita l'applicazione? Punta il browser del tuo telefono su https://io.benfrain.com.

Ecco un riassunto di ciò che tratteremo in questo articolo:

  • L'impostazione del progetto e il motivo per cui ho optato per Gulp come strumento di costruzione;
  • Modelli di progettazione delle applicazioni e cosa significano in pratica;
  • Come memorizzare e visualizzare lo stato dell'applicazione;
  • come l'ambito CSS è stato applicato ai componenti;
  • quali accortezze UI/UX sono state impiegate per rendere le cose più "simili ad app";
  • Come il mandato è cambiato attraverso l'iterazione.

Iniziamo con gli strumenti di compilazione.

Strumenti di costruzione

Per rendere operativi i miei strumenti di base di TypeScipt e PostCSS e creare un'esperienza di sviluppo decente, avrei bisogno di un sistema di compilazione.

Nel mio lavoro quotidiano, negli ultimi cinque anni circa, ho costruito prototipi di interfaccia in HTML/CSS e, in misura minore, JavaScript. Fino a poco tempo, ho usato Gulp con qualsiasi numero di plugin quasi esclusivamente per soddisfare le mie esigenze di build abbastanza umili.

In genere ho bisogno di elaborare CSS, convertire JavaScript o TypeScript in JavaScript più ampiamente supportati e, occasionalmente, svolgere attività correlate come ridurre al minimo l'output del codice e ottimizzare le risorse. L'uso di Gulp mi ha sempre permesso di risolvere questi problemi con aplomb.

Per chi non ha familiarità, Gulp ti consente di scrivere JavaScript per fare "qualcosa" ai file sul tuo file system locale. Per usare Gulp, in genere hai un singolo file (chiamato gulpfile.js ) nella radice del tuo progetto. Questo file JavaScript consente di definire le attività come funzioni. Puoi aggiungere "Plugin" di terze parti, che sono essenzialmente ulteriori funzioni JavaScript, che si occupano di attività specifiche.

Altro dopo il salto! Continua a leggere sotto ↓

Un esempio di attività Gulp

Un'attività di Gulp di esempio potrebbe essere l'utilizzo di un plug-in per sfruttare PostCSS da elaborare in CSS quando si modifica un foglio di stile di creazione (gulp-postcss). O compilando i file TypeScript in JavaScript vanilla (gulp-typescript) mentre li salvi. Ecco un semplice esempio di come si scrive un'attività in Gulp. Questa attività utilizza il plug-in 'del' gulp per eliminare tutti i file in una cartella chiamata 'build':

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

Il require assegna il plugin del a una variabile. Quindi viene chiamato il metodo gulp.task . Nominiamo l'attività con una stringa come primo argomento ("clean") e quindi eseguiamo una funzione, che in questo caso utilizza il metodo 'del' per eliminare la cartella passata come argomento. I simboli di asterisco sono modelli "glob" che essenzialmente dicono "qualsiasi file in qualsiasi cartella" della cartella build.

Le attività di Gulp possono diventare molto più complicate ma, in sostanza, questa è la meccanica di come vengono gestite le cose. La verità è che con Gulp non è necessario essere un mago JavaScript per cavarsela; le abilità di copia e incolla di grado 3 sono tutto ciò di cui hai bisogno.

Sono rimasto con Gulp come strumento di build predefinito/corridore di attività per tutti questi anni con una politica di "se non è rotto; non provare a risolverlo'.

Tuttavia, ero preoccupato di rimanere bloccato nei miei modi. È una trappola facile in cui cadere. In primo luogo, inizi a passare le vacanze nello stesso posto ogni anno, poi rifiutando di adottare qualsiasi nuova tendenza della moda prima e rifiutandoti fermamente di provare qualsiasi nuovo strumento di costruzione.

Avevo sentito molte chiacchiere su Internet su "Webpack" e ho pensato che fosse mio dovere provare un progetto usando il brindisi nuovo di zecca degli sviluppatori di front-end cool-kids.

Pacchetto Web

Ricordo distintamente di essere passato al sito webpack.js.org con vivo interesse. La prima spiegazione di cosa è e cosa fa Webpack è iniziata in questo modo:

 import bar from './bar';

Che cosa? Nelle parole del dottor Evil, "Buttami un fottuto osso qui, Scott".

So che è il mio problema da affrontare, ma ho sviluppato una repulsione per qualsiasi spiegazione del codice che menzioni "pippo", "bar" o "baz". Questo, oltre alla completa mancanza di una descrizione succinta di cosa fosse effettivamente Webpack, mi ha fatto sospettare che forse non fosse per me.

Scavando un po' più a fondo nella documentazione di Webpack, è stata offerta una spiegazione leggermente meno opaca: "Nella sua base, webpack è un bundler di moduli statici per le moderne applicazioni JavaScript".

Hmmm. Fardellatore di moduli statici. Era quello che volevo? non ero convinto. Continuavo a leggere ma più leggevo, meno chiaro ero. All'epoca, concetti come i grafici delle dipendenze, il ricaricamento dei moduli a caldo e i punti di ingresso erano sostanzialmente persi su di me.

Un paio di sere di ricerca sul Webpack dopo, ho abbandonato l'idea di usarlo.

Sono sicuro che nella giusta situazione e in mani più esperte, Webpack è immensamente potente e appropriato ma sembrava un completo eccessivo per i miei umili bisogni. Il raggruppamento dei moduli, lo scuotimento degli alberi e la ricarica dei moduli a caldo hanno suonato alla grande; Semplicemente non ero convinto di averne bisogno per la mia piccola "app".

Quindi, torniamo a Gulp allora.

Sul tema del non cambiare le cose per il bene del cambiamento, un altro pezzo di tecnologia che volevo valutare era Yarn over NPM per la gestione delle dipendenze del progetto. Fino a quel momento, avevo sempre usato NPM e Yarn veniva pubblicizzato come un'alternativa migliore e più veloce. Non ho molto da dire su Yarn a parte il fatto che se stai attualmente utilizzando NPM e tutto è a posto, non devi preoccuparti di provare Yarn.

Uno strumento che è arrivato troppo tardi per me per valutare questa applicazione è Parceljs. Con zero configurazione e un browser come BrowserSync come ricaricamento del browser, da allora ho trovato una grande utilità in esso! Inoltre, a difesa di Webpack, mi è stato detto che dalla v4 in poi di Webpack non è necessario un file di configurazione. Aneddoticamente, in un sondaggio più recente che ho eseguito su Twitter, degli 87 intervistati, oltre la metà ha scelto Webpack su Gulp, Parcel o Grunt.

Ho avviato il mio file Gulp con le funzionalità di base per essere installato e funzionante.

Un'attività "predefinita" guarderebbe le cartelle "sorgenti" dei fogli di stile e dei file TypeScript e le compilerebbe in una cartella di build insieme all'HTML di base e alle mappe di origine associate.

Ho anche BrowserSync che funziona con Gulp. Potrei non sapere cosa fare con un file di configurazione di Webpack, ma ciò non significava che fossi una specie di animale. Dover aggiornare manualmente il browser durante l'iterazione con HTML/CSS è davvero il 2010 e BrowserSync ti offre quel breve ciclo di feedback e iterazione che è così utile per la codifica front-end.

Ecco il file gulp di base dell'11.6.2017

Puoi vedere come ho ottimizzato Gulpfile più vicino alla fine della spedizione, aggiungendo la minimizzazione con ugilify:

Struttura del progetto

In conseguenza delle mie scelte tecnologiche, alcuni elementi dell'organizzazione del codice per l'applicazione si stavano definendo. Un gulpfile.js nella radice del progetto, una cartella node_modules (dove Gulp memorizza il codice del plug-in), una cartella preCSS per i fogli di stile di creazione, una cartella ts per i file TypeScript e una cartella di build per il codice compilato.

L'idea era quella di avere un index.html che contenesse la "shell" dell'applicazione, inclusa qualsiasi struttura HTML non dinamica e quindi collegamenti agli stili e al file JavaScript che avrebbero fatto funzionare l'applicazione. Su disco, sarebbe simile a questo:

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

Configurare BrowserSync per guardare quella cartella di build significava che potevo puntare il mio browser su localhost:3000 e tutto andava bene.

Con un sistema di costruzione di base in atto, l'organizzazione dei file sistemata e alcuni progetti di base con cui iniziare, avevo esaurito il materiale per la procrastinazione che potevo legittimamente usare per impedirmi di costruire effettivamente la cosa!

Scrivere un'applicazione

Il principio di come avrebbe funzionato l'applicazione era questo. Ci sarebbe un archivio di dati. Quando il JavaScript caricato caricava quei dati, scorreva ogni giocatore nei dati, creando l'HTML necessario per rappresentare ogni giocatore come una riga nel layout e posizionandoli nella sezione di entrata/uscita appropriata. Quindi le interazioni dell'utente sposterebbero un giocatore da uno stato all'altro. Semplice.

Quando si è trattato di scrivere effettivamente l'applicazione, le due grandi sfide concettuali che dovevano essere comprese erano:

  1. Come rappresentare i dati per un'applicazione in un modo che possa essere facilmente esteso e manipolato;
  2. Come fare in modo che l'interfaccia utente reagisca quando i dati sono stati modificati dall'input dell'utente.

Uno dei modi più semplici per rappresentare una struttura di dati in JavaScript è con la notazione degli oggetti. Quella frase legge un po' di informatica. Più semplicemente, un "oggetto" nel gergo JavaScript è un modo pratico per archiviare i dati.

Considera questo oggetto JavaScript assegnato a una variabile chiamata ioState (per In/Out State):

 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 }

Se non conosci JavaScript così bene, probabilmente puoi almeno capire cosa sta succedendo: ogni riga all'interno delle parentesi graffe è una proprietà (o "chiave" nel gergo JavaScript) e una coppia di valori. Puoi impostare tutti i tipi di cose su una chiave JavaScript. Ad esempio, funzioni, array di altri dati o oggetti nidificati. Ecco un esempio:

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

Il risultato netto è che usando quel tipo di struttura dati puoi ottenere e impostare qualsiasi chiave dell'oggetto. Ad esempio, se vogliamo impostare il conteggio dell'oggetto ioState su 7:

 ioState.Count = 7;

Se vogliamo impostare un pezzo di testo su quel valore, la notazione funziona in questo modo:

 aTextNode.textContent = ioState.Count;

Puoi vedere che ottenere valori e impostare valori su quell'oggetto stato è semplice nel lato JavaScript delle cose. Tuttavia, riflettere tali modifiche nell'interfaccia utente lo è meno. Questa è l'area principale in cui i framework e le biblioteche cercano di astrarre il dolore.

In termini generali, quando si tratta di aggiornare l'interfaccia utente in base allo stato, è preferibile evitare di interrogare il DOM, poiché questo è generalmente considerato un approccio non ottimale.

Considera l'interfaccia In/Out. In genere mostra un elenco di potenziali giocatori per un gioco. Sono elencati verticalmente, uno sotto l'altro, in fondo alla pagina.

Forse ogni giocatore è rappresentato nel DOM con label che racchiude un input di casella di controllo. In questo modo, facendo clic su un giocatore si commuta il giocatore su "In" in virtù dell'etichetta che rende l'input "selezionato".

Per aggiornare la nostra interfaccia, potremmo avere un "ascoltatore" su ogni elemento di input nel JavaScript. Con un clic o una modifica, la funzione interroga il DOM e conta quanti input del nostro giocatore vengono controllati. Sulla base di tale conteggio, aggiorneremo quindi qualcos'altro nel DOM per mostrare all'utente quanti giocatori vengono controllati.

Consideriamo il costo di quell'operazione di base. Stiamo ascoltando su più nodi DOM il clic/controllo di un input, quindi interrogando il DOM per vedere quanti di un particolare tipo di DOM sono controllati, quindi scrivendo qualcosa nel DOM per mostrare all'utente, per quanto riguarda l'interfaccia utente, il numero di giocatori abbiamo appena contato.

L'alternativa sarebbe mantenere lo stato dell'applicazione come oggetto JavaScript in memoria. Un clic su un pulsante/input nel DOM potrebbe semplicemente aggiornare l'oggetto JavaScript e quindi, in base a tale modifica nell'oggetto JavaScript, eseguire un aggiornamento a passaggio singolo di tutte le modifiche all'interfaccia necessarie. Potremmo saltare la query al DOM per contare i giocatori poiché l'oggetto JavaScript conterrebbe già tali informazioni.

Così. L'uso di una struttura di oggetti JavaScript per lo stato sembrava semplice ma sufficientemente flessibile da incapsulare lo stato dell'applicazione in un dato momento. Anche la teoria su come gestirlo sembrava abbastanza valida: doveva essere questo il significato di frasi come "flusso di dati unidirezionale"? Tuttavia, il primo vero trucco sarebbe creare del codice che aggiornerebbe automaticamente l'interfaccia utente in base a eventuali modifiche a quei dati.

La buona notizia è che le persone più intelligenti di me hanno già capito queste cose ( grazie al cielo! ). Le persone hanno perfezionato approcci a questo tipo di sfida sin dagli albori delle applicazioni. Questa categoria di problemi è il pane quotidiano dei "design patterns". Il soprannome di "design pattern" mi sembrava esoterico all'inizio, ma dopo aver scavato un po' tutto ha iniziato a suonare meno informatica e più buon senso.

Modelli di progettazione

Un design pattern, nel lessico dell'informatica, è un modo predefinito e collaudato per risolvere una sfida tecnica comune. Pensa ai modelli di progettazione come all'equivalente codificato di una ricetta di cucina.

Forse la letteratura più famosa sui design pattern è "Design Patterns: Elements of Reusable Object-Oriented Software" del 1994. Sebbene si occupi di C++ e smalltalk, i concetti sono trasferibili. Per JavaScript, "Learning JavaScript Design Patterns" di Addy Osmani copre un terreno simile. Puoi anche leggerlo gratuitamente online qui.

Modello dell'osservatore

Tipicamente i modelli di progettazione sono divisi in tre gruppi: Creativi, Strutturali e Comportamentali. Stavo cercando qualcosa di comportamentale che aiutasse a gestire la comunicazione dei cambiamenti nelle diverse parti dell'applicazione.

Più di recente, ho visto e letto un ottimo approfondimento sull'implementazione della reattività all'interno di un'app di Gregg Pollack. C'è sia un post sul blog che un video per il tuo divertimento qui.

Quando ho letto la descrizione di apertura del modello 'Osservatore' in Learning JavaScript Design Patterns , ero abbastanza sicuro che fosse il modello per me. È così descritto:

L'Observer è un modello di progettazione in cui un oggetto (noto come soggetto) mantiene un elenco di oggetti dipendenti da esso (osservatori), notificandoli automaticamente di eventuali modifiche allo stato.

Quando un soggetto ha bisogno di notificare agli osservatori qualcosa di interessante che sta accadendo, trasmette una notifica agli osservatori (che può includere dati specifici relativi all'argomento della notifica).

La chiave della mia eccitazione era che questo sembrava offrire un modo per aggiornare le cose da sole quando necessario.

Supponiamo che l'utente abbia fatto clic su un giocatore chiamato "Betty" per selezionare che era "In" per il gioco. Potrebbero essere necessarie alcune cose nell'interfaccia utente:

  1. Aggiungi 1 al conteggio delle partite
  2. Rimuovi Betty dal pool di giocatori "Fuori".
  3. Aggiungi Betty al pool di giocatori "In".

L'app dovrebbe anche aggiornare i dati che rappresentano l'interfaccia utente. Quello che volevo evitare era questo:

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

L'obiettivo era quello di avere un flusso di dati elegante che aggiornasse ciò che era necessario nel DOM quando e se i dati centrali fossero stati modificati.

Con un modello Observer, è stato possibile inviare aggiornamenti allo stato e quindi all'interfaccia utente in modo abbastanza succinto. Ecco un esempio, la funzione effettiva utilizzata per aggiungere un nuovo giocatore all'elenco:

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

La parte rilevante per il pattern Observer è il metodo io.notify . Poiché questo ci mostra la modifica degli items fanno parte dello stato dell'applicazione, lascia che ti mostri l'osservatore che ha ascoltato le modifiche agli "elementi":

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

Abbiamo un metodo di notifica che apporta modifiche ai dati e quindi osservatori a quei dati che rispondono quando le proprietà a cui sono interessati vengono aggiornate.

Con questo approccio, l'app potrebbe avere elementi osservabili che controllano le modifiche in qualsiasi proprietà dei dati ed eseguire una funzione ogni volta che si verifica una modifica.

Se sei interessato al modello Observer che ho scelto, lo descrivo più completamente qui.

Ora esisteva un approccio per aggiornare l'interfaccia utente in modo efficace in base allo stato. pesca. Tuttavia, questo mi ha ancora lasciato con due problemi evidenti.

Uno era come archiviare lo stato tra ricariche/sessioni di pagine e il fatto che, nonostante l'interfaccia utente funzionasse, visivamente non era molto "simile a un'app". Ad esempio, se è stato premuto un pulsante, l'interfaccia utente è cambiata istantaneamente sullo schermo. Semplicemente non era particolarmente avvincente.

Affrontiamo prima il lato di archiviazione delle cose.

Stato di salvataggio

Il mio interesse principale da un lato dello sviluppo che entra in questo incentrato sulla comprensione di come le interfacce delle app potrebbero essere costruite e rese interattive con JavaScript. Il modo in cui archiviare e recuperare i dati da un server o affrontare l'autenticazione degli utenti e gli accessi era "fuori portata".

Pertanto, invece di collegarmi a un servizio web per le esigenze di archiviazione dei dati, ho optato per mantenere tutti i dati sul client. Esistono diversi metodi della piattaforma Web per archiviare i dati su un client. Ho optato per localStorage .

L'API per localStorage è incredibilmente semplice. Imposti e ottieni dati in questo modo:

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

LocalStorage ha un metodo setItem a cui si passano due stringhe. La prima è il nome della chiave con cui si desidera archiviare i dati e la seconda stringa è la stringa effettiva che si desidera archiviare. Il metodo getItem accetta una stringa come argomento che restituisce tutto ciò che è archiviato sotto quella chiave in localStorage. Bello e semplice.

Tuttavia, tra i motivi per non utilizzare localStorage c'è il fatto che tutto deve essere salvato come 'stringa'. Ciò significa che non puoi archiviare direttamente qualcosa come un array o un oggetto. Ad esempio, prova a eseguire questi comandi nella console del browser:

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

Anche se abbiamo provato a impostare il valore di 'myArray' come array; quando l'abbiamo recuperato, era stato memorizzato come stringa (notare le virgolette attorno a "1,2,3,4").

Puoi certamente archiviare oggetti e array con localStorage, ma devi essere consapevole che devono essere convertiti avanti e indietro dalle stringhe.

Quindi, per scrivere i dati di stato in localStorage, è stato scritto in una stringa con il metodo JSON.stringify() questo modo:

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

Quando è stato necessario recuperare i dati da localStorage, la stringa è stata riconvertita in dati utilizzabili con il metodo JSON.parse() in questo modo:

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

L'utilizzo localStorage significava che tutto era sul client e ciò significava che non c'erano servizi di terze parti o problemi di archiviazione dei dati.

I dati ora persistevano negli aggiornamenti e nelle sessioni - Evviva! La cattiva notizia era che localStorage non sopravvive a un utente che svuota i dati del browser. Quando qualcuno lo faceva, tutti i suoi dati In/Out andrebbero persi. Questa è una grave mancanza.

Non è difficile capire che `localStorage` probabilmente non è la soluzione migliore per le applicazioni 'corrette'. Oltre al suddetto problema con le stringhe, è anche lento per lavori seri poiché blocca il "thread principale". Stanno arrivando alternative, come KV Storage, ma per ora, prendi nota mentale di avvertirne l'uso in base all'idoneità.

Nonostante la fragilità del salvataggio dei dati in locale sul dispositivo di un utente, è stato opposto resistenza al collegamento a un servizio oa un database. Invece, il problema è stato aggirato offrendo un'opzione "carica/salva". Ciò consentirebbe a qualsiasi utente di In/Out di salvare i propri dati come file JSON che potrebbe essere caricato nuovamente nell'app se necessario.

Funzionava bene su Android ma in modo molto meno elegante per iOS. Su un iPhone, ha provocato una pazzia di testo sullo schermo come questo:

Su un iPhone, ha provocato una pazzia di testo sullo schermo
(Grande anteprima)

Come puoi immaginare, ero tutt'altro che il solo a rimproverare Apple tramite WebKit per questa mancanza. Il bug rilevante era qui.

Al momento in cui scrivo questo bug ha una soluzione e una patch ma deve ancora farsi strada in iOS Safari. Presumibilmente, iOS13 lo risolve ma è in versione beta mentre scrivo.

Quindi, per il mio prodotto minimo praticabile, quello era lo storage indirizzato. Ora era il momento di provare a rendere le cose più "simili ad app"!

App-I-Ness

Risulta dopo molte discussioni con molte persone, definire esattamente cosa significhi "app come" è abbastanza difficile.

Alla fine, ho deciso che "simile ad un'app" fosse sinonimo di una fluidità visiva che di solito manca dal web. Quando penso alle app che si sentono bene da usare, sono tutte dotate di movimento. Non gratuito, ma movimento che si aggiunge al racconto delle tue azioni. Potrebbero essere le transizioni di pagina tra le schermate, il modo in cui i menu vengono visualizzati. È difficile da descrivere a parole, ma la maggior parte di noi lo sa quando lo vediamo.

Il primo tocco visivo necessario era spostare i nomi dei giocatori in alto o in basso da "In" a "Out" e viceversa quando selezionati. Far passare istantaneamente un giocatore da una sezione all'altra è stato semplice ma certamente non "simile a un'app". Si spera che un'animazione quando si fa clic sul nome di un giocatore enfatizzi il risultato di quell'interazione: il giocatore che si sposta da una categoria all'altra.

Come molti di questi tipi di interazioni visive, la loro apparente semplicità smentisce la complessità implicata nel farlo funzionare bene.

Ci sono volute alcune iterazioni per ottenere il movimento giusto, ma la logica di base era questa:

  • Dopo aver cliccato su un "giocatore", cattura dove si trova, geometricamente, sulla pagina;
  • Misura quanto lontano è la parte superiore dell'area a cui il giocatore deve spostarsi se sale ("In") e quanto è lontano la parte inferiore, se scende ("Out");
  • Se si sale, è necessario lasciare uno spazio uguale all'altezza della riga del giocatore mentre il giocatore si sposta in alto e i giocatori sopra devono crollare verso il basso alla stessa velocità del tempo impiegato dal giocatore per viaggiare in alto per atterrare nello spazio lasciati liberi dai giocatori "In" esistenti (se presenti) che scendono;
  • Se un giocatore sta andando "Out" e si sta spostando verso il basso, tutto il resto deve spostarsi in alto nello spazio a sinistra e il giocatore deve finire al di sotto di tutti i giocatori "Out" attuali.

Uff! È stato più complicato di quanto pensassi in inglese, non importa JavaScript!

C'erano ulteriori complessità da considerare e provare come le velocità di transizione. All'inizio, non era ovvio se una velocità di movimento costante (ad es. 20px per 20 ms) o una durata costante del movimento (ad es. 0,2 s) sarebbe stata migliore. Il primo era leggermente più complicato in quanto la velocità doveva essere calcolata "al volo" in base alla distanza che il giocatore doveva percorrere: una distanza maggiore che richiedeva una durata di transizione più lunga.

Tuttavia, si è scoperto che una durata di transizione costante non era solo più semplice nel codice; in realtà ha prodotto un effetto più favorevole. La differenza era sottile, ma questo è il tipo di scelte che puoi determinare solo dopo aver visto entrambe le opzioni.

Ogni tanto mentre si cercava di inchiodare questo effetto, un difetto visivo catturava l'attenzione ma era impossibile decostruire in tempo reale. Ho scoperto che il miglior processo di debug era creare una registrazione QuickTime dell'animazione e quindi esaminarla un fotogramma alla volta. Invariabilmente questo ha rivelato il problema più rapidamente di qualsiasi debug basato su codice.

Guardando il codice ora, posso apprezzare che su qualcosa al di là della mia umile app, questa funzionalità potrebbe quasi sicuramente essere scritta in modo più efficace. Dato che l'app conoscerebbe il numero di giocatori e conoscerebbe l'altezza fissa delle lamelle, dovrebbe essere del tutto possibile eseguire tutti i calcoli della distanza nel solo JavaScript, senza alcuna lettura del DOM.

Non è che ciò che è stato spedito non funziona, è solo che non è il tipo di soluzione di codice che mostreresti su Internet. Oh, aspetta.

Altre interazioni "simili alle app" erano molto più facili da realizzare. Invece di entrare e uscire semplicemente dai menu con qualcosa di semplice come attivare o disattivare una proprietà di visualizzazione, è stato guadagnato un sacco di chilometri semplicemente esponendoli con un po' più di finezza. Era ancora attivato semplicemente ma CSS stava facendo tutto il lavoro pesante:

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

Lì, quando l' data-evswitcher-showing="true" è stato attivato su un elemento padre, il menu si dissolveva, tornava nella sua posizione predefinita e gli eventi del puntatore venivano riattivati ​​in modo che il menu potesse ricevere clic.

Metodologia del foglio di stile ECSS

Noterai in quel codice precedente che dal punto di vista della creazione, le sostituzioni CSS vengono nidificate all'interno di un selettore padre. Questo è il modo in cui preferisco sempre scrivere fogli di stile dell'interfaccia utente; un'unica fonte di verità per ogni selettore ed eventuali sostituzioni per quel selettore racchiuse in un unico insieme di parentesi graffe. È un modello che richiede l'uso di un processore CSS (Sass, PostCSS, LESS, Stylus, et al) ma ritengo sia l'unico modo positivo per utilizzare la funzionalità di nidificazione.

Ho cementato questo approccio nel mio libro, Enduring CSS e nonostante ci sia una pletora di metodi più coinvolti disponibili per scrivere CSS per gli elementi dell'interfaccia, ECSS ha servito bene me e i grandi team di sviluppo con cui lavoro da quando l'approccio è stato documentato per la prima volta in modo nel 2014! Si è dimostrato altrettanto efficace in questo caso.

Parzializzazione del dattiloscritto

Anche senza un processore CSS o un linguaggio superset come Sass, CSS ha avuto la possibilità di importare uno o più file CSS in un altro con la direttiva import:

 @import "other-file.css";

Quando ho iniziato con JavaScript sono rimasto sorpreso che non esistesse un equivalente. Ogni volta che i file di codice diventano più lunghi di uno schermo o così alti, sembra sempre che dividerli in parti più piccole sarebbe utile.

Un altro vantaggio dell'utilizzo di TypeScript è che ha un modo meravigliosamente semplice per dividere il codice in file e importarli quando necessario.

Questa funzionalità precedeva i moduli JavaScript nativi ed era un'ottima funzionalità di praticità. Quando TypeScript è stato compilato, ha ricucito tutto in un unico file JavaScript. Significava che era possibile suddividere facilmente il codice dell'applicazione in file parziali gestibili per la creazione e importarli quindi facilmente nel file principale. La parte superiore del principale inout.ts simile a questa:

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

Questo semplice compito di pulizia e organizzazione ha aiutato enormemente.

Eventi multipli

All'inizio, ho sentito che dal punto di vista della funzionalità, un singolo evento, come "Tuesday Night Football" sarebbe stato sufficiente. In quello scenario, se hai caricato In/Out hai semplicemente aggiunto/rimosso o spostato giocatori dentro o fuori e basta. Non c'era la nozione di eventi multipli.

Ho subito deciso che (anche cercando un prodotto minimo praticabile) questo avrebbe reso l'esperienza piuttosto limitata. E se qualcuno organizzasse due partite in giorni diversi, con un diverso elenco di giocatori? Sicuramente In/Out potrebbe/dovrebbe soddisfare tale esigenza? Non ci è voluto troppo tempo per rimodellare i dati per renderlo possibile e modificare i metodi necessari per caricare in un set diverso.

All'inizio, il set di dati predefinito era simile a questo:

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

Un array contenente un oggetto per ogni giocatore.

Dopo aver tenuto conto di più eventi, è stato modificato in questo modo:

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

I nuovi dati erano un array con un oggetto per ogni evento. Quindi in ogni evento c'era una proprietà EventData che era un array con oggetti giocatore come prima.

Ci è voluto molto più tempo per riconsiderare come l'interfaccia potesse gestire al meglio questa nuova capacità.

Fin dall'inizio, il design era sempre stato molto sterile. Considerando che questo doveva essere anche un esercizio di design, non mi sentivo abbastanza coraggioso. Quindi è stato aggiunto un po' più di stile visivo, a partire dall'intestazione. Questo è ciò che ho preso in giro in Sketch:

Un mockup del design dell'app rivisto
Mockup di design rivisto. (Grande anteprima)

Non avrebbe vinto premi, ma è stato sicuramente più avvincente di dove è iniziato.

Estetica a parte, è stato solo quando qualcun altro l'ha fatto notare che ho apprezzato che l'icona del grande vantaggio nell'intestazione fosse molto confusa. La maggior parte delle persone pensava che fosse un modo per aggiungere un altro evento. In realtà, è passato a una modalità "Aggiungi giocatore" con una transizione fantasiosa che ti consente di digitare il nome del giocatore nello stesso posto in cui si trovava attualmente il nome dell'evento.

Questo era un altro caso in cui gli occhi nuovi erano inestimabili. È stata anche una lezione importante per lasciar andare. La verità onesta era che avevo mantenuto la transizione della modalità di input nell'intestazione perché sentivo che era interessante e intelligente. Tuttavia, il fatto era che non serviva al design e quindi all'applicazione nel suo insieme.

Questo è stato modificato nella versione live. Invece, l'intestazione si occupa solo degli eventi, uno scenario più comune. Nel frattempo, l'aggiunta di giocatori avviene da un sottomenu. Ciò conferisce all'app una gerarchia molto più comprensibile.

L'altra lezione appresa qui è che, quando possibile, è estremamente vantaggioso ottenere un feedback sincero dai colleghi. Se sono persone buone e oneste, non ti lasceranno passare!

Riepilogo: il mio codice puzza

Destra. Finora, così normale retrospettiva sull'avventura tecnologica; queste cose sono dieci un centesimo su Medium! La formula è più o meno questa: gli sviluppatori spiegano in dettaglio come hanno abbattuto tutti gli ostacoli per rilasciare un software finemente sintonizzato su Internet e quindi raccogliere un'intervista su Google o essere stati assunti da qualche parte. Tuttavia, la verità è che sono stato per la prima volta in questa malarkey di creazione di app, quindi il codice alla fine è stato spedito come l'applicazione "finita" puzzava in paradiso!

Ad esempio, l'implementazione del modello Observer utilizzata ha funzionato molto bene. All'inizio ero organizzato e metodico, ma quell'approccio "è andato a sud" quando sono diventato più disperato nel finire le cose. Come una dieta seriale, le vecchie abitudini familiari si sono insinuate di nuovo e la qualità del codice è successivamente diminuita.

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.