Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectare

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectare

Am fost întotdeauna interesat de modul în care Habr este structurat din interior, cum este structurat fluxul de lucru, cum sunt structurate comunicațiile, ce standarde sunt utilizate și cum este scris codul în general aici. Din fericire, am avut o astfel de oportunitate, pentru că de curând am intrat în echipa habra. Folosind exemplul unei mici refactorizări a versiunii mobile, voi încerca să răspund la întrebarea: cum este să lucrezi aici în față. În program: Node, Vue, Vuex și SSR cu sos din note despre experiența personală în Habr.

Primul lucru pe care trebuie să-l știți despre echipa de dezvoltare este că suntem puțini. Nu este suficient - sunt trei fronturi, doi spate și liderul tehnic al tuturor Habr - Baxley. Există, desigur, și un tester, un designer, trei Vadim, o mătură minune, un specialist în marketing și alți Bumburum. Dar există doar șase contribuitori direcți la sursele lui Habr. Acest lucru este destul de rar - un proiect cu un public de mai multe milioane de dolari, care din exterior arată ca o întreprindere uriașă, în realitate arată mai mult ca un startup confortabil, cu cea mai plată structură organizațională posibilă.

La fel ca multe alte companii IT, Habr profesează idei Agile, practici CI și atât. Dar, conform sentimentelor mele, Habr ca produs se dezvoltă mai mult în valuri decât continuu. Așadar, pentru mai multe sprinturi la rând, codificăm ceva cu sârguință, proiectăm și reproiectăm, spargem ceva și îl reparăm, rezolvăm bilete și creăm altele noi, călcăm pe o greblă și ne împușcăm în picioare, pentru a elibera în sfârșit caracteristica în producție. Și apoi vine o anumită pauză, o perioadă de reamenajare, timpul să faci ceea ce este în cadranul „important-nu urgent”.

Tocmai acest sprint „în afara sezonului” va fi discutat mai jos. De data aceasta a inclus o refactorizare a versiunii mobile a lui Habr. În general, compania are mari speranțe în aceasta, iar în viitor ar trebui să înlocuiască întreaga grădină zoologică a încarnărilor lui Habr și să devină o soluție universală multiplatformă. Într-o zi va exista aspect adaptiv, PWA, modul offline, personalizarea utilizatorului și multe alte lucruri interesante.

Să stabilim sarcina

Odată, la un stand-up obișnuit, unul din front a vorbit despre probleme în arhitectura componentei de comentarii a versiunii mobile. Având în vedere acest lucru, am organizat o microîntâlnire în format psihoterapie de grup. Toți au spus pe rând unde doare, au consemnat totul pe hârtie, au simpatizat, au înțeles, doar că nimeni nu a aplaudat. Rezultatul a fost o listă de 20 de probleme, care a arătat clar că Habr mobil avea încă un drum lung și spinos către succes.

Am fost preocupat în primul rând de eficiența utilizării resurselor și de ceea ce se numește o interfață fluidă. În fiecare zi, pe ruta acasă-lucru-acasă, îmi vedeam vechiul telefon încercând cu disperare să afișeze 20 de titluri în flux. Arăta cam așa:

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectareInterfață Mobile Habr înainte de refactorizare

Ce se petrece aici? Pe scurt, serverul a oferit pagina HTML tuturor în același mod, indiferent dacă utilizatorul a fost conectat sau nu. Apoi clientul JS este încărcat și solicită din nou datele necesare, dar ajustate pentru autorizare. Adică am făcut de fapt aceeași treabă de două ori. Interfața a pâlpâit, iar utilizatorul a descărcat o sută de kilobytes în plus. În detaliu, totul părea și mai înfiorător.

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectareSchema veche SSR-CSR. Autorizarea este posibilă numai în etapele C3 și C4, când Node JS nu este ocupat cu generarea HTML și poate trimite solicitări către API.

Arhitectura noastră din acea vreme a fost descrisă foarte precis de unul dintre utilizatorii Habr:

Versiunea mobilă este o prostie. Îi spun așa cum este. O combinație teribilă de SSR și CSR.

