Registri degli sviluppatori front-end Habr: refactoring e riflessione

Registri degli sviluppatori front-end Habr: refactoring e riflessione

Sono sempre stato interessato a come è strutturato Habr dall'interno, come è strutturato il flusso di lavoro, come sono strutturate le comunicazioni, quali standard vengono utilizzati e come viene generalmente scritto il codice qui. Fortunatamente, ho avuto questa opportunità, perché recentemente sono entrato a far parte del team habra. Utilizzando l’esempio di un piccolo refactoring della versione mobile, proverò a rispondere alla domanda: com’è lavorare qui al fronte? Nel programma: Node, Vue, Vuex e SSR con salsa da appunti sull'esperienza personale in Habr.

La prima cosa che devi sapere sul team di sviluppo è che siamo pochi. Non abbastanza: questi sono tre fronti, due difensori e il vantaggio tecnico di tutti Habr-Baxley. Naturalmente c'è anche un tester, un designer, tre Vadim, una scopa miracolosa, uno specialista di marketing e altri Bumburum. Ma ci sono solo sei contributori diretti alle fonti di Habr. Questo è abbastanza raro: un progetto con un pubblico multimilionario, che dall'esterno sembra un'impresa gigante, in realtà sembra più un'accogliente startup con la struttura organizzativa più piatta possibile.

Come molte altre aziende IT, Habr professa idee Agile, pratiche CI e questo è tutto. Ma secondo i miei sentimenti, Habr come prodotto si sta sviluppando più a ondate che in modo continuo. Quindi, per diversi sprint di seguito, codifichiamo diligentemente qualcosa, progettiamo e riprogettiamo, rompiamo qualcosa e lo aggiustiamo, risolviamo ticket e ne creiamo di nuovi, saliamo su un rastrello e ci spariamo ai piedi, per rilasciare finalmente la funzionalità in produzione. E poi arriva una certa tregua, un periodo di riqualificazione, il tempo di fare ciò che rientra nel quadrante “importante-non urgente”.

È proprio di questo sprint “fuori stagione” che parleremo di seguito. Questa volta includeva un refactoring della versione mobile di Habr. In generale, l’azienda ripone grandi speranze in questo, che in futuro dovrebbe sostituire l’intero zoo delle incarnazioni di Habr e diventare una soluzione multipiattaforma universale. Un giorno ci saranno layout adattivo, PWA, modalità offline, personalizzazione dell'utente e molte altre cose interessanti.

Impostiamo il compito

Una volta, durante un normale stand-up, uno dei fronti ha parlato di problemi nell'architettura della componente commenti della versione mobile. Con questo in mente, abbiamo organizzato un micro-incontro sotto forma di psicoterapia di gruppo. Tutti a turno hanno detto dove faceva male, hanno registrato tutto su carta, hanno simpatizzato, hanno capito, tranne che nessuno ha applaudito. Il risultato è stato un elenco di 20 problemi, che ha reso chiaro che Habr mobile aveva ancora una strada lunga e spinosa verso il successo.

La mia preoccupazione principale era l'efficienza nell'uso delle risorse e quella che viene chiamata un'interfaccia fluida. Ogni giorno, nel tragitto casa-lavoro-casa, vedevo il mio vecchio telefono che cercava disperatamente di visualizzare 20 titoli nel feed. Sembrava qualcosa del genere:

Registri degli sviluppatori front-end Habr: refactoring e riflessioneInterfaccia Mobile Habr prima del refactoring

Cosa sta succedendo qui? In breve, il server ha servito la pagina HTML a tutti allo stesso modo, indipendentemente dal fatto che l'utente avesse effettuato l'accesso o meno. Quindi il client JS viene caricato e richiede nuovamente i dati necessari, ma adattati per l'autorizzazione. Cioè, in realtà abbiamo fatto lo stesso lavoro due volte. L'interfaccia ha tremolato e l'utente ha scaricato un buon centinaio di kilobyte extra. Nel dettaglio tutto sembrava ancora più inquietante.

Registri degli sviluppatori front-end Habr: refactoring e riflessioneVecchio schema SSR-CSR. L'autorizzazione è possibile solo nelle fasi C3 e C4, quando il Nodo JS non è impegnato a generare HTML e può inoltrare richieste all'API.

La nostra architettura di quel tempo è stata descritta in modo molto accurato da uno degli utenti Habr:

La versione mobile è una schifezza. Lo dico così com'è. Una terribile combinazione di SSR e RSI.

