AirbnbEng

Follow

Nov 11, 2013 – 10 min read

By Spike Brehm

Dieser Beitrag wurde quer auf VentureBeat gepostet.

Bei Airbnb haben wir in den letzten Jahren viel gelernt, als wir reichhaltige Web-Erlebnisse entwickelten. Wir haben 2011 mit unserer mobilen Website den Sprung in die Welt der Single-Page-Apps gewagt und seitdem unter anderem Wunschlisten und unsere neu gestaltete Suchseite eingeführt. Bei jeder dieser Anwendungen handelt es sich um eine umfangreiche JavaScript-Anwendung, d. h. der Großteil des Codes wird im Browser ausgeführt, um ein moderneres, interaktives Erlebnis zu ermöglichen.

Dieser Ansatz ist heute weit verbreitet, und Bibliotheken wie Backbone.js, Ember.js und Angular.js haben es Entwicklern erleichtert, diese umfangreichen JavaScript-Anwendungen zu erstellen. Wir haben jedoch festgestellt, dass diese Arten von Anwendungen einige kritische Einschränkungen aufweisen. Um zu erklären, warum das so ist, machen wir zunächst einen kurzen Abstecher in die Geschichte der Webanwendungen.

JavaScript wächst heran

Seit den Anfängen des Webs hat das Surfen wie folgt funktioniert: Ein Webbrowser forderte eine bestimmte Seite an (sagen wir „http://www.geocities.com/“), woraufhin ein Server irgendwo im Internet eine HTML-Seite generierte und sie über die Leitung zurückschickte. Dies hat gut funktioniert, weil die Browser nicht sehr leistungsfähig waren und HTML-Seiten Dokumente darstellten, die meist statisch und in sich geschlossen waren. JavaScript, das entwickelt wurde, um dynamischere Webseiten zu ermöglichen, ermöglichte nicht viel mehr als Bild-Diashows und Datumsauswahl-Widgets.

Nach Jahren des Fortschritts im Bereich der persönlichen Datenverarbeitung haben kreative Technologen das Web an seine Grenzen gebracht, und die Webbrowser haben sich weiterentwickelt, um mitzuhalten. Jetzt ist das Web zu einer vollwertigen Anwendungsplattform herangereift, und schnelle JavaScript-Laufzeiten und HTML5-Standards haben es Entwicklern ermöglicht, reichhaltige Anwendungen zu erstellen, die zuvor nur auf nativen Plattformen möglich waren.

Die Single-Page-App

Es dauerte nicht lange, bis Entwickler damit begannen, ganze Anwendungen im Browser mit JavaScript zu erstellen und diese neuen Möglichkeiten zu nutzen. Apps wie Gmail, das klassische Beispiel für eine Single-Page-App, konnten sofort auf Benutzerinteraktionen reagieren und mussten nicht mehr zum Server zurückkehren, nur um eine neue Seite zu rendern.

Bibliotheken wie Backbone.js, Ember.js und Angular.js werden oft als clientseitige MVC- (Model-View-Controller) oder MVVM- (Model-View-ViewModel) Bibliotheken bezeichnet. Die typische clientseitige MVC-Architektur sieht in etwa so aus:

Der Großteil der Anwendungslogik (Views, Templates, Controller, Modelle, Internationalisierung usw.) befindet sich im Client, der mit einer API für Daten kommuniziert. Der Server kann in einer beliebigen Sprache wie Ruby, Python oder Java geschrieben sein und kümmert sich hauptsächlich um die Bereitstellung einer einfachen HTML-Seite. Sobald die JavaScript-Dateien vom Browser heruntergeladen sind, werden sie ausgewertet und die clientseitige Anwendung wird initialisiert, wobei Daten von der API abgerufen und der Rest der HTML-Seite gerendert wird.

Für den Benutzer ist dies großartig, da die Anwendung, sobald sie einmal geladen ist, eine schnelle Navigation zwischen den Seiten unterstützen kann, ohne dass die Seite aktualisiert werden muss, und wenn sie richtig gemacht wird, sogar offline funktionieren kann.

Für den Entwickler ist dies großartig, weil die idealisierte einseitige Anwendung eine klare Trennung zwischen dem Client und dem Server aufweist, was einen angenehmen Entwicklungsworkflow fördert und verhindert, dass zu viel Logik zwischen den beiden geteilt werden muss, die oft in verschiedenen Sprachen geschrieben sind.

In der Praxis weist dieser Ansatz jedoch einige fatale Mängel auf, die verhindern, dass er für viele Anwendungsfälle geeignet ist.

SEO

Eine Anwendung, die nur auf der Client-Seite ausgeführt werden kann, kann kein HTML an Crawler ausliefern, so dass sie standardmäßig eine schlechte SEO aufweist. Web-Crawler funktionieren, indem sie eine Anfrage an einen Webserver stellen und das Ergebnis interpretieren; aber wenn der Server eine leere Seite zurückgibt, ist das nicht viel wert. Es gibt Umgehungsmöglichkeiten, aber nicht ohne einige Hürden zu überwinden.

Leistung

Wenn der Server keine vollständige HTML-Seite rendert, sondern stattdessen darauf wartet, dass clientseitiges JavaScript dies tut, erleben die Benutzer einige kritische Sekunden mit einer leeren Seite oder einem Ladevorgang, bevor sie den Inhalt der Seite sehen. Es gibt zahlreiche Studien, die zeigen, welche drastischen Auswirkungen eine langsame Website auf die Nutzer und damit auf die Einnahmen hat. Amazon behauptet, dass jede Verkürzung der Seitenladezeit um 100 ms den Umsatz um 1 % steigert. Twitter hat ein Jahr und 40 Ingenieure damit verbracht, seine Website so umzubauen, dass sie auf dem Server statt auf dem Client gerendert wird, und behauptet, dass sich die wahrgenommene Ladezeit um das Fünffache verbessert hat.

Wartbarkeit

Während der Idealfall zu einer schönen, sauberen Trennung von Belangen führen kann, werden unweigerlich einige Teile der Anwendungslogik oder der Ansichtslogik zwischen Client und Server dupliziert, oft in verschiedenen Sprachen. Häufige Beispiele sind Datums- und Währungsformatierungen, Formularvalidierungen und Routing-Logik. Dies macht die Wartung zu einem Alptraum, insbesondere bei komplexeren Anwendungen.

Einige Entwickler, mich selbst eingeschlossen, fühlen sich von diesem Ansatz angegriffen – oft wird erst nach der Investition von Zeit und Mühe in die Entwicklung einer einseitigen Anwendung klar, was die Nachteile sind.

Ein hybrider Ansatz

Am Ende des Tages wollen wir wirklich eine Mischung aus dem neuen und dem alten Ansatz: Wir wollen aus Performance- und SEO-Gründen vollständig geformtes HTML vom Server ausliefern, aber wir wollen die Geschwindigkeit und Flexibilität der clientseitigen Anwendungslogik.

Zu diesem Zweck haben wir bei Airbnb mit „Isomorphic JavaScript“-Anwendungen experimentiert, d. h. JavaScript-Anwendungen, die sowohl auf der Client- als auch auf der Serverseite ausgeführt werden können.

Eine isomorphe Anwendung könnte so aussehen, hier „Client-Server-MVC“ genannt:

In dieser Welt kann ein Teil der Anwendungs- und Ansichtslogik sowohl auf dem Server als auch auf dem Client ausgeführt werden. Das öffnet alle möglichen Türen – Leistungsoptimierungen, bessere Wartbarkeit, SEO-by-default und zustandsorientiertere Webanwendungen.

Mit Node.js, einer schnellen, stabilen serverseitigen JavaScript-Laufzeitumgebung, können wir diesen Traum nun Wirklichkeit werden lassen. Durch die Schaffung der entsprechenden Abstraktionen können wir unsere Anwendungslogik so schreiben, dass sie sowohl auf dem Server als auch auf dem Client läuft – die Definition von isomorphem JavaScript.

Isomorphes JavaScript in freier Wildbahn

Diese Idee ist nicht neu – Nodejitsu hat 2011 eine großartige Beschreibung der isomorphen JavaScript-Architektur verfasst – aber sie wurde nur langsam angenommen. Es gab bereits einige isomorphe Frameworks.

Mojito war das erste isomorphe Open-Source-Framework, das in die Presse kam. Es ist ein fortschrittliches, auf Node.js basierendes Full-Stack-Framework, aber seine Abhängigkeit von YUI und Yahoo!-spezifischen Eigenheiten haben nicht zu großer Popularität in der JavaScript-Community geführt, seit sie es im April 2012 als Open Source zur Verfügung gestellt haben.

Meteor ist heute wahrscheinlich das bekannteste isomorphe Projekt. Meteor wurde von Grund auf für die Unterstützung von Echtzeitanwendungen entwickelt, und das Team baut ein ganzes Ökosystem um seinen Paketmanager und seine Bereitstellungstools herum auf. Wie Mojito ist es ein großes, meinungsstarkes Node.js-Framework, das jedoch die JavaScript-Community viel besser eingebunden hat und dessen lang erwartete Version 1.0 kurz bevorsteht. Meteor ist ein Projekt, das man im Auge behalten sollte – es verfügt über ein All-Star-Team und hat 11,2 Millionen Dollar von Andreessen Horowitz erhalten, was für ein Unternehmen, das sich ganz auf die Veröffentlichung eines Open-Source-Produkts konzentriert, ungewöhnlich ist.

Asana, die von Facebook-Mitbegründer Dustin Moskovitz gegründete App zur Aufgabenverwaltung, hat eine interessante isomorphe Geschichte. In Anbetracht von Moskovitz‘ Status als jüngster Milliardär der Welt hat Asana Jahre in R&D verbracht, um sein Closed-Source-Framework Luna zu entwickeln, eines der fortschrittlichsten Beispiele für isomorphes JavaScript. Luna, das ursprünglich auf v8cgi aufbaute, als es noch kein Node.js gab, ermöglicht es, für jede einzelne Benutzersitzung eine vollständige Kopie der Anwendung auf dem Server laufen zu lassen. Für jeden Benutzer wird ein separater Serverprozess ausgeführt, der auf dem Server denselben JavaScript-Anwendungscode ausführt wie auf dem Client, was eine ganze Reihe fortschrittlicher Optimierungen ermöglicht, wie z. B. robuste Offline-Unterstützung und schnelle Echtzeit-Updates.

Wir haben Anfang dieses Jahres eine eigene isomorphe Bibliothek auf den Markt gebracht. Sie heißt Rendr und ermöglicht es, eine Backbone.js + Handlebars.js Single-Page-App zu erstellen, die auch auf der Server-Seite vollständig gerendert werden kann. Rendr ist das Ergebnis unserer Erfahrungen beim Umbau der mobilen Airbnb-Webanwendung, um die Seitenladezeiten drastisch zu verbessern, was besonders für Nutzer mit mobilen Verbindungen mit hoher Latenz wichtig ist. Rendr ist eher eine Bibliothek als ein Framework und löst daher im Vergleich zu Mojito oder Meteor weniger Probleme, ist aber leicht zu modifizieren und zu erweitern.

Abstraktion, Abstraktion, Abstraktion

Dass es sich bei diesen Projekten in der Regel um große Full-Stack-Web-Frameworks handelt, spricht für die Schwierigkeit des Problems. Der Client und der Server sind sehr unterschiedliche Umgebungen, und so müssen wir eine Reihe von Abstraktionen schaffen, die unsere Anwendungslogik von den zugrundeliegenden Implementierungen entkoppeln, so dass wir dem Anwendungsentwickler eine einzige API zur Verfügung stellen können.

Routing

Wir wollen einen einzigen Satz von Routen, die URI-Muster auf Route-Handler abbilden. Unsere Routehandler müssen in der Lage sein, auf HTTP-Header, Cookies und URI-Informationen zuzugreifen und Umleitungen zu spezifizieren, ohne direkt auf window.location (Browser) oder req und res (Node.js) zuzugreifen.

Daten abrufen und persistieren

Wir wollen die Ressourcen beschreiben, die zum Rendern einer bestimmten Seite oder Komponente benötigt werden, unabhängig vom Abrufmechanismus. Der Ressourcendeskriptor könnte ein einfacher URI sein, der auf einen JSON-Endpunkt verweist, oder für größere Anwendungen kann es sinnvoll sein, Ressourcen in Modellen und Sammlungen zu kapseln und eine Modellklasse und einen Primärschlüssel anzugeben, die dann irgendwann in einen URI übersetzt werden.

View-Rendering

Ob wir uns nun dafür entscheiden, das DOM direkt zu manipulieren, mit String-basiertem HTML-Templating zu arbeiten oder uns für eine UI-Komponentenbibliothek mit einer DOM-Abstraktion zu entscheiden, wir müssen in der Lage sein, Markup isomorph zu erzeugen. Wir sollten in der Lage sein, jede Ansicht entweder auf dem Server oder auf dem Client zu rendern, abhängig von den Anforderungen unserer Anwendung.

Erstellung und Paketierung

Es stellt sich heraus, dass das Schreiben von isomorphem Anwendungscode nur die halbe Miete ist. Tools wie Grunt und Browserify sind wesentliche Bestandteile des Workflows, um die Anwendung tatsächlich zum Laufen zu bringen. Es kann eine Reihe von Build-Schritten geben: Kompilieren von Templates, Einbeziehen von Client-seitigen Abhängigkeiten, Anwenden von Transformationen, Minifizierung usw. Im einfachsten Fall werden der gesamte Anwendungscode, die Ansichten und die Vorlagen in einem einzigen Paket zusammengefasst, aber bei größeren Anwendungen kann dies dazu führen, dass Hunderte von Kilobytes heruntergeladen werden müssen. Ein fortschrittlicherer Ansatz besteht darin, dynamische Pakete zu erstellen und das Lazy-Loading von Assets einzuführen, was jedoch schnell kompliziert wird. Mit statischen Analysewerkzeugen wie Esprima können ehrgeizige Entwickler fortgeschrittene Optimierungen und Metaprogrammierung versuchen, um den Boilerplate-Code zu reduzieren.

Kleine Module zusammenstellen

Wer als Erster mit einem isomorphen Framework auf den Markt kommt, muss all diese Probleme auf einmal lösen. Dies führt jedoch zu großen, unhandlichen Frameworks, die schwer zu übernehmen und in eine bereits bestehende Anwendung zu integrieren sind. Je mehr Entwickler sich mit diesem Problem auseinandersetzen, desto mehr kleine, wiederverwendbare Module können integriert werden, um isomorphe Anwendungen zu erstellen.

Es stellt sich heraus, dass die meisten JavaScript-Module bereits mit wenigen bis keinen Änderungen isomorph verwendet werden können. Zum Beispiel können beliebte Bibliotheken wie Underscore, Backbone.js, Handlebars.js, Moment und sogar jQuery auf dem Server verwendet werden.

Um dies zu demonstrieren, habe ich eine Beispielanwendung namens isomorphic-tutorial erstellt, die Sie auf GitHub überprüfen können. Durch die Kombination einiger Module, die jeweils isomorph verwendet werden können, ist es einfach, eine einfache isomorphe App in nur ein paar hundert Zeilen Code zu erstellen. Es verwendet Director für server- und browserbasiertes Routing, Superagent für HTTP-Anfragen und Handlebars.js für Templates, die alle auf einer einfachen Express.js-App aufbauen. Natürlich muss man mit zunehmender Komplexität einer App mehr Abstraktionsschichten einführen, aber ich hoffe, dass, wenn mehr Entwickler damit experimentieren, neue Bibliotheken und Standards entstehen werden.

Der Blick von hier

Wenn immer mehr Unternehmen Node.js in der Produktion einsetzen, ist es unvermeidlich, dass immer mehr Web-Apps beginnen, Code zwischen ihrem Client- und Server-Code zu teilen. Es ist wichtig, sich daran zu erinnern, dass isomorphes JavaScript ein Spektrum ist – es kann mit der gemeinsamen Nutzung von Templates beginnen, sich zu einer ganzen Anzeigeschicht einer Anwendung entwickeln und bis zum Großteil der Geschäftslogik der Anwendung reichen. Was und wie genau JavaScript-Code zwischen den Umgebungen ausgetauscht wird, hängt ganz von der zu erstellenden Anwendung und ihren einzigartigen Einschränkungen ab.

Nicholas C. Zakas beschreibt sehr schön, wie er sich vorstellt, dass Anwendungen ihre UI-Schicht vom Client auf den Server verlagern werden, um die Leistung und Wartbarkeit zu optimieren. Eine Anwendung muss ihr Backend nicht herausreißen und durch Node.js ersetzen, um isomorphes JavaScript zu verwenden und damit das Kind mit dem Bade ausschütten. Stattdessen kann das traditionelle Backend neben der Node.js-Schicht bestehen bleiben, indem sinnvolle APIs und RESTful-Ressourcen erstellt werden.

Bei Airbnb haben wir bereits damit begonnen, unseren clientseitigen Erstellungsprozess auf Node.js-basierte Tools wie Grunt und Browserify umzustellen. Unsere Rails-Hauptanwendung wird vielleicht nie ganz durch eine Node.js-Anwendung ersetzt werden, aber durch den Einsatz dieser Tools wird es immer einfacher, bestimmte Teile von JavaScript und Templates zwischen verschiedenen Umgebungen auszutauschen.

Mehr erfahren

Wenn Sie diese Idee begeistert, besuchen Sie den Isomorphic JavaScript-Workshop, den ich am Dienstag, den 12. November in San Francisco auf der DevBeat oder am Donnerstag, den 21. November auf der General Assembly geben werde.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.