Habr front-end utvecklarloggar: refaktorering och reflektion

Habr front-end utvecklarloggar: refaktorering och reflektion

Jag har alltid varit intresserad av hur Habr är uppbyggt från insidan, hur arbetsflödet är uppbyggt, hur kommunikationen är uppbyggd, vilka standarder som används och hur kod generellt skrivs här. Lyckligtvis fick jag en sådan möjlighet, eftersom jag nyligen blev en del av habra-teamet. Med hjälp av exemplet med en liten omstrukturering av mobilversionen ska jag försöka svara på frågan: hur är det att arbeta här längst fram. I programmet: Node, Vue, Vuex och SSR med sås från anteckningar om personlig erfarenhet i Habr.

Det första du behöver veta om utvecklingsteamet är att vi är få. Inte nog - det här är tre fronter, två backar och den tekniska ledningen för alla Habr - Baxley. Det finns naturligtvis också en testare, en designer, tre Vadim, en mirakelkvast, en marknadsföringsspecialist och andra Bumburums. Men det finns bara sex direkta bidragsgivare till Habrs källor. Detta är ganska sällsynt - ett projekt med en mångmiljonpublik, som från utsidan ser ut som ett jätteföretag, i verkligheten ser mer ut som en mysig startup med en så platt organisationsstruktur som möjligt.

Precis som många andra IT-företag bekänner Habr sig till agila idéer, CI-praxis och det är allt. Men enligt mina känslor utvecklas Habr som produkt mer i vågor än kontinuerligt. Så, under flera spurter i rad, kodar vi flitigt något, designar och designar om, bryter något och fixar det, löser biljetter och skapar nya, kliver på en rake och skjuter oss själva i fötterna, för att äntligen släppa funktionen i produktion. Och så kommer det en viss lugn, en period av ombyggnad, tid att göra det som står i "viktigt-inte brådskande" kvadranten.

Det är just denna "off-season" sprint som kommer att diskuteras nedan. Den här gången inkluderade det en omstrukturering av mobilversionen av Habr. I allmänhet har företaget stora förhoppningar på det, och i framtiden bör det ersätta hela djurparken i Habrs inkarnationer och bli en universell plattformsoberoende lösning. Någon gång kommer det att finnas adaptiv layout, PWA, offlineläge, användaranpassning och många andra intressanta saker.

Låt oss sätta uppgiften

En gång, vid en vanlig stand-up, talade en av fronten om problem i arkitekturen för kommentarskomponenten i mobilversionen. Med detta i åtanke anordnade vi ett mikromöte i form av grupppsykoterapi. Alla turades om att säga var det gjorde ont, de antecknade allt på papper, de sympatiserade, de förstod, förutom att ingen klappade. Resultatet blev en lista med 20 problem, vilket gjorde det tydligt att mobila Habr fortfarande hade en lång och besvärlig väg till framgång.

Jag var främst oroad över effektiviteten i resursanvändningen och det som kallas ett smidigt gränssnitt. Varje dag, på vägen hem-jobb-hem, såg jag min gamla telefon desperat försöka visa 20 rubriker i flödet. Det såg ut ungefär så här:

Habr front-end utvecklarloggar: refaktorering och reflektionMobilt Habr-gränssnitt före refaktorisering

Vad händer här? Kort sagt serverade servern HTML-sidan till alla på samma sätt, oavsett om användaren var inloggad eller inte. Därefter laddas klientens JS och begär nödvändiga data igen, men justeras för auktorisering. Det vill säga, vi gjorde faktiskt samma jobb två gånger. Gränssnittet flimrade och användaren laddade ner drygt hundra extra kilobyte. I detalj såg allt ännu mer läskigt ut.

Habr front-end utvecklarloggar: refaktorering och reflektionGammalt SSR-CSR-schema. Auktorisering är endast möjlig i steg C3 och C4, när Node JS inte är upptagen med att generera HTML och kan proxyförfrågningar till API:t.

Vår dåtidens arkitektur beskrevs mycket exakt av en av Habr-användarna:

