Come costruiamo i componenti dell'interfaccia utente in Rails su Flywheel
Pubblicato: 2019-11-16Mantenere la coerenza visiva in una grande applicazione Web è un problema comune a molte organizzazioni. In Flywheel, non siamo diversi. La nostra applicazione web principale è costruita con Ruby on Rails e abbiamo circa 15 sviluppatori Rails e tre sviluppatori front-end che scrivono codice su di essa in un dato giorno. Siamo anche grandi nel design (è uno dei nostri valori fondamentali come azienda) e abbiamo tre designer che lavorano a stretto contatto con gli sviluppatori nei nostri team Scrum.
Uno dei nostri principali obiettivi è garantire che qualsiasi sviluppatore di Flywheel possa creare una pagina reattiva senza alcun ostacolo. I blocchi stradali generalmente includevano il non sapere quali componenti esistenti utilizzare per creare un mockup (che porta a gonfiare la base di codice con componenti molto simili e ridondanti) e il non sapere quando discutere della riutilizzabilità con i progettisti. Ciò contribuisce a esperienze dei clienti incoerenti, frustrazione per gli sviluppatori e un linguaggio di progettazione disparato tra sviluppatori e designer.
Abbiamo esaminato diverse iterazioni di guide di stile e metodi per creare/mantenere modelli e componenti dell'interfaccia utente e ogni iterazione ha aiutato a risolvere i problemi che stavamo affrontando in quel momento. Ora siamo a un nuovo (per noi) approccio che sono fiducioso ci preparerà per molto tempo a venire. Se affronti problemi simili nella tua applicazione Rails e desideri avvicinarti ai componenti dal lato server, spero che questo articolo possa darti alcune idee.
In questo articolo mi addentrerò in:
- Per cosa stiamo risolvendo
- Componenti vincolanti
- Rendering dei componenti lato server
- Dove non possiamo utilizzare componenti lato server
Per cosa stiamo risolvendo
Volevamo vincolare completamente i nostri componenti dell'interfaccia utente ed eliminare la possibilità di creare la stessa interfaccia utente in più di un modo. Anche se un cliente potrebbe non essere in grado di dirlo (all'inizio), non avere vincoli sui componenti crea un'esperienza di sviluppo confusa, rende le cose molto difficili da mantenere e rende difficile apportare modifiche al design globale.
Il modo tradizionale in cui ci siamo avvicinati ai componenti era attraverso la nostra guida di stile, che elencava l'intero lotto di markup richiesto per costruire un determinato componente. Ad esempio, ecco come appariva la pagina della guida di stile per il nostro componente lamella:
Questo ha funzionato bene e ci è andato bene per diversi anni, ma i problemi hanno iniziato a insinuarsi quando abbiamo aggiunto varianti, stati o modi alternativi per utilizzare il componente. Con una parte complessa dell'interfaccia utente, è diventato complicato fare riferimento alla guida di stile per sapere quali classi usare e quali evitare e in quale ordine doveva trovarsi il markup per produrre la variazione desiderata. E spesso, i designer apportano piccole aggiunte o modifiche a un determinato componente. Dal momento che la guida allo stile non lo supportava del tutto, gli hack alternativi per visualizzare correttamente quel tweak (come cannibalizzare in modo inappropriato parte di un altro componente) sono diventati irritanti comuni.
Esempio di componente non vincolato
Per illustrare come emergono le incoerenze nel tempo, utilizzerò un esempio semplice (e artificioso) ma molto comune di uno dei nostri componenti nell'app Flywheel: le intestazioni delle carte.
Partendo da un modello di design, ecco come appariva l'intestazione di una carta. Era piuttosto semplice con un titolo, un pulsante e un bordo inferiore.
.card__header
.card__header-left
%h2 Backups
.card__header-right
= link_to "#" do
= icon("plus_small")
Dopo che è stato codificato, immagina un designer che desidera aggiungere un'icona a sinistra del titolo. Fuori dagli schemi, non ci sarà alcun margine tra l'icona e il titolo.
...
.card__header-left
= icon("arrow_backup", color: "gray25")
%h2 Backups
...
Idealmente lo risolveremmo nel CSS per le intestazioni delle carte, ma per questo esempio, diciamo che un altro sviluppatore ha pensato "Oh, lo so! Abbiamo alcuni aiutanti di margine. Darò uno schiaffo a una classe di supporto sul titolo".
...
.card__header-left
= icon("arrow_backup", color: "gray25")
%h2.--ml-10 Backups
...
Beh, tecnicamente sembra come il mockup ha fatto, giusto?! Certo, ma diciamo che un mese dopo, un altro sviluppatore ha bisogno di un'intestazione di carta, ma senza l'icona. Trovano l'ultimo esempio, lo copiano/incollano e rimuovono semplicemente l'icona.
Ancora una volta sembra corretto, giusto? Fuori contesto, per qualcuno senza un occhio acuto per il design, certo! Ma guardalo accanto all'originale. Quel margine sinistro sul titolo è ancora lì perché non si sono resi conto che l'helper del margine sinistro doveva essere rimosso!
Facendo un ulteriore passo avanti in questo esempio, supponiamo che un altro mockup richieda un'intestazione di carta senza bordo inferiore. Si potrebbe trovare uno stato che abbiamo nella guida di stile chiamato "senza bordi" e applicarlo. Perfetto!
Un altro sviluppatore potrebbe quindi provare a riutilizzare quel codice, ma in questo caso ha effettivamente bisogno di un bordo. Diciamo ipoteticamente che ignorino l'uso corretto documentato nella guida di stile e non si rendono conto che la rimozione della classe senza bordi darà loro il bordo. Invece, aggiungono una regola orizzontale. Alla fine c'è un'imbottitura extra tra il titolo e il bordo, quindi applicano una classe di supporto all'ora e voilà!
Con tutte queste modifiche all'intestazione della carta originale, ora abbiamo un pasticcio nel codice.
.card__header.--borderless
.card__header-left
%h2.--ml-10 Backups
.card__header-right
= link_to "#" do
= icon("plus_small")
%hr.--mt-0.--mb-0
Tieni presente che l'esempio sopra serve solo per illustrare il punto in cui i componenti non vincolati possono diventare disordinati nel tempo. Se qualcuno del nostro team ha provato a spedire una variante dell'intestazione di una carta, dovrebbe essere intercettato da una revisione del design o dalla revisione del codice. Ma cose come questa a volte sfuggono alle crepe, da qui il nostro bisogno di cose a prova di proiettile!
Componenti vincolanti
Potresti pensare che i problemi sopra elencati siano già stati chiaramente risolti con i componenti. È un presupposto corretto! I framework front-end come React e Vue sono molto popolari per questo scopo esatto; sono strumenti straordinari per incapsulare l'interfaccia utente. Tuttavia, c'è un inconveniente con loro che non sempre ci piace: richiedono che l'interfaccia utente sia visualizzata da JavaScript.
L'applicazione Flywheel è molto pesante nel back-end con HTML principalmente renderizzato dal server, ma fortunatamente per noi i componenti possono assumere molte forme. Alla fine della giornata, un componente dell'interfaccia utente è un incapsulamento di stili e regole di progettazione che restituisce il markup a un browser. Con questa consapevolezza, possiamo adottare lo stesso approccio ai componenti, ma senza il sovraccarico di un framework JavaScript.
Di seguito esamineremo il modo in cui costruiamo componenti vincolati, ma ecco alcuni dei vantaggi che abbiamo riscontrato utilizzandoli:
- Non c'è mai un modo davvero sbagliato per mettere insieme un componente.
- Il componente fa tutto il design thinking per te. (Passi solo le opzioni!)
- La sintassi per creare un componente è molto coerente e facile da ragionare.
- Se è necessaria una modifica del design su un componente, possiamo cambiarla una volta nel componente e essere certi che sia aggiornato ovunque.
Rendering dei componenti lato server
Quindi di cosa stiamo parlando vincolando i componenti? Scendiamo!
Come accennato in precedenza, vogliamo che qualsiasi sviluppatore che lavora nell'applicazione Flywheel sia in grado di guardare un modello di progettazione di una pagina e sia in grado di creare immediatamente quella pagina senza impedimenti. Ciò significa che il metodo di creazione dell'interfaccia utente deve essere A) documentato molto bene e B) molto dichiarativo e privo di congetture.
Parziali in soccorso (o almeno così pensavamo)
Una prima prova che abbiamo provato in passato è stata quella di utilizzare i parziali di Rails. I parziali sono l'unico strumento che Rails ti offre per il riutilizzo nei modelli. Naturalmente, sono la prima cosa che tutti cercano. Ma ci sono svantaggi significativi nell'affidarsi a loro perché se è necessario combinare la logica con un modello riutilizzabile hai due scelte: duplicare la logica su ogni controller che utilizza il parziale o incorporare la logica nel parziale stesso.
I parziali prevengono gli errori di duplicazione di copia/incolla e funzionano bene per le prime due volte in cui è necessario riutilizzare qualcosa. Ma dalla nostra esperienza, i parziali vengono presto ingombrati dal supporto per sempre più funzionalità e logica. Ma la logica non dovrebbe vivere nei modelli!
Introduzione alle cellule
Fortunatamente, esiste un'alternativa migliore ai parziali che ci consente sia di riutilizzare il codice che di mantenere la logica fuori dalla vista. Si chiama Cells, una gemma Rubino sviluppata da Trailblazer. Le celle esistono molto prima dell'aumento della popolarità nei framework front-end come React e Vue e consentono di scrivere modelli di visualizzazione incapsulati che gestiscono sia la logica che i modelli. Forniscono un'astrazione del modello di visualizzazione, che Rails non ha proprio pronto all'uso. In realtà utilizziamo Cells nell'app Flywheel da un po' di tempo ormai, ma non su scala globale e super riutilizzabile.
Al livello più semplice, Cells ci consente di astrarre un pezzo di markup come questo (usiamo Haml per il nostro linguaggio di template):
%div
%h1 Hello, world!
In un modello di visualizzazione riutilizzabile (molto simile ai parziali a questo punto) e trasformalo in questo:
= cell("hello_world")
Questo alla fine ci aiuta a vincolare il componente a dove non è possibile aggiungere classi helper o componenti figlio errati senza modificare la cella stessa.
Costruire cellule
Mettiamo tutte le nostre celle dell'interfaccia utente in una directory app/cells/ui. Ogni cella deve contenere un solo file Ruby, con suffisso _cell.rb. Puoi tecnicamente scrivere i modelli direttamente in Ruby con l'helper content_tag, ma la maggior parte delle nostre celle contiene anche un modello Haml corrispondente che risiede in una cartella denominata dal componente.
Una cella super basilare senza logica al suo interno assomiglia a questa:
// cells/ui/slat_cell.rb
module UI
class SlatCell < ViewModel
def show
end
end
end
Il metodo show è ciò che viene visualizzato quando istanzia la cella e cercherà automaticamente un file show.haml corrispondente nella cartella con lo stesso nome della cella. In questo caso, è app/cells/ui/slat (tuttiamo l'ambito di tutte le nostre celle dell'interfaccia utente nel modulo dell'interfaccia utente).
Nel modello, puoi accedere alle opzioni passate alla cella. Ad esempio, se la cella è istanziata in una vista come = cell(“ui/slat”, title: “Title”, subtitle: “Subtitle”, label: “Label”), possiamo accedere a tali opzioni tramite l'oggetto options.
// cells/ui/slat/show.haml
.slat
.slat__inner
.slat__content
%h4= options[:title]
%p= options[:subtitle]
= icon(options[:icon], color: "blue")
Molte volte sposteremo elementi semplici e i loro valori in un metodo nella cella per impedire il rendering di elementi vuoti se non è presente un'opzione.
// cells/ui/slat_cell.rb
def title
return unless options[:title]
content_tag :h4, options[:title]
end
def subtitle
return unless options[:subtitle]
content_tag :p, options[:subtitle]
end
// cells/ui/slat/show.haml
.slat
.slat__inner
.slat__content
= title
= subtitle
Avvolgere le celle con un'utilità dell'interfaccia utente
Dopo aver dimostrato il concetto che questo potrebbe funzionare su larga scala, ho voluto affrontare il markup estraneo richiesto per chiamare una cella. Semplicemente non scorre abbastanza bene ed è difficile da ricordare. Quindi abbiamo creato un piccolo aiuto per questo! Ora possiamo semplicemente chiamare = ui "nome_del_componente" e passare le opzioni in linea.
= ui "slat", title: "Title", subtitle: "Subtitle", label: "Label"
Passare le opzioni come blocco anziché in linea
Portando un po' oltre l'utilità dell'interfaccia utente, è diventato subito evidente che una cella con un sacco di opzioni tutte su una riga sarebbe stata super difficile da seguire e semplicemente brutta. Ecco un esempio di una cella con molte opzioni definite in linea:
= ui “slat", title: “Title”, subtitle: “Subtitle”, label: “Label”, link: “#”, tertiary_title: “Tertiary”, disabled: true, checklist: [“Item 1”, “Item 2”, “Item 3”]
È molto ingombrante, il che ci porta a creare una classe chiamata OptionProxy che intercetta i metodi di setter di Cells e li traduce in valori hash, che vengono poi uniti in opzioni. Se sembra complicato, non preoccuparti, è complicato anche per me. Ecco una sintesi della classe OptionProxy scritta da Adam, uno dei nostri ingegneri software senior.
Ecco un esempio di utilizzo della classe OptionProxy all'interno della nostra cella:
module UI
class SlatCell < ViewModel
def show
OptionProxy.new(self).yield!(options, &block)
super()
end
end
end
Ora con quello in atto, possiamo trasformare le nostre ingombranti opzioni in linea in un blocco più piacevole!
= ui "slat" do |slat|
- slat.title = "Title"
- slat.subtitle = "Subtitle"
- slat.label = "Label"
- slat.link = "#"
- slat.tertiary_title = "Tertiary"
- slat.disabled = true
- slat.checklist = ["Item 1", "Item 2", "Item 3"]
Introduzione alla logica
Fino a questo punto, gli esempi non hanno incluso alcuna logica su ciò che viene visualizzato nella vista. Questa è una delle cose migliori che Cells offre, quindi parliamone!
Rimanendo con il nostro componente slat, abbiamo la necessità di rendere a volte l'intera cosa come un collegamento e talvolta renderla come un div, a seconda che sia presente o meno un'opzione di collegamento. Credo che questo sia l'unico componente che abbiamo che può essere reso come un div o un collegamento, ma è un esempio piuttosto chiaro del potere di Cells.
Il metodo seguente chiama un helper link_to o content_tag a seconda della presenza di opzioni [:link]
.
Nota: questo è stato ispirato e creato da Adam Lassek, che è stato estremamente influente nell'aiutarci a costruire l'intero metodo di sviluppo dell'interfaccia utente con Cells.
def container(&block)
tag =
if options[:link]
[:link_to, options[:link]]
else
[:content_tag, :div]
end
send(*tag, class: “slat__inner”, &block)
end
Ciò ci consente di sostituire l'elemento .slat__inner nel modello con un blocco contenitore:
.slat
= container do
...
Un altro esempio di logica in Cells che utilizziamo molto è quello dell'output condizionale delle classi. Diciamo di aggiungere un'opzione disabilitata alla cella. Nient'altro nell'invocazione della cella cambia, a parte il fatto che ora puoi passare un'opzione disabilitata: true e guarda mentre l'intera cosa si trasforma in uno stato disabilitato (in grigio con collegamenti non selezionabili).
= ui "slat" do |slat|
...
- slat.disabled = true
Quando l'opzione disabilitata è vera, possiamo impostare classi sugli elementi nel modello che sono necessari per ottenere l'aspetto disabilitato desiderato.
.slat{ class: possible_classes("--disabled": options[:disabled]) }
.slat__inner
.slat__content
%h4{ class: possible_classes("--alt": options[:disabled]) }= options[:title]
%p{ class: possible_classes("--alt": options[:disabled]) }=
options[:subtitle]
= icon(options[:icon], color: "gray")
Tradizionalmente, avremmo dovuto ricordare (o fare riferimento alla guida di stile) quali singoli elementi necessitavano di classi aggiuntive per far funzionare tutto correttamente nello stato disabilitato. Le celle ci consentono di dichiarare un'opzione e quindi fare il lavoro pesante per noi.
Nota: possible_classes è un metodo che abbiamo creato per consentire l'applicazione condizionale delle classi in Haml in un modo carino.
Dove non possiamo utilizzare componenti lato server
Sebbene l'approccio cellulare sia estremamente utile per la nostra particolare applicazione e il modo in cui lavoriamo, sarei negligente nel dire che ha risolto il 100% dei nostri problemi. Scriviamo ancora JavaScript (molto di esso) e creiamo alcune esperienze in Vue attraverso la nostra app. Il 75% delle volte, il nostro modello Vue è ancora presente in Haml e leghiamo le nostre istanze Vue all'elemento contenitore, il che ci consente di sfruttare ancora l'approccio cellulare.
Tuttavia, nei punti in cui ha più senso vincolare completamente un componente come istanza Vue a file singolo, non possiamo usare Cells. Le nostre liste selezionate, ad esempio, sono tutte Vue. Ma penso che vada bene! Non abbiamo davvero riscontrato la necessità di avere versioni duplicate dei componenti in entrambi i componenti Cells e Vue, quindi va bene che alcuni componenti siano costruiti al 100% con Vue e altri con Cells. Se un componente viene creato con Vue, significa che è necessario JavaScript per compilarlo nel DOM e per farlo sfruttiamo il framework Vue. Per la maggior parte dei nostri altri componenti, tuttavia, non richiedono JavaScript e, in tal caso, richiedono che il DOM sia già stato creato e noi ci colleghiamo e aggiungiamo listener di eventi.
Mentre continuiamo a progredire con l'approccio cellulare, sperimenteremo sicuramente la combinazione di componenti cellulari e componenti Vue in modo da avere un solo modo per creare e utilizzare i componenti. Non so ancora che aspetto abbia, quindi attraverseremo quel ponte quando arriveremo!
La nostra conclusione
Finora abbiamo convertito una trentina di componenti visivi più utilizzati in Cells. Ci ha dato un'enorme esplosione di produttività e dà agli sviluppatori un senso di conferma che le esperienze che stanno costruendo sono corrette e non hackerate insieme.
Il nostro team di progettazione è più sicuro che mai che i componenti e le esperienze nella nostra app siano 1:1 con ciò che hanno progettato in Adobe XD. Le modifiche o le aggiunte ai componenti ora vengono gestite esclusivamente attraverso un'interazione con un designer e uno sviluppatore front-end, il che mantiene il resto del team concentrato e senza preoccupazioni nel sapere come modificare un componente in modo che corrisponda a un modello di progettazione.
Stiamo costantemente ripetendo il nostro approccio alla limitazione dei componenti dell'interfaccia utente, ma spero che le tecniche illustrate in questo articolo ti diano un'idea di ciò che funziona bene per noi!
Vieni a lavorare in Flywheel!
In Flywheel, ogni reparto ha un impatto significativo sui nostri clienti e sui profitti. Che si tratti di assistenza clienti, sviluppo software, marketing o qualsiasi altra via di mezzo, stiamo tutti lavorando insieme per la nostra missione di creare una società di hosting di cui le persone possano davvero innamorarsi.
Pronto per entrare a far parte del nostro team? Stiamo assumendo! Candidati qui.