Zadaniem Handlebars jest połączenie szablonu z obiektem danych i wyprodukowanie z tego tekstu, najczęściej HTML. Reguła, która odróżnia to narzędzie od zwykłego doklejania napisów, brzmi: w szablonie nie ma języka programowania. Są miejsca do wypełnienia i nazwane operacje przygotowane wcześniej w kodzie.
Szablon wycięty z kartonu
Malarz pokojowy używa szablonu z wyciętymi otworami. Przykłada go do ściany, przeciąga wałkiem i farba trafia tylko tam, gdzie karton ma dziurę. Szablon nie decyduje o kolorze farby, nie decyduje o tym, czy w ogóle malować, i nie potrafi policzyć, ile razy powtórzyć wzór, jeżeli nikt mu tego nie wyciął. Cała inteligencja siedzi w rękach malarza i w warsztacie, w którym karton przygotowano.
Handlebars działa tą samą metodą. Otwory to wyrażenia w podwójnych nawiasach klamrowych, farba to dane, a warsztat to kod aplikacji rejestrujący operacje, których szablon sam nie umie wykonać. Ostatnia część obrazu wraca dalej: karton daje się wyciąć raz, w warsztacie, zamiast wycinać go od nowa przy każdej ścianie.
Wyrażenia, ścieżki i ucieczka znaków
Najprostsze wyrażenie to nazwa pola: {{imie}}. Do pól zagnieżdżonych prowadzi kropka, {{osoba.imie}}, a do kontekstu nadrzędnego dwie kropki ze slashem, {{../adres}}, co bywa potrzebne wewnątrz pętli. Pole o nazwie zawierającej znaki niedozwolone w identyfikatorze bierze się w nawias kwadratowy.
Domyślnie wynik jest chroniony: znaki specjalne HTML są zamieniane, więc ampersand wychodzi jako &, a wstrzyknięcie znacznika przez dane się nie udaje. Potrójny nawias klamrowy tę ochronę wyłącza i wstawia treść bez zmian.
Tu leży pułapka opisana wprost w dokumentacji: mechanizm chroni składnię HTML, nie chroni składni JavaScriptu. Wartość wstawiona wewnątrz atrybutu obsługującego zdarzenie albo wewnątrz znacznika ze skryptem pozostaje niebezpieczna mimo domyślnej ucieczki, bo tam obowiązują inne reguły cytowania.
Helpery, bloki i partiale
Skoro szablon nie ma logiki, potrzebne operacje rejestruje się w kodzie i wywołuje po nazwie. Helper zwykły przyjmuje argumenty pozycyjne i nazwane, a wynik jednego wywołania można podać drugiemu przez wyrażenie w nawiasie okrągłym.
Helper blokowy obejmuje fragment szablonu i dostaje w opcjach funkcję fn renderującą ten fragment oraz inverse renderującą gałąź else. Na tym stoją wbudowane konstrukcje warunkowe i pętle. Iterator udostępnia w środku zmienne pomocnicze, między innymi @index, @first i @last, a od pewnego czasu również nazwane parametry bloku zapisywane jako {{#each osoby as |osoba id|}}, dzięki czemu przy zagnieżdżeniu nie trzeba zgadywać, do którego poziomu odnosi się zmienna.
Partiale to zarejestrowane fragmenty szablonów wstawiane w innych szablonach, czyli sposób na to, żeby stopka albo karta produktu istniały w jednym miejscu.
Prekompilacja, czyli dwie różne biblioteki
Szablon nie jest interpretowany przy każdym renderowaniu. Handlebars zamienia go najpierw w funkcję JavaScriptu i dopiero ta funkcja produkuje tekst. Ten krok da się wykonać wcześniej, poleceniem handlebars dostarczanym z pakietem, a wtedy do przeglądarki jedzie sama gotowa funkcja i okrojona biblioteka bez kompilatora.
Plik z wydania 4.7.9 | Rozmiar | Zawiera kompilator |
|---|
handlebars.js | około 204 kB | tak |
handlebars.min.js | około 89 kB | tak |
handlebars.runtime.js | około 74 kB | nie |
handlebars.runtime.min.js | około 29 kB | nie |
Różnica między dziewięćdziesięcioma a dwudziestoma dziewięcioma kilobajtami to dokładnie kompilator, którego przeglądarka nie potrzebuje, jeżeli karton wycięto wcześniej. Do tego dochodzi czas: kompilacja szablonu na urządzeniu użytkownika wykonuje się przy każdym wejściu na stronę.
Skąd biorą się w tym luki bezpieczeństwa
Historia podatności Handlebars krąży wokół jednego motywu: dostępu do prototypów obiektów JavaScriptu. Odczyt pola przez ścieżkę mógł trafić poza właściwe dane i sięgnąć do wnętrza języka, a to otwierało drogę do wykonania cudzego kodu.
Odpowiedzią były opcje wykonania z ostrożnymi wartościami domyślnymi. Właściwości i metody z prototypu nie są rozwiązywane, o ile nie zostaną wskazane z nazwy. Pole __proto__ jest zablokowane bezwarunkowo, podobnie jak constructor oraz metody definiujące i odczytujące akcesory. Osobna opcja przywracająca stare zachowanie wywołań brakującego helpera jest w dokumentacji opatrzona ostrzeżeniem, że pozwala autorowi szablonu wykonać kod w środowisku uruchamiającym bibliotekę.
Praktyczna reguła jest z tego prosta: szablon to kod, więc szablon pochodzący od użytkownika traktuje się jak kod od użytkownika, a nie jak dane.
Kiedy brak logiki zaczyna przeszkadzać
Ograniczenie, które jest tu zaletą, bywa też źródłem najczęstszego nadużycia. Skoro szablon nie umie porównać dwóch liczb ani sformatować daty, ktoś rejestruje po kolei kilkanaście drobnych helperów, aż powstaje z nich niepisany język programowania rozsypany po kodzie i pozbawiony jakichkolwiek testów. Wtedy logika wróciła do widoku, tylko została schowana.
Zdrowsze rozwiązanie polega na przygotowaniu danych przed renderowaniem, tak żeby szablon dostał gotowe napisy, wartości logiczne i posortowane listy. Handlebars nie jest też właściwym narzędziem tam, gdzie interfejs zmienia się w reakcji na działania użytkownika: szablon produkuje tekst raz i nie pilnuje niczego później, więc utrzymywanie zgodności widoku ze stanem trzeba by dopisać samodzielnie.