Mobilversionen är skit. Jag säger det som det är. En fruktansvärd kombination av SSR och CSR.

Vi fick erkänna det, hur sorgligt det än var.

Jag utvärderade alternativen, skapade en biljett i Jira med en beskrivning på nivån "det är dåligt nu, gör det rätt" och dekomponerade uppgiften i stora drag:

  • återanvända data,
  • minimera antalet omritningar,
  • eliminera dubbletter av förfrågningar,
  • göra laddningsprocessen mer uppenbar.

Låt oss återanvända datan

I teorin är rendering på serversidan utformad för att lösa två problem: att inte drabbas av sökmotorbegränsningar i form av SPA-indexering och förbättra måttet FMP (oundvikligen förvärras TTI). I ett klassiskt scenario som äntligen formulerades på Airbnb 2013 år (fortfarande på Backbone.js), är SSR samma isomorfa JS-applikation som körs i Node-miljön. Servern skickar helt enkelt den genererade layouten som ett svar på begäran. Sedan sker rehydrering på klientsidan, och sedan fungerar allt utan att sidan laddas om. För Habr, liksom för många andra resurser med textinnehåll, är serverrendering ett avgörande element för att bygga vänskapliga relationer med sökmotorer.

Trots det faktum att det har gått mer än sex år sedan teknikens tillkomst, och under denna tid har mycket vatten verkligen flugit under bron i front-end-världen, för många utvecklare är denna idé fortfarande höljd i hemlighet. Vi ställde oss inte åt sidan och rullade ut en Vue-applikation med SSR-stöd till produktionen, utan en liten detalj: vi skickade inte initialtillståndet till kunden.

Varför? Det finns inget exakt svar på denna fråga. Antingen ville de inte öka storleken på svaret från servern eller på grund av en massa andra arkitektoniska problem, eller så tog det helt enkelt inte fart. På ett eller annat sätt verkar det ganska lämpligt och användbart att kasta ut staten och återanvända allt som servern gjorde. Uppgiften är faktiskt trivial - staten injiceras helt enkelt in i exekveringskontexten, och Vue lägger automatiskt till den i den genererade layouten som en global variabel: window.__INITIAL_STATE__.

Ett av problemen som har uppstått är oförmågan att omvandla cykliska strukturer till JSON (cirkulär referens); löstes genom att helt enkelt ersätta sådana strukturer med sina platta motsvarigheter.

Dessutom, när du hanterar UGC-innehåll, bör du komma ihåg att data bör konverteras till HTML-entiteter för att inte bryta HTML-koden. För dessa ändamål använder vi he.

Minimera omritningar

Som du kan se från diagrammet ovan utför en Node JS-instans i vårt fall två funktioner: SSR och "proxy" i API:t, där användarauktorisering sker. Denna omständighet gör det omöjligt att auktorisera medan JS-koden körs på servern, eftersom noden är entrådad och SSR-funktionen är synkron. Det vill säga, servern kan helt enkelt inte skicka förfrågningar till sig själv medan callstacken är upptagen med något. Det visade sig att vi uppdaterade tillståndet, men gränssnittet slutade inte rycka, eftersom data på klienten måste uppdateras med hänsyn till användarsessionen. Vi behövde lära vår applikation att sätta rätt data i det ursprungliga tillståndet, med hänsyn till användarens inloggning.

Det fanns bara två lösningar på problemet:

  • bifoga behörighetsdata till förfrågningar över servrar;
  • dela upp Node JS-lager i två separata instanser.

Den första lösningen krävde användning av globala variabler på servern, och den andra förlängde tidsfristen för att slutföra uppgiften med minst en månad.

Hur gör man ett val? Habr rör sig ofta längs minsta motståndets väg. Informellt finns det en allmän önskan att minska cykeln från idé till prototyp till ett minimum. Modellen för attityd till produkten påminner en del om postulaten från booking.com, med den enda skillnaden att Habr tar användarfeedback mycket mer seriöst och litar på att du som utvecklare ska fatta sådana beslut.

