AirbnbEng

Follow

11 nov, 2013 – 10 min read

By Spike Brehm

Detta inlägg har korsats på VentureBeat.

På Airbnb har vi lärt oss mycket under de senaste åren när vi byggt rika webbupplevelser. Vi dök in i appvärlden med en enda sida 2011 med vår mobila webbplats, och har sedan dess lanserat bland annat Wish Lists och vår nydesignade söksida. Var och en av dessa är en stor JavaScript-app, vilket innebär att huvuddelen av koden körs i webbläsaren för att stödja en modernare, interaktiv upplevelse.

Detta tillvägagångssätt är vanligt idag, och bibliotek som Backbone.js, Ember.js och Angular.js har gjort det enklare för utvecklare att bygga dessa rika JavaScript-appar. Vi har dock upptäckt att dessa typer av appar har vissa kritiska begränsningar. För att förklara varför, låt oss först ta en snabb omväg genom webbapparnas historia.

JavaScript växer upp

Sedan webbens början har surfupplevelsen fungerat så här: en webbläsare begärde en viss sida (låt oss säga ”http://www.geocities.com/”), vilket ledde till att en server någonstans på Internet genererade en HTML-sida och skickade den tillbaka över kabeln. Detta har fungerat bra eftersom webbläsare inte var särskilt kraftfulla och HTML-sidor representerade dokument som för det mesta var statiska och fristående. JavaScript, som skapades för att göra webbsidor mer dynamiska, möjliggjorde inte mycket mer än bildspel och widgetar för att välja datum.

Efter år av framsteg inom persondatorer har kreativa tekniker drivit webben till dess gränser, och webbläsare har utvecklats för att hänga med. Nu har webben mognat till en applikationsplattform med fullständiga funktioner, och snabba JavaScript-körtider och HTML5-standarder har gjort det möjligt för utvecklare att skapa de rika appar som tidigare bara var möjliga på inhemska plattformar.

Den ensidiga appen

Det dröjde inte länge förrän utvecklare började bygga upp hela applikationer i webbläsaren med hjälp av JavaScript, och dra nytta av dessa nya möjligheter. Appar som Gmail, det klassiska exemplet på en-sidig app, kunde reagera omedelbart på användarens interaktioner och behövde inte längre göra en rundresa till servern bara för att rendera en ny sida.

Bibliotek som Backbone.js, Ember.js och Angular.js kallas ofta för klientsidebibliotek med MVC- (Model-View-Controller) eller MVVM- (Model-View-ViewModel) beteckningar. Den typiska MVC-arkitekturen på klientsidan ser ut ungefär så här:

Den största delen av applikationslogiken (vyer, mallar, controllers, modeller, internationalisering etc.) bor i klienten, och den pratar med ett API för data. Servern kan skrivas i vilket språk som helst, t.ex. Ruby, Python eller Java, och den hanterar huvudsakligen servering av en första HTML-sida. När JavaScript-filerna laddas ner av webbläsaren utvärderas de och appen på klientsidan initieras, hämtar data från API:et och renderar resten av HTML-sidan.

Detta är bra för användaren eftersom appen, när den väl har laddats initialt, kan stödja snabb navigering mellan sidor utan att behöva uppdatera sidan, och om det görs på rätt sätt kan den till och med fungera offline.

Detta är bra för utvecklaren eftersom den idealiserade appen med en enda sida har en tydlig åtskillnad mellan klienten och servern, vilket främjar ett trevligt utvecklingsarbetsflöde och förhindrar behovet av att dela för mycket logik mellan de två, som ofta är skrivna i olika språk.

I praktiken finns det dock några fatala brister med det här tillvägagångssättet som gör att det inte är rätt för många användningsfall.

SEO

En applikation som bara kan köras på klientsidan kan inte servera HTML till crawlers, så den kommer att ha dålig SEO som standard. Webbsökare fungerar genom att göra en förfrågan till en webbserver och tolka resultatet; men om servern returnerar en tom sida är det inte av särskilt stort värde. Det finns lösningar, men inte utan att man måste ta sig igenom några hinder.

Prestanda

På samma sätt, om servern inte renderar en hel HTML-sida utan i stället väntar på att JavaScript på klientsidan ska göra det, kommer användarna att få uppleva några kritiska sekunder med en tom sida eller en laddningsspinnare innan de kan se innehållet på sidan. Det finns gott om studier som visar den drastiska effekt som en långsam webbplats har på användarna och därmed på intäkterna. Amazon hävdar att varje 100 ms minskning av sidans laddningstid ökar intäkterna med 1 %. Twitter ägnade ett år och 40 ingenjörer åt att bygga om sin webbplats för att rendera på servern i stället för på klienten och hävdar att den upplevda laddningstiden förbättrats med fem gånger.

Underhållbarhet

Samtidigt som idealfallet kan leda till en snygg och ren separation av problemställningar, är det oundvikligt att vissa bitar av applikationslogik eller visningslogik hamnar i duplicerad form mellan klient och server, ofta i olika språk. Vanliga exempel är datum- och valutaformatering, validering av formulär och routningslogik. Detta gör underhållet till en mardröm, särskilt för mer komplexa appar.

En del utvecklare, däribland jag själv, känner sig biten av detta tillvägagångssätt – det är ofta först efter att ha investerat tid och ansträngning i att bygga en app med en enda sida som det blir tydligt vilka nackdelarna är.

En hybridmetod

I slutändan vill vi egentligen ha en hybrid av den nya och den gamla metoden: vi vill servera helt formad HTML från servern för prestanda och SEO, men vi vill ha snabbheten och flexibiliteten hos programlogik på klientsidan.

För detta ändamål har vi på Airbnb experimenterat med ”Isomorphic JavaScript”-appar, som är JavaScript-applikationer som kan köras både på klient- och serversidan.

En isomorf app kan se ut så här, som här kallas ”Client-server MVC”:

I den här världen kan en del av din applikations- och visningslogik exekveras på både server och klient. Detta öppnar alla möjliga dörrar – optimering av prestanda, bättre underhållbarhet, SEO-by-default och mer tillståndskrävande webbapplikationer.

Med Node.js, en snabb och stabil serverbaserad JavaScript-körningstid på serversidan, kan vi nu göra den här drömmen till verklighet. Genom att skapa lämpliga abstraktioner kan vi skriva vår programlogik så att den körs på både server och klient – definitionen av isomorfiskt JavaScript.

Isomorfiskt JavaScript i det vilda

Den här idén är inte ny – Nodejitsu skrev en bra beskrivning av isomorfisk JavaScript-arkitektur 2011 – men det har gått långsamt att införa den. Det har redan dykt upp några isomorfa ramverk.

Mojito var det första isomorfa ramverket med öppen källkod som fick någon press. Det är ett avancerat, fullstackigt Node.js-baserat ramverk, men dess beroende av YUI och Yahoo!-specifika egenheter har inte lett till någon större popularitet i JavaScript-communityn sedan de öppnade upp det i april 2012.

Meteor är förmodligen det mest kända isomorfa projektet idag. Meteor är byggt från grunden för att stödja realtidsappar, och teamet bygger ett helt ekosystem kring dess pakethanterare och distributionsverktyg. Liksom Mojito är det ett stort, åsiktsstyrt Node.js-framework, men det har gjort ett mycket bättre jobb med att engagera JavaScript-communityn, och dess mycket efterlängtade 1.0-release är precis runt hörnet. Meteor är ett projekt att hålla koll på – det har ett stjärnteam och har samlat in 11,2 miljoner dollar från Andreessen Horowitz – något som är ovanligt för ett företag som är helt inriktat på att släppa en produkt med öppen källkod.

Asana, appen för uppgiftshantering som grundades av Facebooks medgrundare Dustin Moskovitz, har en intressant isomorfisk historia. Asana, som inte har ont om finansiering med tanke på Moskovitz’ status som världens yngsta miljardär, tillbringade flera år i R&D för att utveckla sitt Luna-ramverk med sluten källkod, ett av de mest avancerade exemplen på isomorfiskt JavaScript som finns. Luna, som ursprungligen byggdes på v8cgi på den tiden innan Node.js existerade, gör det möjligt att köra en komplett kopia av appen på servern för varje enskild användarsession. Den kör en separat serverprocess för varje användare och utför samma JavaScript-applikationskod på servern som körs i klienten, vilket möjliggör en hel klass av avancerade optimeringar, t.ex. robust offline-stöd och snabba uppdateringar i realtid.

Vi lanserade ett eget isomorfiskt bibliotek tidigare i år. Det heter Rendr och gör det möjligt att bygga en Backbone.js + Handlebars.js app med en enda sida som också kan renderas fullt ut på serversidan. Rendr är en produkt av vår erfarenhet av att bygga om Airbnbs mobila webbapp för att drastiskt förbättra sidladdningstiderna, vilket är särskilt viktigt för användare på mobila anslutningar med hög latenstid. Rendr strävar efter att vara ett bibliotek snarare än ett ramverk, så det löser färre problem åt dig jämfört med Mojito eller Meteor, men det är lätt att modifiera och utöka.

Abstraktion, abstraktion, abstraktion

Det faktum att dessa projekt tenderar att vara stora, fullstackiga webbramverk talar för svårigheten i problemet. Klienten och servern är mycket olika miljöer, och därför måste vi skapa en uppsättning abstraktioner som frikopplar vår programlogik från de underliggande implementeringarna, så att vi kan exponera ett enda API för programutvecklaren.

Routing

Vi vill ha en enda uppsättning rutter som mappar URI-mönster till rutthanterare. Våra rutthanterare måste kunna få tillgång till HTTP-huvuden, cookies och URI-information samt ange omdirigeringar utan att direkt få tillgång till window.location (webbläsare) eller req och res (Node.js).

Hämtning och lagring av data

Vi vill beskriva de resurser som behövs för att rendera en viss sida eller komponent oberoende av hämtningsmekanismen. Resursbeskrivningen kan vara en enkel URI som pekar på en JSON-slutpunkt, eller för större tillämpningar kan det vara användbart att kapsla in resurser i modeller och samlingar och specificera en modellklass och primärnyckel, som vid någon tidpunkt översätts till en URI.

Vytrendering

Oavsett om vi väljer att direkt manipulera DOM, hålla oss till strängbaserad HTML-templatering eller välja ett bibliotek för gränssnittskomponenter med en DOM-abstraktion, måste vi kunna generera markup på ett isomorft sätt. Vi bör kunna rendera vilken vy som helst på antingen servern eller klienten, beroende på behoven i vår applikation.

Byggande och paketering

Det visar sig att skriva isomorfisk applikationskod bara är halva slaget. Verktyg som Grunt och Browserify är viktiga delar av arbetsflödet för att faktiskt få igång appen. Det kan finnas ett antal byggsteg: kompilering av mallar, inklusive beroenden på klientsidan, tillämpning av transformationer, minifiering osv. Det enkla fallet är att kombinera all applikationskod, vyer och mallar i ett enda paket, men för större appar kan detta resultera i hundratals kilobyte att ladda ner. Ett mer avancerat tillvägagångssätt är att skapa dynamiska buntar och införa ”lazy-loading” av tillgångar, men detta blir snabbt komplicerat. Statiska analysverktyg som Esprima kan göra det möjligt för ambitiösa utvecklare att försöka sig på avancerad optimering och metaprogrammering för att minska boilerplate-kod.

Sammansättning av små moduler tillsammans

Att vara först ut på marknaden med ett isomorfiskt ramverk innebär att du måste lösa alla dessa problem på en gång. Men detta leder till stora, otympliga ramverk som är svåra att ta till sig och integrera i en redan befintlig app. När fler utvecklare tar itu med det här problemet kommer vi att få se en explosion av små, återanvändbara moduler som kan integreras tillsammans för att bygga isomorfa appar.

Det visar sig att de flesta JavaScript-moduler redan kan användas isomorfiskt med liten eller ingen ändring. Till exempel kan populära bibliotek som Underscore, Backbone.js, Handlebars.js, Moment och till och med jQuery användas på servern.

För att demonstrera detta har jag skapat en provexemplarisk app kallad isomorphic-tutorial som du kan kolla in på GitHub. Genom att kombinera några moduler, som var och en kan användas isomorft, är det lätt att skapa en enkel isomorf app på bara några hundra rader kod. Den använder Director för server- och webbläsarbaserad routing, Superagent för HTTP-förfrågningar och Handlebars.js för templating, allt byggt ovanpå en grundläggande Express.js-app. När en app växer i komplexitet måste man naturligtvis införa fler abstraktionslager, men min förhoppning är att när fler utvecklare experimenterar med detta kommer det att dyka upp nya bibliotek och standarder.

The View From Here

När fler organisationer blir bekväma med att köra Node.js i produktion är det oundvikligt att fler och fler webbapplikationer kommer att börja dela kod mellan sin klient- och serverkod. Det är viktigt att komma ihåg att isomorfiskt JavaScript är ett spektrum – det kan börja med att bara dela mallar, utvecklas till att vara en hel applikations visningslager, hela vägen till majoriteten av appens affärslogik. Exakt vad och hur JavaScript-kod delas mellan miljöer beror helt och hållet på den applikation som byggs och dess unika uppsättning begränsningar.

Nicholas C. Zakas har en fin beskrivning av hur han föreställer sig att appar kommer att börja dra sitt gränssnittslager ner till servern från klienten, vilket möjliggör optimeringar av prestanda och underhållbarhet. En app behöver inte slita ut sin backend och ersätta den med Node.js för att använda isomorfiskt JavaScript, vilket i princip innebär att barnet kastas ut med badvattnet. Genom att skapa förnuftiga API:er och REST-resurser kan den traditionella baksidan istället leva parallellt med Node.js-skiktet.

På Airbnb har vi redan börjat omarbeta vår byggprocess på klientsidan för att använda Node.js-baserade verktyg som Grunt och Browserify. Vår huvudsakliga Rails-app kanske aldrig helt och hållet ersätts av en Node.js-app, men genom att anamma dessa verktyg blir det allt enklare att dela vissa bitar av JavaScript och mallar mellan miljöer.

Du hörde det här först – inom några år kommer det att vara sällsynt att se en avancerad webbapplikation som inte kör någon JavaScript på servern.

Lär dig mer

Om du är intresserad av den här idén kan du komma och kolla in workshopen Isomorphic JavaScript som jag kommer att hålla på DevBeat tisdagen den 12 november i San Francisco, eller på General Assembly torsdagen den 21 november. Vi kommer att hacka tillsammans på Node.js isomorphic-tutorial-appen som jag har skapat för att visa hur lätt det verkligen är att komma igång med att skriva isomorfa appar.

Håll också koll på utvecklingen av Airbnbs webbappar genom att följa mig på @spikebrehm och Airbnb Engineering-teamet på @AirbnbEng.

Lämna ett svar

Din e-postadress kommer inte publiceras.