Trebuia să recunoaștem, oricât de trist ar fi fost.

Am evaluat opțiunile, am creat un bilet în Jira cu o descriere la nivelul „e rău acum, fă-o bine” și am descompus sarcina în linii mari:

  • reutilizarea datelor,
  • minimizați numărul de retrageri,
  • eliminarea cererilor duplicate,
  • face procesul de încărcare mai evident.

Să reutilizam datele

În teorie, randarea pe server este concepută pentru a rezolva două probleme: să nu sufere de limitările motorului de căutare în ceea ce privește Indexarea SPA și îmbunătățiți metrica FMP (inevitabil se agravează TTI). Într-un scenariu clasic că în sfârșit formulat la Airbnb în 2013 anul (încă pe Backbone.js), SSR este aceeași aplicație JS izomorfă care rulează în mediul Node. Serverul trimite pur și simplu aspectul generat ca răspuns la cerere. Apoi, rehidratarea are loc pe partea clientului și apoi totul funcționează fără reîncărcări ale paginii. Pentru Habr, ca și pentru multe alte resurse cu conținut text, redarea serverului este un element critic în construirea de relații prietenoase cu motoarele de căutare.

În ciuda faptului că au trecut mai bine de șase ani de la apariția tehnologiei și, în acest timp, multă apă a zburat cu adevărat sub pod în lumea front-end, pentru mulți dezvoltatori această idee este încă învăluită în secret. Nu am stat deoparte și am lansat o aplicație Vue cu suport SSR în producție, lipsind un mic detaliu: nu am trimis starea inițială clientului.

De ce? Nu există un răspuns exact la această întrebare. Fie nu au vrut să mărească dimensiunea răspunsului de la server, fie din cauza unei grămadă de alte probleme arhitecturale, fie pur și simplu nu a decolat. Într-un fel sau altul, eliminarea stării și reutilizarea a tot ceea ce a făcut serverul pare destul de adecvată și utilă. Sarcina este de fapt banală - starea este pur și simplu injectată în contextul de execuție, iar Vue îl adaugă automat la aspectul generat ca variabilă globală: window.__INITIAL_STATE__.

Una dintre problemele care a apărut este incapacitatea de a converti structurile ciclice în JSON (referință circulară); a fost rezolvată prin simpla înlocuire a unor astfel de structuri cu omologii lor plate.

În plus, atunci când aveți de-a face cu conținutul UGC, ar trebui să vă amintiți că datele ar trebui convertite în entități HTML pentru a nu rupe HTML. În aceste scopuri folosim he.

Minimizarea redesenărilor

După cum puteți vedea din diagrama de mai sus, în cazul nostru, o instanță Node JS îndeplinește două funcții: SSR și „proxy” în API, unde are loc autorizarea utilizatorului. Această circumstanță face imposibilă autorizarea în timp ce codul JS rulează pe server, deoarece nodul este cu un singur thread, iar funcția SSR este sincronă. Adică, serverul pur și simplu nu poate trimite cereri către el însuși în timp ce stiva de apeluri este ocupată cu ceva. S-a dovedit că am actualizat starea, dar interfața nu a încetat să treacă, deoarece datele de pe client trebuiau actualizate ținând cont de sesiunea utilizatorului. Trebuia să învățăm aplicația noastră să pună datele corecte în starea inițială, ținând cont de autentificarea utilizatorului.

Au existat doar două soluții la problemă:

  • atașați date de autorizare la cererile pe mai multe servere;
  • împărțiți straturile Node JS în două instanțe separate.

Prima soluție a necesitat utilizarea variabilelor globale pe server, iar a doua a prelungit termenul de finalizare a sarcinii cu cel puțin o lună.

Cum să faci o alegere? Habr se deplasează adesea pe calea celei mai mici rezistențe. În mod informal, există o dorință generală de a reduce la minimum ciclul de la idee la prototip. Modelul de atitudine față de produs amintește oarecum de postulatele booking.com, singura diferență fiind că Habr ia mult mai în serios feedback-ul utilizatorilor și are încredere în tine, ca dezvoltator, pentru a lua astfel de decizii.