Efter denna logik och min egen önskan att snabbt lösa problemet, valde jag globala variabler. Och, som ofta händer, måste du betala för dem förr eller senare. Vi betalade nästan omedelbart: vi jobbade på helgen, reda ut konsekvenserna, skrev obduktion och började dela upp servern i två delar. Felet var väldigt dumt, och felet som involverade det var inte lätt att återskapa. Och ja, det är synd för detta, men på ett eller annat sätt, snubblande och stönande, gick min PoC med globala variabler ändå i produktion och fungerar ganska framgångsrikt i väntan på flytten till en ny "två-nods"-arkitektur. Detta var ett viktigt steg, för formellt uppnåddes målet – SSR lärde sig att leverera en helt färdig-att-använda sida, och användargränssnittet blev mycket lugnare.

Habr front-end utvecklarloggar: refaktorering och reflektionMobilt Habr-gränssnitt efter det första steget av refactoring

I slutändan leder SSR-CSR-arkitekturen för den mobila versionen till denna bild:

Habr front-end utvecklarloggar: refaktorering och reflektion"Två-nods" SSR-CSR-krets. Node JS API är alltid redo för asynkron I/O och blockeras inte av SSR-funktionen, eftersom den senare finns i en separat instans. Frågekedja #3 behövs inte.

Eliminera dubbletter av förfrågningar

Efter att manipulationerna utförts, provocerade den initiala renderingen av sidan inte längre epilepsi. Men den fortsatta användningen av Habr i SPA-läge orsakade fortfarande förvirring.

Eftersom grunden för användarflödet är övergångar av formuläret lista över artiklar → artikel → kommentarer och vice versa var det viktigt att optimera resursförbrukningen i denna kedja i första hand.

Habr front-end utvecklarloggar: refaktorering och reflektionAtt återgå till postflödet provocerar fram en ny dataförfrågan

Det fanns ingen anledning att gräva djupt. I screencasten ovan kan du se att applikationen begär listan på nytt när du sveper tillbaka, och under förfrågan ser vi inte artiklarna, vilket innebär att tidigare data försvinner någonstans. Det ser ut som att artikellistans komponent använder en lokal stat och förlorar den vid förstöring. Faktum är att applikationen använde ett globalt tillstånd, men Vuex-arkitekturen byggdes direkt: moduler är bundna till sidor, som i sin tur är bundna till rutter. Dessutom är alla moduler "engångsbara" - varje efterföljande besök på sidan skrev om hela modulen:

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

Totalt hade vi en modul Artikellista, som innehåller objekt av typen Artikeln och modul Sidartikel, som var en utökad version av objektet Artikeln, ungefär ArtikelFull. I stort sett bär den här implementeringen inte på något hemskt i sig - det är väldigt enkelt, man kan till och med säga naivt, men extremt förståeligt. Om du återställer modulen varje gång du ändrar rutten kan du till och med leva med den. Men att flytta mellan artikelflöden, till exempel /flöde → /alla, kommer garanterat att slänga allt som har med det personliga flödet att göra, eftersom vi bara har ett Artikellista, där du behöver lägga in ny data. Detta leder återigen till dubblering av förfrågningar.

Efter att ha samlat allt som jag kunde gräva fram om ämnet, formulerade jag en ny statlig struktur och presenterade den för mina kollegor. Diskussionerna var långa, men till slut uppvägde argumenten för tvivelna och jag började implementera.

Logiken i en lösning avslöjas bäst i två steg. Först försöker vi koppla bort Vuex-modulen från sidor och binda direkt till rutter. Ja, det kommer att finnas lite mer data i butiken, getters blir lite mer komplexa, men vi kommer inte att ladda artiklar två gånger. För mobilversionen är detta kanske det starkaste argumentet. Det kommer att se ut ungefär så här:

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