Dovevamo ammetterlo, per quanto triste fosse.

Ho valutato le opzioni, creato un ticket in Jira con una descrizione a livello di "va male adesso, fallo bene" e ho scomposto l'attività a grandi linee:

  • riutilizzare i dati,
  • ridurre al minimo il numero di ridisegnazioni,
  • eliminare le richieste doppie,
  • rendere il processo di caricamento più ovvio.

Riutilizziamo i dati

In teoria, il rendering lato server è progettato per risolvere due problemi: non soffrire delle limitazioni dei motori di ricerca in termini di Indicizzazione SPA e migliorare la metrica FMP (inevitabilmente in peggioramento TTI). In uno scenario classico che finalmente formulato presso Airbnb nel 2013 anno (ancora su Backbone.js), SSR è la stessa applicazione JS isomorfa in esecuzione nell'ambiente Node. Il server invia semplicemente il layout generato come risposta alla richiesta. Quindi avviene la reidratazione sul lato client e quindi tutto funziona senza ricaricare la pagina. Per Habr, come per molte altre risorse con contenuto testuale, il rendering del server è un elemento critico nella costruzione di relazioni amichevoli con i motori di ricerca.

Nonostante siano trascorsi più di sei anni dall'avvento della tecnologia e durante questo periodo molta acqua sia passata sotto i ponti nel mondo front-end, per molti sviluppatori questa idea è ancora avvolta nel segreto. Non ci siamo fatti da parte e abbiamo lanciato in produzione un'applicazione Vue con supporto SSR, tralasciando un piccolo dettaglio: non abbiamo inviato lo stato iniziale al client.

Perché? Non esiste una risposta esatta a questa domanda. O non volevano aumentare la dimensione della risposta dal server, o a causa di una serie di altri problemi di architettura, o semplicemente non è decollato. In un modo o nell'altro, eliminare lo stato e riutilizzare tutto ciò che ha fatto il server sembra abbastanza appropriato e utile. Il compito è in realtà banale - lo stato viene semplicemente iniettato nel contesto di esecuzione e Vue lo aggiunge automaticamente al layout generato come variabile globale: window.__INITIAL_STATE__.

Uno dei problemi sorti è l'incapacità di convertire le strutture cicliche in JSON (riferimento circolare); è stato risolto semplicemente sostituendo tali strutture con le loro controparti piatte.

Inoltre, quando si ha a che fare con contenuti UGC, è necessario ricordare che i dati devono essere convertiti in entità HTML per non interrompere l'HTML. Per questi scopi utilizziamo he.

Riduzione al minimo dei ridisegnamenti

Come puoi vedere dal diagramma sopra, nel nostro caso, un'istanza di Node JS esegue due funzioni: SSR e "proxy" nell'API, dove avviene l'autorizzazione dell'utente. Questa circostanza rende impossibile l'autorizzazione mentre il codice JS è in esecuzione sul server, poiché il nodo è a thread singolo e la funzione SSR è sincrona. Cioè, il server semplicemente non può inviare richieste a se stesso mentre lo stack di chiamate è occupato con qualcosa. Si è scoperto che abbiamo aggiornato lo stato, ma l'interfaccia non ha smesso di contrarsi, poiché i dati sul client dovevano essere aggiornati tenendo conto della sessione dell'utente. Dovevamo insegnare alla nostra applicazione a inserire i dati corretti nello stato iniziale, tenendo conto del login dell’utente.

C'erano solo due soluzioni al problema:

  • allegare i dati di autorizzazione alle richieste tra server;
  • dividere i livelli Node JS in due istanze separate.

La prima soluzione richiedeva l'utilizzo di variabili globali sul server, mentre la seconda estendeva di almeno un mese il termine per il completamento dell'attività.

Come fare una scelta? Habr spesso si muove lungo il percorso di minor resistenza. A livello informale, c'è un desiderio generale di ridurre al minimo il ciclo dall'idea al prototipo. Il modello di atteggiamento nei confronti del prodotto ricorda in qualche modo i postulati di booking.com, con l'unica differenza che Habr prende molto più seriamente il feedback degli utenti e si fida di te, come sviluppatore, per prendere tali decisioni.