Urmând această logică și propria mea dorință de a rezolva rapid problema, am ales variabile globale. Și, așa cum se întâmplă adesea, mai devreme sau mai târziu trebuie să plătești pentru ele. Am plătit aproape imediat: am lucrat în weekend, am lămurit consecințele, am scris post-mortem și a început să împartă serverul în două părți. Eroarea a fost foarte stupidă, iar bug-ul care o implică nu a fost ușor de reprodus. Și da, este păcat pentru asta, dar într-un fel sau altul, poticnindu-se și gemând, PoC-ul meu cu variabile globale a intrat totuși în producție și funcționează cu succes în așteptarea trecerii la o nouă arhitectură „cu două noduri”. Acesta a fost un pas important, deoarece în mod oficial obiectivul a fost atins - SSR a învățat să livreze o pagină complet gata de utilizare, iar UI a devenit mult mai calmă.

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectareInterfață Mobile Habr după prima etapă de refactorizare

În cele din urmă, arhitectura SSR-CSR a versiunii mobile duce la această imagine:

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectareCircuit SSR-CSR „cu două noduri”. API-ul Node JS este întotdeauna gata pentru I/O asincron și nu este blocat de funcția SSR, deoarece aceasta din urmă este situată într-o instanță separată. Lanțul de interogări nr. 3 nu este necesar.

Eliminarea cererilor duplicate

După ce au fost efectuate manipulările, redarea inițială a paginii nu a mai provocat epilepsie. Dar utilizarea ulterioară a lui Habr în modul SPA a provocat încă confuzie.

Deoarece baza fluxului de utilizatori sunt tranzițiile formularului lista de articole → articol → comentarii și invers, a fost important să se optimizeze consumul de resurse al acestui lanț în primul rând.

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectareRevenirea la fluxul de postare provoacă o nouă solicitare de date

Nu era nevoie să sapi adânc. În screencast-ul de mai sus puteți vedea că aplicația re-solicită lista de articole atunci când treceți înapoi, iar în timpul solicitării nu vedem articolele, ceea ce înseamnă că datele anterioare dispar undeva. Se pare că componenta listă de articole folosește un stat local și o pierde la distrugere. De fapt, aplicația a folosit o stare globală, dar arhitectura Vuex a fost construită frontal: modulele sunt legate de pagini, care la rândul lor sunt legate de rute. În plus, toate modulele sunt „de unică folosință” - fiecare vizită ulterioară a paginii a rescris întregul modul:

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

În total, am avut un modul Lista articolelor, care conține obiecte de tip Articol și modul PaginaArticol, care a fost o versiune extinsă a obiectului Articol, cam ArticolulComplet. În general, această implementare nu are nimic groaznic în sine - este foarte simplă, s-ar putea spune chiar naivă, dar extrem de de înțeles. Dacă resetați modulul de fiecare dată când schimbați ruta, atunci puteți chiar să trăiți cu el. Cu toate acestea, deplasarea între fluxurile de articole, de exemplu /feed → /toate, este garantat să arunce tot ce are legătură cu feedul personal, deoarece avem doar unul Lista articolelor, în care trebuie să introduceți date noi. Acest lucru ne duce din nou la duplicarea cererilor.

După ce am adunat tot ce am putut să dezgrou pe această temă, am formulat o nouă structură de stat și am prezentat-o ​​colegilor mei. Discuțiile au fost lungi, dar până la urmă argumentele în favoare au depășit îndoielile și am început implementarea.

Logica unei soluții este cel mai bine dezvăluită în doi pași. Mai întâi încercăm să decuplăm modulul Vuex de pagini și să le legăm direct la rute. Da, vor fi ceva mai multe date în magazin, getters vor deveni puțin mai complexi, dar nu vom încărca articole de două ori. Pentru versiunea mobilă, acesta este poate cel mai puternic argument. Va arata cam asa:

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

Dar ce se întâmplă dacă listele de articole se pot suprapune între mai multe rute și dacă vrem să reutilizam datele obiectului Articol pentru a reda pagina de postare, transformând-o în ArticolulComplet? În acest caz, ar fi mai logic să folosiți o astfel de structură:

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