Men vad händer om artikellistor kan överlappa flera rutter och vad händer om vi vill återanvända objektdata Artikeln för att rendera inläggssidan, förvandla den till ArtikelFull? I det här fallet skulle det vara mer logiskt att använda en sådan struktur:

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

Artikellista här är det bara ett slags arkiv med artiklar. Alla artiklar som laddades ner under användarsessionen. Vi behandlar dem med största försiktighet, eftersom det här är trafik som kan ha laddats ner genom smärta någonstans i tunnelbanan mellan stationerna, och vi vill definitivt inte orsaka denna smärta för användaren igen genom att tvinga honom att ladda data som han redan har nedladdade. Ett objekt ArticlesIds är helt enkelt en uppsättning ID:n (som om "länkar") till objekt Artikeln. Denna struktur låter dig undvika att duplicera data som är gemensam för rutter och återanvända objektet Artikeln när du renderar en inläggssida genom att slå samman utökad data till den.

Utdata från listan med artiklar har också blivit mer transparent: iteratorkomponenten itererar genom arrayen med artikel-ID:n och ritar artikelteaser-komponenten, skickar ID:t som en rekvisita, och den underordnade komponenten hämtar i sin tur nödvändig data från Artikellista. När du går till publiceringssidan får vi det redan befintliga datumet från Artikellista, gör vi en begäran om att erhålla den saknade informationen och lägger helt enkelt till den i det befintliga objektet.

Varför är detta tillvägagångssätt bättre? Som jag skrev ovan är detta tillvägagångssätt mer skonsamt med avseende på nedladdade data och låter dig återanvända det. Men förutom detta öppnar det vägen för några nya möjligheter som passar perfekt in i en sådan arkitektur. Till exempel polling och inläsning av artiklar i flödet när de visas. Vi kan helt enkelt lägga de senaste inläggen i ett "lager" Artikellista, spara en separat lista med nya ID i ArticlesIds och meddela användaren om det. När vi klickar på knappen "Visa nya publikationer" kommer vi helt enkelt att infoga nya ID i början av den aktuella listan med artiklar och allt kommer att fungera nästan magiskt.

Gör nedladdning roligare

Pricken över i:et är begreppet skelett, vilket gör processen att ladda ner innehåll på ett långsamt internet lite mindre äckligt. Det fanns inga diskussioner om denna fråga, vägen från idé till prototyp tog bokstavligen två timmar. Designen ritade praktiskt taget sig själv, och vi lärde våra komponenter att rendera enkla, knappt flimrande div-block i väntan på data. Subjektivt sett minskar detta tillvägagångssätt för laddning faktiskt mängden stresshormoner i användarens kropp. Skelettet ser ut så här:

Habr front-end utvecklarloggar: refaktorering och reflektion
Habraloading

Reflekterar

Jag har jobbat i Habré i sex månader och mina vänner frågar fortfarande: ja, hur trivs du där? Okej, bekvämt - ja. Men det är något som gör det här arbetet annorlunda än andras. Jag arbetade i team som var helt likgiltiga för deras produkt, inte visste eller förstod vilka deras användare var. Men här är allt annorlunda. Här känner du dig ansvarig för det du gör. I processen att utveckla en funktion blir du delvis dess ägare, tar del av alla produktmöten som rör din funktionalitet, ger förslag och fattar själv beslut. Att göra en produkt som du själv använder varje dag är väldigt coolt, men att skriva kod för människor som förmodligen är bättre på det än du är bara en otrolig känsla (ingen sarkasm).

Efter släppet av alla dessa förändringar fick vi positiv feedback, och det var väldigt, väldigt trevligt. Det är inspirerande. Tack! Skriv mer.

Låt mig påminna dig om att vi efter globala variabler bestämde oss för att ändra arkitekturen och allokera proxylagret i en separat instans. "Två-nods"-arkitekturen har redan nått release i form av offentlig beta-testning. Nu kan vem som helst byta till det och hjälpa oss att göra mobila Habr bättre. Det är allt för idag. Jag svarar gärna på alla dina frågor i kommentarerna.

Källa: will.com

Lägg en kommentar