Door Spike Brehm
Dit bericht is cross-posted op VentureBeat.
Bij Airbnb hebben we de afgelopen jaren veel geleerd tijdens het bouwen van rijke webervaringen. In 2011 doken we in de wereld van de single-page apps met onze mobiele website. Sindsdien hebben we onder andere Wish Lists en onze nieuw ontworpen zoekpagina gelanceerd. Elk van deze is een grote JavaScript-app, wat betekent dat het grootste deel van de code in de browser wordt uitgevoerd om een modernere, interactieve ervaring te ondersteunen.
Deze aanpak is vandaag de dag gemeengoed, en bibliotheken zoals Backbone.js, Ember.js en Angular.js hebben het voor ontwikkelaars gemakkelijker gemaakt om deze rijke JavaScript-apps te bouwen. We hebben echter gemerkt dat dit soort apps een aantal kritieke beperkingen hebben. Om uit te leggen waarom, laten we eerst een korte omweg maken door de geschiedenis van webapps.
JavaScript Grows Up
Sinds de dageraad van het web heeft de browse-ervaring als volgt gewerkt: een webbrowser vroeg een bepaalde pagina aan (zeg, “http://www.geocities.com/”), waardoor een server ergens op internet een HTML-pagina genereerde en deze terugstuurde over de draad. Dit werkte goed omdat browsers niet erg krachtig waren en HTML-pagina’s documenten vertegenwoordigden die meestal statisch en op zichzelf staand waren. JavaScript, dat was ontwikkeld om webpagina’s dynamischer te maken, maakte niet veel meer mogelijk dan diavoorstellingen met afbeeldingen en datumkiezer-widgets.
Na jaren van vooruitgang in personal computing hebben creatieve technologen de grenzen van het web opgezocht, en webbrowsers hebben zich ontwikkeld om bij te blijven. Nu is het web uitgegroeid tot een volledig functioneel applicatieplatform, en snelle JavaScript-runtimes en HTML5-standaarden hebben ontwikkelaars in staat gesteld rijke apps te maken die voorheen alleen mogelijk waren op native platforms.
The Single-Page App
Het duurde niet lang voordat ontwikkelaars hele applicaties in de browser begonnen op te bouwen met behulp van JavaScript, waarbij ze gebruikmaakten van deze nieuwe mogelijkheden. Apps als Gmail, het klassieke voorbeeld van de single-page app, konden onmiddellijk reageren op interacties van de gebruiker en hoefden niet langer een round-trip naar de server te maken om een nieuwe pagina te renderen.
Bibliotheken als Backbone.js, Ember.js en Angular.js worden vaak aangeduid als client-side MVC (Model-View-Controller) of MVVM (Model-View-ViewModel) bibliotheken. De typische client-side MVC-architectuur ziet er ongeveer zo uit:
Het grootste deel van de applicatielogica (views, templates, controllers, modellen, internationalisatie, enz.) bevindt zich in de client, en deze praat met een API voor gegevens. De server kan in om het even welke taal worden geschreven, zoals Ruby, Python of Java, en zorgt er vooral voor dat de HTML-pagina initieel op een kale manier wordt aangeboden. Zodra de JavaScript-bestanden worden gedownload door de browser, worden ze geëvalueerd en de client-side app wordt geïnitialiseerd, het ophalen van gegevens uit de API en het renderen van de rest van de HTML-pagina.
Dit is geweldig voor de gebruiker, omdat zodra de app is initieel geladen, kan het ondersteunen snelle navigatie tussen pagina’s zonder verversing van de pagina, en als het goed gedaan, kan zelfs offline werken.
Dit is geweldig voor de ontwikkelaar, omdat de geïdealiseerde single-page app heeft een duidelijke scheiding van belangen tussen de client en de server, het bevorderen van een mooie ontwikkeling workflow en het voorkomen van de noodzaak om te veel logica te delen tussen de twee, die vaak zijn geschreven in verschillende talen.
In de praktijk zijn er echter een paar fatale gebreken aan deze aanpak die voorkomen dat het goed is voor veel use cases.
SEO
Een applicatie die alleen in de client-side kan draaien kan geen HTML serveren aan crawlers, dus het zal standaard een slechte SEO hebben. Webcrawlers doen een verzoek aan een webserver en interpreteren het resultaat; maar als de server een lege pagina terugstuurt, is dat niet veel waard. Er zijn workarounds, maar niet zonder te springen door een aantal hoepels.
Performance
Op dezelfde manier, als de server niet rendert een volledige pagina van HTML, maar in plaats daarvan wacht op client-side JavaScript om dat te doen, zullen gebruikers ervaren een paar kritische seconden van lege pagina of het laden spinner voordat het zien van de inhoud op de pagina. Er zijn genoeg studies die het drastische effect van een trage site op gebruikers, en dus inkomsten, aantonen. Amazon beweert dat elke 100ms vermindering in laadtijd van de pagina de inkomsten met 1% verhoogt. Twitter spendeerde een jaar en 40 engineers aan het herbouwen van hun site om deze op de server te renderen in plaats van op de client, en claimde een 5x verbetering in de waargenomen laadtijd.
Onderhoudbaarheid
Hoewel het ideale geval kan leiden tot een mooie, schone scheiding van belangen, is het onvermijdelijk dat sommige stukjes applicatielogica of view logica dubbel worden uitgevoerd tussen client en server, vaak in verschillende talen. Veel voorkomende voorbeelden zijn datum en valuta formattering, formulier validaties, en routing logica. Dit maakt het onderhoud een nachtmerrie, vooral voor meer complexe apps.
Sommige ontwikkelaars, waaronder ikzelf, voelen zich gebeten door deze aanpak – het is vaak pas na de tijd en moeite te hebben geïnvesteerd om een single-page app te bouwen dat het duidelijk wordt wat de nadelen zijn.
Een hybride aanpak
Op het einde van de dag, willen we echt een hybride van de nieuwe en oude benaderingen: we willen volledig-gevormde HTML serveren vanaf de server voor de prestaties en SEO, maar we willen de snelheid en flexibiliteit van client-side applicatie logica.
Daartoe hebben we bij Airbnb geëxperimenteerd met “Isomorphic JavaScript” apps, dat zijn JavaScript-toepassingen die zowel op de client-side als op de server-side kunnen draaien.
Een isomorfe app kan er als volgt uitzien, hier “Client-server MVC” genoemd:
In deze wereld kan een deel van uw applicatie en view logica worden uitgevoerd op zowel de server als de client. Dit opent allerlei deuren – prestatie-optimalisaties, betere onderhoudbaarheid, SEO-by-default, en meer stateful web apps.
Met Node.js, een snelle, stabiele server-side JavaScript runtime, kunnen we deze droom nu werkelijkheid laten worden. Door de juiste abstracties te creëren, kunnen we onze applicatielogica zo schrijven dat deze zowel op de server als op de client draait – de definitie van isomorf JavaScript.
Isomorf JavaScript in het wild
Dit idee is niet nieuw – Nodejitsu schreef in 2011 een geweldige beschrijving van isomorfe JavaScript-architectuur – maar het heeft zich maar traag doorgezet. Er zijn al een paar isomorfe frameworks opgestaan.
Mojito was het eerste open-source isomorfe framework dat enige pers kreeg. Het is een geavanceerd, full-stack Node.js-gebaseerd framework, maar zijn afhankelijkheid van YUI en Yahoo!-specifieke eigenaardigheden hebben niet geleid tot veel populariteit in de JavaScript-gemeenschap sinds ze het in april 2012 open-sourcen.
Meteor is waarschijnlijk het meest bekende isomorfe project van vandaag. Meteor is van de grond af gebouwd om real-time apps te ondersteunen, en het team bouwt een heel ecosysteem rond zijn package manager en deployment tools. Net als Mojito is het een groot, opiniërend Node.js framework, maar het is er veel beter in geslaagd om de JavaScript-gemeenschap erbij te betrekken, en de langverwachte 1.0 release staat voor de deur. Meteor is een project om in de gaten te houden – het heeft een all-star team, en het heeft $ 11,2 miljoen opgehaald bij Andreessen Horowitz – ongehoord voor een bedrijf dat zich volledig richt op het uitbrengen van een open-source product.
Asana, de taakbeheer app opgericht door Facebook mede-oprichter Dustin Moskovitz, heeft een interessant isomorphic verhaal. Niet hurting voor financiering, gezien Moskovitz’ status als jongste miljardair ter wereld, Asana bracht jaren door in R&D ontwikkeling van hun closed-source Luna framework, een van de meest geavanceerde voorbeelden van isomorphic JavaScript rond. Luna, oorspronkelijk gebouwd op v8cgi in de dagen voordat Node.js bestond, staat een volledige kopie van de app toe om op de server te draaien voor elke gebruikerssessie. Het draait een apart server proces voor elke gebruiker, het uitvoeren van dezelfde JavaScript applicatie code op de server die wordt uitgevoerd in de client, waardoor een hele klasse van geavanceerde optimalisaties, zoals robuuste offline ondersteuning en snelle real-time updates.
We lanceerden een isomorfe bibliotheek van onze eigen eerder dit jaar. Genaamd Rendr, het stelt u in staat om een Backbone.js + Handlebars.js single-page app die ook volledig kan worden gerenderd op de server-side te bouwen. Rendr is een product van onze ervaring met het herbouwen van de Airbnb mobiele web app om de pageload tijden drastisch te verbeteren, wat vooral belangrijk is voor gebruikers op mobiele verbindingen met een hoge latency. Rendr streeft ernaar een bibliotheek te zijn in plaats van een framework, dus het lost minder problemen voor je op in vergelijking met Mojito of Meteor, maar het is gemakkelijk aan te passen en uit te breiden.
Abstraction, Abstraction, Abstraction
Dat deze projecten de neiging hebben om grote, full-stack webframeworks te zijn, spreekt voor de moeilijkheid van het probleem. De client en server zijn zeer verschillende omgevingen, en dus moeten we een set abstracties maken die onze applicatie logica loskoppelen van de onderliggende implementaties, zodat we een enkele API kunnen blootstellen aan de applicatie ontwikkelaar.
Routing
We willen een enkele set van routes die URI patronen in kaart brengen aan route handlers. Onze route handlers moeten toegang hebben tot HTTP headers, cookies en URI informatie, en redirects kunnen specificeren zonder direct toegang te hebben tot window.location (browser) of req en res (Node.js).
Fetching and persisting data
We willen de bronnen beschrijven die nodig zijn om een bepaalde pagina of component te renderen, onafhankelijk van het fetching mechanisme. De resource descriptor kan een eenvoudige URI zijn die wijst naar een JSON endpoint, of voor grotere applicaties kan het nuttig zijn om resources in te kapselen in modellen en collecties en een model class en primary key te specificeren, die op een gegeven moment vertaald zou worden naar een URI.
View rendering
Of we er nu voor kiezen om direct de DOM te manipuleren, vasthouden aan string-based HTML templating, of kiezen voor een UI component library met een DOM abstractie, we moeten in staat zijn om markup isomorphically te genereren. We moeten in staat zijn om elke view te renderen op de server of de client, afhankelijk van de behoeften van onze applicatie.
Bouwen en verpakken
Het blijkt dat het schrijven van isomorfe applicatiecode slechts de helft van het werk is. Hulpmiddelen zoals Grunt en Browserify zijn essentiële onderdelen van de workflow om de app daadwerkelijk aan de praat te krijgen. Er kunnen een aantal build stappen zijn: compileren van templates, inclusief client-side afhankelijkheden, toepassen van transforms, minification, etc. Het eenvoudige geval is om alle applicatiecode, views en templates te combineren in een enkele bundel, maar voor grotere apps kan dit resulteren in honderden kilobytes om te downloaden. Een meer geavanceerde aanpak is om dynamische bundels te maken en asset lazy-loading te introduceren, maar dit wordt al snel gecompliceerd. Met statische analyse tools zoals Esprima kunnen ambitieuze ontwikkelaars geavanceerde optimalisatie en metaprogrammering proberen om boilerplate code te verminderen.
Samenstellen van kleine modules
De eerste zijn op de markt met een isomorf framework betekent dat je al deze problemen in een keer moet oplossen. Maar dit leidt tot grote, logge frameworks die moeilijk zijn te adopteren en te integreren in een reeds bestaande app. Naarmate meer ontwikkelaars dit probleem aanpakken, zullen we een explosie zien van kleine, herbruikbare modules die samen kunnen worden geïntegreerd om isomorfe apps te bouwen.
Het blijkt dat de meeste JavaScript-modules al isomorf kunnen worden gebruikt met weinig tot geen aanpassingen. Bijvoorbeeld, populaire bibliotheken zoals Underscore, Backbone.js, Handlebars.js, Moment, en zelfs jQuery kunnen worden gebruikt op de server.
Om dit punt te demonstreren, heb ik een voorbeeld app gemaakt genaamd isomorphic-tutorial die je kunt bekijken op GitHub. Door het combineren van een paar modules, elk die kan worden gebruikt isomorphically, het is gemakkelijk om een eenvoudige isomorphic app in slechts een paar honderd regels code te maken. Het gebruikt Director voor server- en browser-gebaseerde routing, Superagent voor HTTP requests, en Handlebars.js voor templating, allemaal gebouwd bovenop een basis Express.js app. Natuurlijk, als een app groeit in complexiteit, moet men meer lagen van abstractie introduceren, maar mijn hoop is dat als meer ontwikkelaars hiermee experimenteren, er nieuwe bibliotheken en standaarden zullen ontstaan.
The View From Here
Als meer organisaties zich comfortabel voelen met het draaien van Node.js in productie, is het onvermijdelijk dat meer en meer web apps code zullen beginnen te delen tussen hun client en server code. Het is belangrijk om te onthouden dat isomorphic JavaScript een spectrum is – het kan beginnen met alleen het delen van sjablonen, oplopen tot de view layer van een hele applicatie, helemaal tot het grootste deel van de business logica van de app. Wat en hoe JavaScript code precies wordt gedeeld tussen omgevingen hangt volledig af van de applicatie die wordt gebouwd en zijn unieke set van beperkingen.
Nicholas C. Zakas heeft een mooie beschrijving van hoe hij zich voorstelt dat apps hun UI-laag van de client naar de server beginnen te trekken, wat optimalisaties op het gebied van prestaties en onderhoudbaarheid mogelijk maakt. Een app hoeft niet zijn backend eruit te rukken en te vervangen door Node.js om isomorfe JavaScript te gebruiken, in wezen het kind met het badwater weggooien. In plaats daarvan, door het creëren van verstandige API’s en RESTful bronnen, kan de traditionele backend leven naast de Node.js laag.
Bij Airbnb, zijn we al begonnen met het omvormen van onze client-side build proces om Node.js gebaseerde tools te gebruiken, zoals Grunt en Browserify. Onze belangrijkste Rails app zal misschien nooit volledig worden vervangen door een Node.js app, maar door het omarmen van deze tools wordt het steeds makkelijker om bepaalde stukjes JavaScript en templates te delen tussen omgevingen.
Je hoorde het hier eerst – binnen een paar jaar, zal het zeldzaam zijn om een geavanceerde web app te zien die niet een aantal JavaScript op de server draait.
Learn More
Als je enthousiast bent over dit idee, kom dan naar de Isomorphic JavaScript workshop die ik geef bij DevBeat op dinsdag 12 november in San Francisco, of bij General Assembly op donderdag 21 november. We zullen samen hacken op het voorbeeld Node.js isomorphic-tutorial app die ik heb gemaakt om te laten zien hoe eenvoudig het is om aan de slag te gaan met het schrijven van isomorphic apps.
Volg mij ook op de voet over de evolutie van de Airbnb web apps door mij te volgen op @spikebrehm en het Airbnb Engineering team op @AirbnbEng.