AirbnbEng

Follow

11. nov, 2013 – 10 min read

Af Spike Brehm

Dette indlæg er blevet krydspostet på VentureBeat.

I Airbnb har vi lært meget i løbet af de sidste par år, mens vi har bygget rige weboplevelser. Vi kastede os ud i app-verdenen med en enkelt side i 2011 med vores mobilwebsite, og siden da har vi bl.a. lanceret Wish Lists og vores nyligt omdesignede søgeside. Hver af disse er en stor JavaScript-app, hvilket betyder, at hovedparten af koden kører i browseren for at understøtte en mere moderne, interaktiv oplevelse.

Denne tilgang er almindelig i dag, og biblioteker som Backbone.js, Ember.js og Angular.js har gjort det lettere for udviklere at bygge disse rige JavaScript-apps. Vi har dog fundet ud af, at disse typer apps har nogle kritiske begrænsninger. For at forklare hvorfor, skal vi først tage en hurtig omvej gennem webapps historie.

JavaScript vokser op

Siden starten af internettet har browsing-oplevelsen fungeret på følgende måde: En webbrowser anmodede om en bestemt side (lad os sige “http://www.geocities.com/”), hvilket fik en server et sted på internettet til at generere en HTML-side og sende den tilbage over kablet. Dette har fungeret godt, fordi browsere ikke var særlig effektive, og HTML-sider repræsenterede dokumenter, der for det meste var statiske og selvstændige. JavaScript, der blev skabt for at gøre websiderne mere dynamiske, gav ikke mulighed for meget mere end billeddiasshow og widgets til at vælge dato.

Efter mange års fremskridt inden for personlig databehandling har kreative teknologer presset internettet til dets grænser, og webbrowsere har udviklet sig for at følge med. Nu er internettet modnet til en applikationsplatform med alle funktioner, og hurtige JavaScript-køretider og HTML5-standarder har gjort det muligt for udviklere at skabe de rige apps, som før kun var mulige på native platforme.

The Single-Page App

Det varede ikke længe, før udviklere begyndte at opbygge hele applikationer i browseren ved hjælp af JavaScript og drage fordel af disse nye muligheder. Apps som Gmail, det klassiske eksempel på single-page-appen, kunne reagere øjeblikkeligt på brugerinteraktioner og behøvede ikke længere at foretage en rundrejse til serveren blot for at rendere en ny side.

Biblioteker som Backbone.js, Ember.js og Angular.js omtales ofte som klientside MVC- (Model-View-Controller) eller MVVM- (Model-View-ViewModel) biblioteker. Den typiske klientside MVC-arkitektur ser nogenlunde sådan ud:

Størstedelen af applikationslogikken (visninger, skabeloner, controllere, modeller, internationalisering osv.) bor i klienten, og den taler til et API for at få data. Serveren kan være skrevet i et hvilket som helst sprog, f.eks. Ruby, Python eller Java, og den håndterer for det meste at servere en indledende HTML-side med barebones. Når JavaScript-filerne er hentet af browseren, evalueres de, og appen på klientsiden initialiseres, idet den henter data fra API’et og gengiver resten af HTML-siden.

Dette er godt for brugeren, for når appen først er indlæst, kan den understøtte hurtig navigation mellem siderne uden at opdatere siden, og hvis det gøres rigtigt, kan den endda fungere offline.

Det er godt for udvikleren, fordi den idealiserede single-page-app har en klar adskillelse af bekymringer mellem klienten og serveren, hvilket fremmer en god udviklingsarbejdsgang og forhindrer behovet for at dele for meget logik mellem de to, som ofte er skrevet i forskellige sprog.

I praksis er der imidlertid et par fatale fejl ved denne tilgang, som forhindrer den i at være den rigtige til mange anvendelsestilfælde.

SEO

En applikation, der kun kan køre på klientsiden, kan ikke servere HTML til crawlere, så den vil som standard have dårlig SEO. Webcrawlere fungerer ved at sende en forespørgsel til en webserver og fortolke resultatet; men hvis serveren returnerer en tom side, er det ikke af stor værdi. Der findes løsninger, men ikke uden at hoppe igennem nogle huller.

Performance

På samme måde vil brugerne, hvis serveren ikke renderer en hel HTML-side, men i stedet venter på, at JavaScript på klientens side gør det, opleve et par kritiske sekunder med en tom side eller en indlæsningsspinner, før de kan se indholdet på siden. Der er masser af undersøgelser, der viser den drastiske virkning, som et langsomt websted har på brugerne og dermed på indtægterne. Amazon hævder, at hver 100 ms reduktion i sideindlæsningstiden øger omsætningen med 1 %. Twitter brugte et år og 40 ingeniører på at ombygge deres websted til at blive gengivet på serveren i stedet for på klienten og hævder, at den opfattede indlæsningstid blev forbedret 5 gange.

Vedligeholdbarhed

Selv om det ideelle tilfælde kan føre til en fin, ren adskillelse af problemer, ender det uundgåeligt med, at nogle dele af applikationslogikken eller visningslogikken ender med at blive dubleret mellem klient og server, ofte i forskellige sprog. Almindelige eksempler er dato- og valutaformatering, valideringer af formularer og routing-logik. Dette gør vedligeholdelsen til et mareridt, især for mere komplekse apps.

Nogle udviklere, mig selv inklusive, føler sig bidt af denne tilgang – det er ofte først efter at have investeret tid og kræfter i at bygge en single-page-app, at det bliver klart, hvad ulemperne er.

En hybrid tilgang

I sidste ende ønsker vi egentlig en hybrid af den nye og den gamle tilgang: Vi ønsker at servere fuldt formet HTML fra serveren af hensyn til ydeevne og SEO, men vi ønsker hastigheden og fleksibiliteten ved applikationslogikken på klient-siden.

Med henblik herpå har vi hos Airbnb eksperimenteret med “Isomorphic JavaScript”-apps, som er JavaScript-applikationer, der kan køre både på klientsiden og server-siden.

En isomorphic app kan se således ud, her døbt “Client-server MVC”:

I denne verden kan noget af din applikations- og visningslogik udføres på både serveren og klienten. Det åbner alle mulige døre – optimering af ydeevnen, bedre vedligeholdelsesmuligheder, SEO-by-default og mere tilstandsprægede webapps.

Med Node.js, en hurtig og stabil server-side JavaScript-køringstid, kan vi nu gøre denne drøm til virkelighed. Ved at skabe de relevante abstraktioner kan vi skrive vores applikationslogik således, at den kører på både serveren og klienten – definitionen af isomorfisk JavaScript.

Isomorfisk JavaScript i naturen

Denne idé er ikke ny – Nodejitsu skrev en god beskrivelse af isomorfisk JavaScript-arkitektur i 2011 – men den har været langsom at indføre. Der er allerede opstået et par isomorfe frameworks.

Mojito var det første open source isomorfe framework, der fik noget presse. Det er en avanceret, fuldstack Node.js-baseret ramme, men dens afhængighed af YUI og Yahoo!-specifikke særheder har ikke ført til megen popularitet i JavaScript-fællesskabet, siden de åbnede den i april 2012.

Meteor er nok det mest kendte isomorfe projekt i dag. Meteor er bygget fra bunden til at understøtte realtidsapps, og holdet er ved at opbygge et helt økosystem omkring dets pakkehåndtering og implementeringsværktøjer. Ligesom Mojito er det et stort, meningsfyldt Node.js framework, men det har gjort et langt bedre stykke arbejde med at engagere JavaScript-fællesskabet, og dets meget ventede 1.0-udgivelse er lige rundt om hjørnet. Meteor er et projekt, som man skal holde øje med – det har et all-star team, og det har rejst 11,2 millioner dollars fra Andreessen Horowitz – uhørt for en virksomhed, der udelukkende fokuserer på at frigive et open source-produkt.

Asana, opgavehåndteringsappen, der er grundlagt af Facebooks medstifter Dustin Moskovitz, har en interessant isomorfihistorie. Da Moskovitz’ status som verdens yngste milliardær ikke gør ham ondt med hensyn til finansiering, brugte Asana flere år i R&D på at udvikle deres lukkede Luna-ramme, et af de mest avancerede eksempler på isomorfisk JavaScript, der findes. Luna, der oprindeligt blev bygget på v8cgi i tiden før Node.js eksisterede, gør det muligt at køre en komplet kopi af appen på serveren for hver eneste brugersession. Den kører en separat serverproces for hver bruger og udfører den samme JavaScript-applikationskode på serveren, som kører i klienten, hvilket muliggør en hel klasse af avancerede optimeringer, f.eks. robust offline-understøttelse og hurtige opdateringer i realtid.

Vi lancerede selv et isomorft bibliotek tidligere i år. Det hedder Rendr og giver dig mulighed for at bygge en Backbone.js + Handlebars.js-app med en enkelt side, som også kan gengives fuldt ud på serversiden. Rendr er et produkt af vores erfaring med at ombygge Airbnbs mobile webapp for at forbedre pageload-tiderne drastisk, hvilket især er vigtigt for brugere på mobilforbindelser med høj latenstid. Rendr stræber efter at være et bibliotek snarere end et framework, så det løser færre af problemerne for dig sammenlignet med Mojito eller Meteor, men det er let at ændre og udvide.

Abstraktion, abstraktion, abstraktion

Det faktum, at disse projekter har tendens til at være store, fuldstackede webframeworks, taler for problemets sværhedsgrad. Klienten og serveren er meget forskellige miljøer, og derfor skal vi skabe et sæt abstraktioner, der afkobler vores applikationslogik fra de underliggende implementeringer, så vi kan udstille et enkelt API for applikationsudvikleren.

Routing

Vi ønsker et enkelt sæt af ruter, der kortlægger URI-mønstre til rutehåndterer. Vores rutehåndterer skal kunne få adgang til HTTP-headere, cookies og URI-oplysninger og angive omdirigeringer uden direkte adgang til window.location (browser) eller req og res (Node.js).

Hentning og persistering af data

Vi ønsker at beskrive de ressourcer, der er nødvendige for at gengive en bestemt side eller komponent uafhængigt af hentningsmekanismen. Ressourcebeskrivelsen kunne være en simpel URI, der peger på et JSON-slutpunkt, eller for større programmer kan det være nyttigt at indkapsle ressourcer i modeller og samlinger og angive en modelklasse og primær nøgle, som på et tidspunkt bliver oversat til en URI.

View rendering

Hvorvidt vi vælger at manipulere DOM direkte, holde os til strengbaseret HTML-templating eller vælge et UI-komponentbibliotek med en DOM-abstraktion, skal vi være i stand til at generere markup isomorft. Vi skal kunne gengive enhver visning på enten serveren eller klienten, afhængigt af vores applikations behov.

Opbygning og pakning

Det viser sig, at det kun er halvdelen af slaget at skrive isomorfe applikationskode. Værktøjer som Grunt og Browserify er vigtige dele af arbejdsgangen for rent faktisk at få appen op at køre. Der kan være en række build-trin: kompilering af skabeloner, herunder klientsideafhængigheder, anvendelse af transformationer, minificering osv. Det enkle tilfælde er at kombinere al applikationskode, visninger og skabeloner i et enkelt bundle, men for større apps kan dette resultere i hundredvis af kilobyte, der skal downloades. En mere avanceret tilgang er at oprette dynamiske bundles og indføre lazy-loading af aktiver, men det bliver hurtigt kompliceret. Statiske analyseværktøjer som Esprima kan give ambitiøse udviklere mulighed for at forsøge sig med avanceret optimering og metaprogrammering for at reducere boilerplate-kode.

Sammensætning af små moduler sammen

At være først på markedet med et isomorft framework betyder, at man skal løse alle disse problemer på én gang. Men det fører til store, uhåndterlige frameworks, som er svære at tage til sig og integrere i en allerede eksisterende app. Efterhånden som flere udviklere tager fat på dette problem, vil vi se en eksplosion af små, genanvendelige moduler, der kan integreres sammen for at bygge isomorfe apps.

Det viser sig, at de fleste JavaScript-moduler allerede kan bruges isomorft med få eller ingen ændringer. For eksempel kan populære biblioteker som Underscore, Backbone.js, Handlebars.js, Moment og endda jQuery bruges på serveren.

For at demonstrere dette punkt har jeg oprettet en prøveapp kaldet isomorphic-tutorial, som du kan tjekke ud på GitHub. Ved at kombinere et par moduler, der hver især kan bruges isomorft, er det nemt at oprette en simpel isomorft app på blot et par hundrede linjer kode. Den bruger Director til server- og browserbaseret routing, Superagent til HTTP-forespørgsler og Handlebars.js til templating, alt sammen bygget på toppen af en grundlæggende Express.js-app. Selvfølgelig må man indføre flere abstraktionslag, efterhånden som en app vokser i kompleksitet, men mit håb er, at efterhånden som flere udviklere eksperimenterer med dette, vil der dukke nye biblioteker og standarder op.

The View From Here

Når flere organisationer bliver trygge ved at køre Node.js i produktion, er det uundgåeligt, at flere og flere webapps vil begynde at dele kode mellem deres klient- og serverkode. Det er vigtigt at huske, at isomorfisk JavaScript er et spektrum – det kan starte med blot at dele skabeloner, udvikle sig til at være en hel applikations visningslag og hele vejen til størstedelen af appens forretningslogik. Præcis hvad og hvordan JavaScript-kode deles mellem miljøer afhænger helt af den applikation, der bygges, og dens unikke sæt af begrænsninger.

Nicholas C. Zakas har en fin beskrivelse af, hvordan han forestiller sig, at apps vil begynde at trække deres UI-lag ned til serveren fra klienten, hvilket muliggør optimeringer af ydeevne og vedligeholdbarhed. En app behøver ikke at rive sin backend ud og erstatte den med Node.js for at bruge isomorphic JavaScript, hvilket i bund og grund betyder, at man smider barnet ud med badevandet. Ved at skabe fornuftige API’er og RESTful-ressourcer kan den traditionelle backend i stedet leve side om side med Node.js-laget.

På Airbnb er vi allerede begyndt at ombygge vores byggeproces på klientsiden til at bruge Node.js-baserede værktøjer som Grunt og Browserify. Vores primære Rails-app bliver måske aldrig helt erstattet af en Node.js-app, men ved at tage disse værktøjer til sig bliver det stadig nemmere at dele visse dele af JavaScript og skabeloner mellem miljøer.

Du hørte det her først – inden for et par år vil det være sjældent at se en avanceret web-app, der ikke kører noget JavaScript på serveren.

Lær mere

Hvis denne idé begejstrer dig, så kom og se den Isomorphic JavaScript-workshop, som jeg underviser på DevBeat tirsdag den 12. november i San Francisco eller på General Assembly torsdag den 21. november. Vi vil hacke sammen på den isomorfe Node.js-tutorial-app, som jeg har oprettet for at demonstrere, hvor nemt det virkelig er at komme i gang med at skrive isomorfe apps.

Hold også øje med udviklingen af Airbnb-webapps ved at følge mig på @spikebrehm og Airbnb Engineering-teamet på @AirbnbEng.

Skriv et svar

Din e-mailadresse vil ikke blive publiceret.