Lista articolelor aici este doar un fel de depozit de articole. Toate articolele care au fost descărcate în timpul sesiunii utilizator. Îi tratăm cu cea mai mare grijă, deoarece acesta este trafic care poate fi descărcat din cauza durerii undeva în metroul între stații și cu siguranță nu vrem să provocăm din nou această durere utilizatorului forțându-l să încarce date pe care le are deja descărcat. Un obiect ArticoleIds este pur și simplu o serie de ID-uri (ca și cum ar fi „linkuri”) către obiecte Articol. Această structură vă permite să evitați duplicarea datelor comune rutelor și reutilizarea obiectului Articol atunci când redați o pagină de postare prin îmbinarea datelor extinse în ea.

Ieșirea listei de articole a devenit, de asemenea, mai transparentă: componenta iterator iterează prin matrice cu ID-uri de articol și desenează componenta teaser de articol, trecând ID-ul ca prop, iar componenta copil, la rândul său, preia datele necesare din Lista articolelor. Când accesați pagina de publicare, primim data deja existentă de la Lista articolelor, facem o cerere pentru a obține datele lipsă și pur și simplu le adăugăm la obiectul existent.

De ce este mai bună această abordare? După cum am scris mai sus, această abordare este mai blândă în ceea ce privește datele descărcate și vă permite să le reutilizați. Dar, pe lângă aceasta, deschide calea unor noi posibilități care se potrivesc perfect într-o astfel de arhitectură. De exemplu, interogarea și încărcarea articolelor în feed așa cum apar. Putem pur și simplu să punem cele mai recente postări într-un „depozit” Lista articolelor, salvați o listă separată de ID-uri noi în ArticoleIds și anunță utilizatorul despre asta. Când facem clic pe butonul „Afișează publicații noi”, pur și simplu vom introduce ID-uri noi la începutul matricei listei curente de articole și totul va funcționa aproape magic.

Faceți descărcarea mai plăcută

Cireașa de pe tortul de refactorizare este conceptul de schelete, ceea ce face procesul de descărcare a conținutului pe un internet lent puțin mai puțin dezgustător. Nu au existat discuții pe această temă; drumul de la idee la prototip a durat literalmente două ore. Designul practic s-a desenat singur și ne-am învățat componentele să redeze blocuri div simple, abia pâlpâitoare în timp ce așteptăm date. Subiectiv, această abordare a încărcării reduce de fapt cantitatea de hormoni de stres din corpul utilizatorului. Scheletul arată astfel:

Jurnalele pentru dezvoltatori front-end Habr: refactorizare și reflectare
Habraloading

Reflectând

Lucrez în Habré de șase luni și prietenii mei încă mă întreabă: ei bine, cum îți place acolo? Bine, confortabil - da. Dar există ceva care face această lucrare diferită de altele. Am lucrat în echipe care erau complet indiferente față de produsul lor, nu știau sau înțeleg cine erau utilizatorii lor. Dar aici totul este diferit. Aici te simți responsabil pentru ceea ce faci. În procesul de dezvoltare a unei funcții, deveniți parțial proprietarul acesteia, participați la toate întâlnirile despre produse legate de funcționalitatea dvs., faceți sugestii și luați singur decizii. Să faci singur un produs pe care îl folosești în fiecare zi este foarte tare, dar să scrii cod pentru oameni care probabil se pricep mai bine decât tine este doar un sentiment incredibil (fără sarcasm).

După lansarea tuturor acestor modificări, am primit feedback pozitiv și a fost foarte, foarte frumos. Este inspirat. Mulțumesc! Scrie mai mult.

Permiteți-mi să vă reamintesc că după variabilele globale am decis să schimbăm arhitectura și să alocăm stratul proxy într-o instanță separată. Arhitectura „cu două noduri” a ajuns deja la lansare sub formă de testare publică beta. Acum oricine poate trece la el și ne poate ajuta să îmbunătățim Habr mobil. Asta e tot pentru azi. Voi fi bucuros să vă răspund la toate întrebările în comentarii.

Sursa: www.habr.com

Adauga un comentariu