poniedziałek, 4 kwietnia 2016

Trochę o programowaniu interfejsów użytkownika

Dawno, dawno temu było sobie JQuery. Co było wcześniej już chyba nikt nie pamięta. Ci, którym tak jak mnie coś tam świta, pamiętają jedynie, że JQuery przyniosło wybawienie. Skończyły się czasy żmudnego odwoływania się do elementów po konkretnej ścieżce (lub nadawania wszędzie id), a co więcej skończyły się różnice w pisaniu kodu pomiędzy przeglądarkami. To drugie na początku nie miało żadnego znaczenia. Wszyscy używali IE więc w sumie to była tylko jedna przeglądarka. Ale nie o JQuery będzie dzisiejszy post lecz o podejściu do tworzenia interfejsu użytkownika w Javascriptcie.

Zatem było sobie JQuery. Później pojawiło się JQuery UI. Rozszerzało ono, jeśli się nie mylę, prototyp JQuery o funkcję widget. Jak sama nazwa wskazuje pojawiły się więc widgety. Nagle na formularzu mogliśmy mieć klikalny kalendarz, wyskakujące okienko zamiast brzydkiego alertu, jakieś zakładki, przyciski i pewnie jeszcze parę innych elementów. Niektóre z tych widgetów potrzebowały stanu (np. po wyborze daty z kalendarza, gdzieś trzeba tę datę zapisać). Gdzie go przechowywano? Bezpośrednio w elementach. Czy to w jakimś ukrytym input'cie (diplay: none lub po prostu <hidden>), czy za pomocą magicznego wywołania $.data na elemencie. I na tym JQuery powinno się zakończyć. Niestety wyobraźnia (czy też jej brak) programistów i zapewne ich aktualne doświadczenie powodowały, że szli oni o krok za daleko. Zaczęły powstawać różnorakie dynamiczne widoki. Nie lada sztuką było odnalezienie się w powiązaniach między poszczególnymi elementami. Problemy zaczynały się kiedy to kliknięcie na przycisk ukrywało element formularza, a jego walidacja decydowała czy walidować element na podstawie jego widoczności. Wystarczyło najpierw wypełnić niepoprawnie pole i później je ukryć by w ten podstępny sposób błędne dane przedostały się do serwera.

Każdy większy widok kończył się niekończącym śledzeniem podpiętych zdarzeń. Na szczęście zarówno po stronie serwera jak i klienta zaczął pojawiać się wzorzec MVC. Oddzielał on widok od danych, które w przypadku programowania w JQuery oznaczały dokładnie to samo (przechowywanie stanu w DOMie oraz logika zależna od DOMu). Wzorzec ten po stronie Javascriptu mocno upowszechnił Backbone.

Backbone

Sama biblioteka powstała mniej więcej w tym samym czasie co Angular. Jednak na początku to Backbone cieszył się sporą popularnością. W bardzo prosty sposób definiowało się model danych jako po prostu osobny obiekt, który posiadał różne ciekawe metody. Jedne z nich umożliwiały synchronizację modelu z serwerem, inne dawały możliwość łatwego walidowania danych czy reagowania na zmiany. W widokach nadal używało się JQuery choć Backbone za pomocą biblioteki Underscore wprowadził obsługę szablonów. Walidacja nigdy więcej nie zależała już od widoku. Gdy pojawiał się błąd wiadomo było gdzie szukać.

Jednak pojawił się inny problem. Widoki były świetne ale tylko dopóki się nie rozrosły. Na początku każdy zaczynając projekt odświeżał sobie widok na każdej zmianie modelu. Widok nie analizował niczego tylko usuwał co było wyświetlone i dodawał od nowa. Kod był banalnie prosty i łatwo było go testować. Niestety bardzo szybko to podejście skutkowało utratą wydajności. Ponadto tworzenie większości widoków i modeli stało się żmudnym zadaniem typu kopiuj-wklej. Powstało więc dużo frameworków i pluginów typu Marionette czy Thorax, które tę sprawę ułatwiały. Angular z kolei skupiał się na czym innym.

Angular

Idea Angulara polegała na minimalizowaniu pisania kodu w Javascript'cie. Kod miał być bardziej deklaratywny jak HTML. Angular w swoich szablonach wykorzystywał więc wiele dodatkowych atrybutów zaczynających się od "ng". Pisanie w Javascript'cie ograniczało się do pisania kontrolerów, gdzie umieszczaliśmy nasze funkcje i dane, które wykorzystywał widok. Dostarczał on też mnóstwo rozwiązań na niemal każdy problem (np. logowanie). Pozwalało to bardzo szybko wystartować z projektem. Jednak jak to zwykle bywa im dalej w las tym więcej drzew. Prędzej czy później do Angulara trzeba było coś dopisać. A to nie było już takie banalne jak w przypadku Backbone'a. A szybkość działania? W Angularze nie mieliśmy specjalnie kontroli nad tym co kiedy się odrysowuje. Najczęściej następowało to po zmianie modelu. W większych aplikacjach stanowiło to spory problem.

React

Facebook doskonale ten problem rozumiał. Wszyscy chcieli mieć prosty widok, bez zbędnych ifów i jeszcze musiał być szybki. Jak tę szybkość można było uzyskać? No jedynie analizując co jest narysowane oraz co się zmieniło by nie odrysowywać za dużo. Samo odrysowywanie to oczywiście uogólnienie. Podpinanie zdarzeń i usuwanie elementów też zabiera istotny czas. A co jak byśmy sobie narysowali widok na boku i porównali z tym co mamy aktualnie w DOMie? Tak narodziła się idea VirtualDOM. Niczego od razu nie odrysowujemy, najpierw robimy to w pamięci co jest bardzo szybkie, a później porównujemy i nanosimy odpowiednie, najczęściej mikroskopijne, zmiany. Widok jest prosty, model jest prosty i kontroler pozostaje prosty. Nie musimy też, tak samo jak w Angularze, ręcznie decydować co kiedy odrysować. React pilnuje, który model się zmienił, i który widok należy odświeżyć.