Seguendo questa logica e il mio desiderio di risolvere rapidamente il problema, ho scelto le variabili globali. E, come spesso accade, prima o poi bisogna pagarli. Abbiamo pagato quasi subito: abbiamo lavorato nel fine settimana, chiarito le conseguenze, scritto Post mortem e cominciò a dividere il server in due parti. L'errore era molto stupido e il bug che lo coinvolgeva non era facile da riprodurre. E sì, è un peccato per questo, ma in un modo o nell'altro, inciampando e gemendo, il mio PoC con variabili globali è comunque entrato in produzione e sta funzionando con successo in attesa del passaggio a una nuova architettura “a due nodi”. Questo è stato un passo importante, perché formalmente l'obiettivo è stato raggiunto: SSR ha imparato a fornire una pagina completamente pronta per l'uso e l'interfaccia utente è diventata molto più calma.

Registri degli sviluppatori front-end Habr: refactoring e riflessioneInterfaccia Mobile Habr dopo la prima fase di refactoring

In definitiva, l’architettura SSR-CSR della versione mobile porta a questa immagine:

Registri degli sviluppatori front-end Habr: refactoring e riflessioneCircuito SSR-CSR “a due nodi”. L'API Node JS è sempre pronta per l'I/O asincrono e non è bloccata dalla funzione SSR, poiché quest'ultima si trova in un'istanza separata. La catena di query n. 3 non è necessaria.

Eliminazione delle richieste duplicate

Dopo aver eseguito le manipolazioni, la resa iniziale della pagina non provocava più l'epilessia. Ma l’ulteriore utilizzo di Habr in modalità SPA causava ancora confusione.

Poiché la base del flusso dell'utente sono le transizioni del modulo elenco articoli → articolo → commenti e viceversa, era importante in primo luogo ottimizzare il consumo di risorse di questa catena.

Registri degli sviluppatori front-end Habr: refactoring e riflessioneIl ritorno al feed del post provoca una nuova richiesta di dati

Non c'era bisogno di scavare in profondità. Nello screencast sopra puoi vedere che l'applicazione richiede nuovamente l'elenco degli articoli quando si scorre indietro e durante la richiesta non vediamo gli articoli, il che significa che i dati precedenti scompaiono da qualche parte. Sembra che il componente dell'elenco degli articoli utilizzi uno stato locale e lo perda durante la distruzione. In effetti, l'applicazione utilizzava uno stato globale, ma l'architettura Vuex è stata costruita frontalmente: i moduli sono legati alle pagine, che a loro volta sono legate ai percorsi. Inoltre, tutti i moduli sono "usa e getta": ogni visita successiva alla pagina riscrive l'intero modulo:

ArticlesList: [
  { Article1 },
  ...
],
PageArticle: { ArticleFull1 },

In totale, avevamo un modulo Elenco articoli, che contiene oggetti di tipo Articolo e modulo PaginaArticolo, che era una versione estesa dell'oggetto Articolo, tipo ArticoloCompleto. Nel complesso, questa implementazione non porta nulla di terribile in sé: è molto semplice, si potrebbe anche dire ingenuo, ma estremamente comprensibile. Se ripristini il modulo ogni volta che cambi percorso, puoi persino conviverci. Tuttavia, spostandosi tra i feed di articoli, ad esempio /alimentazione → /all, è sicuro di buttare via tutto ciò che riguarda il feed personale, dato che ne abbiamo solo uno Elenco articoli, in cui è necessario inserire nuovi dati. Anche questo ci porta alla duplicazione delle richieste.

Dopo aver raccolto tutto ciò che sono riuscito a scoprire sull'argomento, ho formulato una nuova struttura statale e l'ho presentata ai miei colleghi. Le discussioni sono state lunghe, ma alla fine gli argomenti a favore hanno prevalso sui dubbi e ho iniziato l’attuazione.

La logica di una soluzione si rivela meglio in due passaggi. Per prima cosa proviamo a disaccoppiare il modulo Vuex dalle pagine e ad associarlo direttamente ai percorsi. Sì, ci saranno un po' più di dati nel negozio, i getter diventeranno un po' più complessi, ma non caricheremo gli articoli due volte. Per la versione mobile, questo è forse l’argomento più forte. Apparirà qualcosa del genere:

ArticlesList: {
  ROUTE_FEED: [ 
    { Article1 },
    ...
  ],
  ROUTE_ALL: [ 
    { Article2 },
    ...
  ],
}

Ma cosa succede se gli elenchi di articoli possono sovrapporsi tra più percorsi e cosa succede se vogliamo riutilizzare i dati degli oggetti? Articolo per eseguire il rendering della pagina del post, trasformandola in ArticoloCompleto? In questo caso, sarebbe più logico utilizzare una struttura del genere:

