AirbnbEng

Follow

Nov 11, 2013 – 10 min read

By Spike Brehm

This post has been cross-posted on VentureBeat.

W Airbnb, wiele się nauczyliśmy w ciągu ostatnich kilku lat, budując bogate doświadczenia internetowe. W 2011 roku, wraz z naszą mobilną stroną internetową, wkroczyliśmy w świat aplikacji jednostronicowych, a od tego czasu uruchomiliśmy między innymi Listy Życzeń i naszą nowo zaprojektowaną stronę wyszukiwania. Każda z nich jest dużą aplikacją JavaScript, co oznacza, że większość kodu działa w przeglądarce, aby wspierać bardziej nowoczesne, interaktywne doświadczenie.

To podejście jest dziś powszechne, a biblioteki takie jak Backbone.js, Ember.js i Angular.js ułatwiły programistom budowanie tych bogatych aplikacji JavaScript. Stwierdziliśmy jednak, że tego typu aplikacje mają pewne krytyczne ograniczenia. Aby wyjaśnić dlaczego, najpierw zróbmy szybki objazd przez historię aplikacji sieciowych.

JavaScript dorasta

Od zarania sieci, przeglądanie stron działało w następujący sposób: przeglądarka żądała określonej strony (powiedzmy, „http://www.geocities.com/”), powodując, że serwer gdzieś w Internecie generował stronę HTML i wysyłał ją z powrotem przez kabel. To działało dobrze, ponieważ przeglądarki nie były zbyt potężne, a strony HTML reprezentowały dokumenty, które były w większości statyczne i samowystarczalne. JavaScript, stworzony, aby umożliwić stronom internetowym bycie bardziej dynamicznymi, nie pozwalał na wiele więcej niż pokazy slajdów i widżety do wybierania daty.

Po latach postępu w dziedzinie komputerów osobistych, kreatywni technolodzy popchnęli sieć do granic możliwości, a przeglądarki internetowe ewoluowały, aby dotrzymać im kroku. Teraz sieć dojrzała do w pełni funkcjonalnej platformy aplikacyjnej, a szybki JavaScript i standardy HTML5 umożliwiły programistom tworzenie bogatych aplikacji, które wcześniej były możliwe tylko na platformach natywnych.

Aplikacja jednostronicowa

Nie minęło wiele czasu, zanim programiści zaczęli budować całe aplikacje w przeglądarce przy użyciu JavaScript, korzystając z tych nowych możliwości. Aplikacje takie jak Gmail, klasyczny przykład aplikacji typu single-page, mogły natychmiast reagować na interakcje użytkownika, nie potrzebując już robić rundki do serwera tylko po to, by wyrenderować nową stronę.

Biblioteki takie jak Backbone.js, Ember.js i Angular.js są często określane jako biblioteki MVC (Model-View-Controller) lub MVVM (Model-View-ViewModel) po stronie klienta. Typowa architektura MVC po stronie klienta wygląda mniej więcej tak:

Większość logiki aplikacji (widoki, szablony, kontrolery, modele, internacjonalizacja, itp.) żyje w kliencie, a on rozmawia z API dla danych. Serwer może być napisany w dowolnym języku, takim jak Ruby, Python, czy Java, i głównie zajmuje się obsługą początkowej strony HTML. Po pobraniu plików JavaScript przez przeglądarkę, są one oceniane, a aplikacja po stronie klienta jest inicjalizowana, pobierając dane z API i renderując resztę strony HTML.

Jest to świetne dla użytkownika, ponieważ po wstępnym załadowaniu aplikacji, może ona obsługiwać szybką nawigację między stronami bez odświeżania strony, a jeśli jest zrobiona dobrze, może nawet pracować w trybie offline.

Jest to świetne rozwiązanie dla deweloperów, ponieważ wyidealizowana aplikacja jednostronicowa posiada wyraźne rozdzielenie obaw pomiędzy klientem a serwerem, promując przyjemny przepływ pracy i zapobiegając potrzebie dzielenia się zbyt dużą ilością logiki pomiędzy nimi, które często są napisane w różnych językach.

W praktyce jednak, istnieje kilka fatalnych wad tego podejścia, które uniemożliwiają jego zastosowanie w wielu przypadkach użycia.

SEO

Aplikacja, która może działać tylko po stronie klienta, nie może obsługiwać HTML dla robotów indeksujących, więc domyślnie będzie miała słabe SEO. Działanie crawlerów polega na wysyłaniu żądania do serwera WWW i interpretowaniu wyniku, ale jeśli serwer zwraca pustą stronę, nie ma to większej wartości. Istnieją obejścia, ale nie bez przeskakiwania przez pewne obręcze.

Wydajność

Przez ten sam znak, jeśli serwer nie renderuje pełnej strony HTML, ale zamiast tego czeka na JavaScript po stronie klienta, aby to zrobić, użytkownicy doświadczą kilku krytycznych sekund pustej strony lub ładowania spinnera przed zobaczeniem treści na stronie. Istnieje wiele badań pokazujących drastyczny wpływ powolnej strony na użytkowników, a tym samym na przychody. Amazon twierdzi, że każde 100ms redukcji czasu ładowania strony podnosi przychody o 1%. Twitter spędził rok i 40 inżynierów przebudowujących swoją stronę tak, aby renderowała się na serwerze zamiast na kliencie, twierdząc, że 5-krotna poprawa postrzeganego czasu ładowania.

Maintainability

Choć idealny przypadek może prowadzić do ładnej, czystej separacji obaw, nieuchronnie niektóre elementy logiki aplikacji lub logiki widoku kończą się duplikacją między klientem a serwerem, często w różnych językach. Powszechnymi przykładami są formatowanie daty i waluty, sprawdzanie poprawności formularzy i logika routingu. To sprawia, że konserwacja staje się koszmarem, szczególnie w przypadku bardziej złożonych aplikacji.

Niektórzy programiści, w tym ja, czują się ugryzieni przez to podejście – często dopiero po zainwestowaniu czasu i wysiłku w budowę aplikacji jednostronicowej staje się jasne, jakie są jej wady.

Podejście hybrydowe

Na koniec dnia tak naprawdę chcemy hybrydy nowego i starego podejścia: chcemy serwować w pełni sformatowany HTML z serwera dla wydajności i SEO, ale chcemy szybkości i elastyczności logiki aplikacji po stronie klienta.

W tym celu, eksperymentowaliśmy w Airbnb z aplikacjami „Isomorphic JavaScript”, czyli aplikacjami JavaScript, które mogą działać zarówno po stronie klienta, jak i po stronie serwera.

Aplikacja izomorficzna może wyglądać w ten sposób, nazwana tutaj „Client-server MVC”:

W tym świecie, część logiki aplikacji i widoku może być wykonywana zarówno na serwerze jak i na kliencie. Otwiera to wszelkiego rodzaju drzwi – optymalizacja wydajności, lepsza konserwacja, SEO-by-default i bardziej stanowe aplikacje webowe.

Z Node.js, szybkim, stabilnym runtime JavaScript po stronie serwera, możemy teraz urzeczywistnić to marzenie. Tworząc odpowiednie abstrakcje, możemy napisać naszą logikę aplikacji tak, aby działała zarówno na serwerze, jak i na kliencie – definicja izomorficznego JavaScriptu.

Isomorphic JavaScript in the Wild

Ta idea nie jest nowa – Nodejitsu napisało świetny opis architektury izomorficznego JavaScriptu w 2011 roku – ale przyjmowała się powoli. Było kilka izomorficznych frameworków, które już się pojawiły.

Mojito był pierwszym open-source’owym izomorficznym frameworkiem, który dostał jakąkolwiek prasę. Jest to zaawansowany, pełnowartościowy framework oparty na Node.js, ale jego zależność od YUI i specyficzne dziwactwa Yahoo!nie doprowadziły do dużej popularności w społeczności JavaScript od czasu otwarcia źródła w kwietniu 2012.

Meteor jest prawdopodobnie najbardziej znanym obecnie projektem izomorficznym. Meteor jest zbudowany od podstaw, aby wspierać aplikacje czasu rzeczywistego, a zespół buduje cały ekosystem wokół jego menedżera pakietów i narzędzi do wdrażania. Podobnie jak Mojito, jest to duży, opiniotwórczy framework Node.js, jednak znacznie lepiej radzi sobie z angażowaniem społeczności JavaScript, a jego wyczekiwane wydanie 1.0 jest tuż za rogiem. Meteor to projekt, który warto śledzić – ma zespół gwiazd i zebrał 11,2 miliona dolarów od Andreessen Horowitz – co jest niespotykane w przypadku firmy całkowicie skoncentrowanej na wydaniu produktu open-source.

Asana, aplikacja do zarządzania zadaniami założona przez współzałożyciela Facebooka, Dustina Moskovitza, ma ciekawą izomorficzną historię. Nie bojąc się o fundusze, biorąc pod uwagę status Moskovitza jako najmłodszego miliardera na świecie, Asana spędziła lata w R&D rozwijając swój zamknięty framework Luna, jeden z najbardziej zaawansowanych przykładów izomorficznego JavaScriptu. Luna, pierwotnie zbudowana na v8cgi w czasach zanim istniał Node.js, pozwala na uruchomienie kompletnej kopii aplikacji na serwerze dla każdej pojedynczej sesji użytkownika. Uruchamia oddzielny proces serwera dla każdego użytkownika, wykonując ten sam kod aplikacji JavaScript na serwerze, który jest uruchamiany w kliencie, umożliwiając całą klasę zaawansowanych optymalizacji, takich jak solidne wsparcie offline i szybkie aktualizacje w czasie rzeczywistym.

Na początku tego roku uruchomiliśmy własną bibliotekę izomorficzną. Nazywa się Rendr i pozwala na zbudowanie aplikacji Backbone.js + Handlebars.js, która może być w pełni renderowana po stronie serwera. Rendr jest produktem naszego doświadczenia w przebudowywaniu mobilnej aplikacji internetowej Airbnb, aby drastycznie poprawić czas ładowania stron, co jest szczególnie ważne dla użytkowników korzystających z połączeń mobilnych o dużych opóźnieniach. Rendr stara się być raczej biblioteką niż frameworkiem, więc rozwiązuje mniej problemów niż Mojito czy Meteor, ale jest łatwy w modyfikacji i rozbudowie.

Abstrakcja, abstrakcja, abstrakcja

To, że te projekty mają tendencję do bycia dużymi, pełnymi frameworkami webowymi, świadczy o trudności problemu. Klient i serwer to bardzo niepodobne do siebie środowiska, dlatego musimy stworzyć zestaw abstrakcji, które odłączą logikę naszej aplikacji od bazowych implementacji, dzięki czemu będziemy mogli wyeksponować pojedynczy interfejs API dla twórcy aplikacji.

Routing

Chcemy mieć pojedynczy zestaw tras, które mapują wzorce URI do handlerów tras. Nasze route handlery muszą mieć dostęp do nagłówków HTTP, ciasteczek i informacji URI oraz określać przekierowania bez bezpośredniego dostępu do window.location (przeglądarka) lub req i res (Node.js).

Pobieranie i przechowywanie danych

Chcemy opisać zasoby potrzebne do renderowania konkretnej strony lub komponentu niezależnie od mechanizmu pobierania. Deskryptor zasobu może być prostym URI wskazującym na punkt końcowy JSON, lub dla większych aplikacji, użyteczne może być zamknięcie zasobów w modelach i kolekcjach oraz określenie klasy modelu i klucza głównego, które w pewnym momencie zostaną przetłumaczone na URI.

Renderowanie widoków

Czy zdecydujemy się bezpośrednio manipulować DOM, trzymać się szablonów HTML opartych na łańcuchach znaków, czy zdecydować się na bibliotekę komponentów UI z abstrakcją DOM, musimy być w stanie generować znaczniki izomorficznie. Powinniśmy być w stanie renderować dowolny widok na serwerze lub kliencie, w zależności od potrzeb naszej aplikacji.

Budowanie i pakowanie

Okazuje się, że pisanie izomorficznego kodu aplikacji to tylko połowa bitwy. Narzędzia takie jak Grunt i Browserify są niezbędnymi częściami przepływu pracy, aby faktycznie uruchomić aplikację. Może istnieć wiele kroków budowania: kompilacja szablonów, włączanie zależności po stronie klienta, stosowanie transformacji, minifikacja, itp. Najprostszym rozwiązaniem jest połączenie całego kodu aplikacji, widoków i szablonów w jeden pakiet, ale w przypadku większych aplikacji może to skutkować pobraniem setek kilobajtów. Bardziej zaawansowanym podejściem jest tworzenie dynamicznych pakietów i wprowadzenie leniwego ładowania zasobów, jednak to szybko się komplikuje. Narzędzia do analizy statycznej, takie jak Esprima, mogą pozwolić ambitnym programistom na próby zaawansowanej optymalizacji i metaprogramowania w celu zredukowania szablonowego kodu.

Składanie razem małych modułów

Bycie pierwszym na rynku z izomorficznym frameworkiem oznacza, że musisz rozwiązać wszystkie te problemy naraz. Ale to prowadzi do dużych, nieporęcznych frameworków, które są trudne do zaadoptowania i zintegrowania z już istniejącą aplikacją. Gdy więcej programistów zajmie się tym problemem, zobaczymy eksplozję małych, wielokrotnego użytku modułów, które mogą być zintegrowane razem, aby budować izomorficzne aplikacje.

Okazuje się, że większość modułów JavaScript może być już używana izomorficznie z niewielką lub żadną modyfikacją. Na przykład, popularne biblioteki takie jak Underscore, Backbone.js, Handlebars.js, Moment, a nawet jQuery mogą być używane na serwerze.

Aby zademonstrować ten punkt, stworzyłem przykładową aplikację o nazwie isomorphic-tutorial, którą można sprawdzić na GitHubie. Łącząc razem kilka modułów, z których każdy może być użyty izomorficznie, łatwo jest stworzyć prostą izomorficzną aplikację w zaledwie kilkuset liniach kodu. Wykorzystuje Director do routingu opartego na serwerze i przeglądarce, Superagent do obsługi żądań HTTP oraz Handlebars.js do szablonowania, a wszystko to zbudowane na bazie podstawowej aplikacji Express.js. Oczywiście, gdy aplikacja staje się coraz bardziej złożona, trzeba wprowadzić więcej warstw abstrakcji, ale mam nadzieję, że gdy więcej programistów będzie z tym eksperymentować, pojawią się nowe biblioteki i standardy.

The View From Here

As more organizations get comfortable running Node.js in production, it’s inevitable that more and more web apps will begin to share code between their client and server code. Ważne jest, aby pamiętać, że izomorficzny JavaScript jest spektrum – może zacząć się od współdzielenia szablonów, rozwinąć się do warstwy widoku całej aplikacji, aż do większości logiki biznesowej aplikacji. Dokładnie co i jak kod JavaScript jest współdzielony pomiędzy środowiskami zależy całkowicie od budowanej aplikacji i jej unikalnego zestawu ograniczeń.

Nicholas C. Zakas ma ładny opis tego, jak przewiduje, że aplikacje zaczną ściągać swoją warstwę UI z klienta na serwer, umożliwiając optymalizację wydajności i łatwości utrzymania. Aplikacja nie musi wyrywać swojego backendu i zastępować go Node.js, aby używać izomorficznego JavaScriptu, zasadniczo wylewając dziecko z kąpielą. Zamiast tego, poprzez tworzenie sensownych API i zasobów RESTful, tradycyjny backend może żyć obok warstwy Node.js.

W Airbnb, już zaczęliśmy zmieniać nasz proces budowania po stronie klienta, by używać narzędzi opartych na Node.js, takich jak Grunt i Browserify. Nasza główna aplikacja Rails może nigdy nie zostać całkowicie wyparta przez aplikację Node.js, ale dzięki tym narzędziom coraz łatwiej jest współdzielić pewne fragmenty JavaScript i szablony między środowiskami.

Słyszeliście to pierwsi – w ciągu kilku lat rzadko będzie można zobaczyć zaawansowaną aplikację internetową, która nie używa JavaScript na serwerze.

Learn More

Jeśli ten pomysł Cię ekscytuje, przyjdź na warsztaty Isomorphic JavaScript, które poprowadzę w DevBeat we wtorek, 12 listopada w San Francisco lub na General Assembly w czwartek, 21 listopada. Będziemy wspólnie pracować nad przykładową, izomorficzną aplikacją Node.js, którą stworzyłem, by zademonstrować, jak łatwo jest zacząć pisać izomorficzne aplikacje.

Bądź na bieżąco z ewolucją aplikacji internetowych Airbnb, śledząc mnie na @spikebrehm, a zespół inżynierów Airbnb na @AirbnbEng.

.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.