Co dalej?

Tutaj tak naprawdę zaczyna się sedno tego postu. Pewnie każdy wie, że Angular 2 pisany jest w TypeScript'cie. React z kolei wykorzystuje Javascript w o wiele bardziej rozbudowanej wersji niż tam wspierana przez przeglądarki. Poza tym pojawiło się wiele kompilatorów innych języków do Javascriptu. Na dodatek cały czas toczy się projekt WebAssembly, który jak nazwa wskazuje, ma wprowadzić do przeglądarek obsługę bytecode'u by móc pisać cięższe aplikacje (jak np. gry). Ale po co to komu?

Javascript po prostu muli. Da się w nim pisać, tak aby nie mulił ale taki kod nie przypomina w niczym kodu czytelnego. Weźmy taki przykład:

var x;
var result = 1;
for (x = 0; x < 10; x++) {
    result += x*x*result - 1;
}

Kod oczywiście nic sensownego nie robi ale załóżmy, że tak jest. W pewnym momencie dochodzimy do sytuacji, że wzór obliczeń wykorzystywany w pętli jest potrzebny w kilku miejscach. Najprościej jest więc go skopiować. Ale z drugiej strony jak utworzymy sobie funkcję to będzie łatwiej nim zarządzać. Niestety wywołanie każdej funkcji kosztuje, a już najwięcej w Javascript'cie kosztuje wywołanie funkcji rekurencyjnych. Optymalny kod jest najczęściej nie do strawienia.

Z drugiej strony taki kompilator to może sobie optymalizować jak chce. W naszym przykładzie mógłby wziąć ciało naszej funkcji i powrzucać je w każde miejsce jej wywołania (tzw. inlining). W tym właśnie celu (optymalizacji) powstał Google Closure Compiler. Jest to kompilator z Javascriptu do Javascriptu. Potrafi zoptymalizować i znacząco skrócić kod jeśli tylko trzymamy się jego wytycznych. Szybkość działania nie jest jednak jedynym problemem. Poprawność i łatwość utrzymania jest, jeśli nie ważniejsza to przynajmniej równie ważna. Dlatego warto się przyjrzeć dostępnym technologiom.

TypeScript

Jest to język rozszerzający Javascript o klasy, interfejsy i proste typy danych. Statyczne typowanie ułatwia tworzenie kodu bo o jego spójność dba kompilator. Jednak same typy to trochę mało. Dynamiczne języki dla odmiany działają na zasadzie, nie ważne jaki typ zostanie przekazany, dla większości powinno działać poprawnie (do pozostałej części dopisze się unit testy). I też programiści dają radę. Nie podzielam więc wizji Angulara. Jak już wybierać to coś co daje nam konkretne zalety.

ClojureScript

ClojureScript to oczywiście Clojure czyli dynamiczny język funkcyjny. Zaczynając pisać w czymś takim mamy tę zaletę (poza tym, że jest funkcyjny), że wiele bibliotek do Clojure już powstało a teraz możemy je wykorzystać (o ile nie korzystają z Javy) do części webowej. Dostępna jest w tym języku bardzo dobra biblioteka o nazwie Om, która wykorzystuje Reacta. Wykorzystuje i właściwie rozszerza. Sprawia, że widoki odrysowują się rzadziej i programista ma nad tym większą kontrolę. Co tu dużo mówić, fajna sprawa.

PureScript i Haskell

W Haskellu mamy dostępnych kilka kompilatorów do Javascriptu. Jest GHCJS, które to potrafi skompilować większość kodu Haskellowego ale kod wynikowy jest dość duży. Jest też haste, które nie potrafi kompilować kodu wykorzystującego TemplateHaskell ale za to wynikowy Javascript jest o wiele lżejszy i szybszy. Do tego jeszcze mamy całkiem osobny projekt o nazwie PureScript, który jest osobnym językiem naśladującym Haskella. W tych technologiach (jak i w powyższych) musimy wykorzystać, jakąś bibliotekę wykorzystującą np. Reacta lub VirtualDOM by nie powielać podejścia Backbone'a czy Angulara. Ale za to mamy naprawdę porządne narzędzie do pisania aplikacji.

Elm

Ten język zostawiłem na koniec, ponieważ według mnie jest on najbardziej dopracowany pod kątem tworzenia interfejsów. Daje on nam nie tylko bezpośredni dostęp do VirtualDOMa ale też wprowadza programowanie w stylu FRP (Functional Reactive Programming). W tym stylu w uproszczeniu za pomocą sygnałów łączymy aplikację w całość. Sygnały mogą pochodzić z widoku (np. po kliknięciu) albo z zewnątrz (np. wywołanie ajaxowe). Mogą one być stałe (np. co 60 sek.) albo na żądanie. Elm jest podobny do Haskella ale o wiele prostszy. Jednak mimo to mamy całkowitą kontrolę nad efektami, mamy język funkcyjny oraz mamy FRP. A do tego jeszcze jest bardzo szybki. Naprawdę warto dać mu szansę.

Brak komentarzy:

Prześlij komentarz