ArticlesIds: {
  ROUTE_FEED: [ '1', ... ],
  ROUTE_ALL: [ '1', '2', ... ],
},
ArticlesList: {
  '1': { Article1 }, 
  '2': { Article2 },
  ...
}

Elenco articoli qui è solo una sorta di repository di articoli. Tutti gli articoli scaricati durante la sessione utente. Li trattiamo con la massima cura, perché si tratta di traffico che potrebbe essere stato scaricato faticosamente da qualche parte nella metropolitana tra le stazioni, e sicuramente non vogliamo causare nuovamente questo dolore all'utente costringendolo a caricare dati che ha già scaricato. Un oggetto IDArticoli è semplicemente un array di ID (come se fossero "collegamenti") agli oggetti Articolo. Questa struttura consente di evitare la duplicazione dei dati comuni alle rotte e il riutilizzo dell'oggetto Articolo quando si esegue il rendering di una pagina di post unendo in essa dati estesi.

Anche l'output dell'elenco di articoli è diventato più trasparente: il componente iteratore scorre l'array con gli ID degli articoli e disegna il componente teaser dell'articolo, passando l'Id come prop, e il componente figlio, a sua volta, recupera i dati necessari da Elenco articoli. Quando vai alla pagina di pubblicazione, otteniamo la data già esistente da Elenco articoli, facciamo una richiesta per ottenere i dati mancanti e li aggiungiamo semplicemente all'oggetto esistente.

Perché questo approccio è migliore? Come ho scritto sopra, questo approccio è più delicato rispetto ai dati scaricati e consente di riutilizzarli. Ma oltre a ciò, apre la strada ad alcune nuove possibilità che si adattano perfettamente a tale architettura. Ad esempio, eseguire sondaggi e caricare articoli nel feed così come appaiono. Possiamo semplicemente mettere gli ultimi post in un “archivio” Elenco articoli, salva un elenco separato di nuovi ID in IDArticoli e informarne l'utente. Quando clicchiamo sul pulsante “Mostra nuove pubblicazioni”, inseriremo semplicemente i nuovi ID all'inizio dell'array dell'elenco corrente degli articoli e tutto funzionerà quasi magicamente.

Rendere il download più piacevole

La ciliegina sulla torta del refactoring è il concetto di scheletro, che rende il processo di download di contenuti su una Internet lenta un po' meno disgustoso. Non ci sono state discussioni su questo argomento; il percorso dall'idea al prototipo è durato letteralmente due ore. Il design praticamente si è disegnato da solo e abbiamo insegnato ai nostri componenti a eseguire il rendering di blocchi div semplici, appena tremolanti, in attesa dei dati. Soggettivamente, questo approccio al carico riduce effettivamente la quantità di ormoni dello stress nel corpo dell'utente. Lo scheletro si presenta così:

Registri degli sviluppatori front-end Habr: refactoring e riflessione
Habraloading

Riflettendo

Lavoro a Habré da sei mesi e i miei amici continuano a chiedermi: beh, come ti trovi lì? Ok, comodo, sì. Ma c’è qualcosa che rende questo lavoro diverso dagli altri. Ho lavorato in team che erano completamente indifferenti al loro prodotto, non sapevano né capivano chi fossero i loro utenti. Ma qui tutto è diverso. Qui ti senti responsabile di quello che fai. Nel processo di sviluppo di una funzionalità, ne diventi parzialmente il proprietario, partecipi a tutte le riunioni sul prodotto relative alla tua funzionalità, dai suggerimenti e prendi tu stesso le decisioni. Realizzare da solo un prodotto che usi ogni giorno è molto interessante, ma scrivere codice per persone che probabilmente sono più brave di te è semplicemente una sensazione incredibile (niente sarcasmo).

Dopo il rilascio di tutte queste modifiche, abbiamo ricevuto feedback positivi ed è stato molto, molto bello. È stimolante. Grazie! Scrivi di più.

Permettimi di ricordarti che dopo le variabili globali abbiamo deciso di modificare l'architettura e allocare il livello proxy in un'istanza separata. L'architettura "a due nodi" è già stata rilasciata sotto forma di beta testing pubblico. Ora chiunque può passare ad esso e aiutarci a migliorare Habr mobile. È tutto per oggi. Sarò felice di rispondere a tutte le vostre domande nei commenti.

Fonte: habr.com

Aggiungi un commento