Jiří Hýbek
Proč jsem si vyrobil vlastní šablonovací engine v JavaScriptu?

Proč jsem si vyrobil vlastní šablonovací engine v JavaScriptu?

Srovnání různých druhů šablonovacích enginů a představení vlastní knihovny.

#Aktualizace z prosince 2016

Po roce a půl práce na různých projektech jsem se setkal s dalšími knihovnami a frameworky, které jsem se rozhodl použít raději, než svůj šablonovací engine.

#Angular2

Angular2 je novou generací Angular JS frameworku, který nám řeší téměř vše a to formou webových komponent. Výhodou je zápis podobný Polymeru, který se poté zkompiluje do klasického DOMu a framework si řeší i two-way data binding.

Ovšem téměř po půl roce práce s Angularem2, kdy jsem začal na beta verzi, a v rámci několika RC překopali půlku API a stále měl framework mnoho much, jsem se rozhodl od jeho dalšího použití upustit. Další nevýhodou, která se začala projevovat, byly problémy s výkonem. Ovšem určitě se najde mnoho aplikací, kde bude Angular2 užitečným pomocníkem.

Oficiální web Angular2

#ReactJS od Facebooku

ReactJS je opravdu jen šablonovací engine. Ovšem velmi promakaný šablonovací engine.

Funguje na principu virtuálního DOMu a umožňuje používat komponentový model. Pokud tedy dojde ke změně dat v komponentě, je celý DOM znovu vykreslen stejně, jako to dělají hloupé string-based enginy. Ovšem tentokrát s tím rozdílem, že se vykresluje virtuální DOM, což je výpočetně velmi nenáročné, jelikož se jedná jen o datovou reprezentaci modelu. React poté provede patching reálného DOMu, tedy upraví co nejefektivnější cestu jen to, co se liší od virtuálního DOMU. Tím je dosaženo kombinace pohodleného vývoje a výkonu.

Nevýhodou je větší velikost knihovny a nutnost zdrojový kód předím “zkompilovat” nebo použít Babel. Ovšem ve srovnání s komplexními frameworky je to jen zlomek. Pro větší aplikaci se jedná o minimální zátěž.

Osobně považuji za velkou výhodu to, že se jedná jen o šablonovací engin. Nikdo mi tedy nediktuje, jak mám postavit svou aplikaci. React dělá jen jednu věc - vykresluje DOM, a dělá jej dobře.

Více na stránce ReactJS.


Pokračování původního článku

Poslední dobou se poměrně často zabývám tvorbou webových aplikací (rozuměj dospělá aplikace, která se jednou načte a běží bez přenačítání stránek). Existuje mnoho skvělých technologií, které tento vývoj usnadňují.
Většinou jsou vcelku robustní, mají mnoho zbytečných funkcí, ale budiž…

Aktuálně pracuji na projektu META Platform což je platforma pro tvorbu „informačních systémů“ a ta se skládá z webového klienta, který běží jako webová aplikace.

A zde jsem narazil na potřebu šablonovacího jazyka v JavaScriptu, jelikož generovat celé HTML ručně pomocí manipulace s DOMem je na hlavu padlé a už poněkud historická záležitost.

Vyzkoušel jsem mnoho různých enginů, způsobů a technologií až jsem nakonec dospěl k tomu, že jsem si engine napsal sám.
Chci se s vámi podělit o důvody, proč mi ostatní enginy nevyhovovaly a co jsem to vlastně vyrobil.

#Textové šablony

První enginy, které historicky vznikly, fungovaly na jednoduchém principu. Programátor šablonu nadefinoval jako HTML v řetězci a mohl používat šablonovací značky, např.: {if}…{/if} nebo . Engine poté vyprasoval řetězec, provedl v něm úpravy a výsledek vložil jako obsah HTML elementu.

Jednoduchý příklad použití:

1
document.getElementById('obsah').innerHTML = template("{{hello}}", { hello: "World!" });

Samozřejmě tyto enginy prošly mnoha vylepšeními, optimalizací, apod. To ovšem nemění nic na tom, jak reálně pracují.

#Jak to funguje?

Co se stane, když použijeme výše uvedený způsob?

  1. Engine musí přelouskat šablonu a vygenerovat HTML jako string
  2. HTML se přiřadí do elementu jako string
  3. Prohlížeč musí string vyparsovat
  4. Prohlížeč musí vytvořit příslušné DOM elementy a vložit je do stávajícího dokumentu

Co dále se stane?

Představte si, že máte jednoduchý to-do list, kam přidáváte úkoly, a můžete po kliknutí na úkol změnit jeho stav na dokončený, což se projeví přeškrtnutým textem. A aby to vypadalo přehledně, tak úkol má na pozadí barvu.

Co se stane, když na úkol kliknete? Ano, celé HTML se překreslí, stávající elementy jsou odebrány a nové kompletně přidány. A to jsme chtěli jen přidat css třídu k <li> elementu.

A reálný dopad krom výkonu? Obsah šablony problikne! Takže kliknete na položku a ona blikne, jak elegantní! :)
Takže tento typ enginů je velmi jednoduchý na implementaci ale na profi aplikace prakticky nepoužitelný.

Výhody:

  • Jednoduchá implementace

Nevýhody:

  • Výkonově náročné
  • Nepěkné vedlejší efekty

Pár příkladů:

#DOM atributy

Tato varianta je již příhodnější. Nepracuje se stringem, žádné zbytečné parsování a operuje přímo nad DOMem.

Menší příklad z AngularJS:

1
2
3
4
5
6
7
8
9
10
<body ng-controller="PhoneListCtrl">

<ul>
<li ng-repeat="phone in phones">
<span>{{phone.name}}</span>
<p>{{phone.snippet}}</p>
</li>
</ul>

</body>

Jak vidíme, v HTML jsou nějaké custom attributy, které nám udávají chování.

#Jak to funguje?

Nejdříve musíme projet všechny elementy, které obsahují naše custom attributy a postarat se o jejich chování.

Např. ng-repeat: chci zopakovat tag <li> pro všechny phones, a kde ty phones vezmu? Mám tam nějaký controller, který bude mít model a tam budou potřebná data. Takže buď si musíme držet referenci na controller, třeba ve scopu a nebo vždy hledat parent element, který má atribut controlleru.

Proměnné! Dále musíme projít všechny textové nody, zda náhodou neobsahují naše tagy, a buďto si nabindujeme celý textnode a nebo ho rozsekneme na více elementů – např. z { { phone.snippet } } uděláme <span>.

A co ve chvíli kdy nám do již nabindované struktury přibude nový element? Nejprve se musíme dozvědět o tom, že se tak stalo… a poté musíme spustit celé kolo od znovu (samozřejmě jen pro přidané elementy).

Pak si ještě pravděpodobně kvůli optimalizaci uděláme nějaký cache elementů a jejich nabindování, a tak dále, a tak dále. Mně osobně to přijde na pohled pěkné, ovšem velmi robustní a složité řešení se spoustou obsluhy uvnitř.

Výhody:

  • Efektivní – pracuje přímo s DOMem
  • Zápis view-logic přímo v HTML

Nevýhody:

  • Složité vnitřní pochody
  • Většinou závislé na frameworku – nelze použít samostatně

Pár příkladů:

#Generované kompletně kódem

Další kategorií enginů jsou takové, ve kterých se šablona definuje v JS.

Ukázka:

1
2
3
4
5
6
7
8
9
header(
h1('Heading'),
h2('Subheading'));

nav(
ul({ 'class': 'breadcrumbs' },
li(a({ href: '/' }, 'Home')),
li(a({ href: '/section/'}, 'Section')),
li(a('Subject'))));

Kód je přehledný, jasný a s prvky bychom mohli pomocí kódu pravděpodobně manipulovat. Výkon může být v tomto případě velmi vysoký. Ale to už utíkáme od principů HTML a vracíme se někam do dob dávných, kdy jsme i v desktopových aplikacích museli celé UI generovat kódem.

Výhody:

  • Pracuje s DOMem
  • Jednoduchost a přehlednost

Nevýhody:

  • Šablona není definována v HTML (pro mě osobně je to kritický faktor)

Pár příkladů:

  • domjs
  • JADE (ale nevím, zda nezařadit spíše do string-based enginů)

#HTML5 + Polymer

Polymer od Googlu je úžasná technologie a zasloužila by si samostatný článek. Kromě spousty jiných věcí podporuje two-way data binding, šablony přímo v HTML a to díky rozšíření nativního HTML tagu template, apod.

Již výše jsem jej zmínil v kategorii DOM atributy, jelikož funguje na stejném principu. Řešení je elegantnější díky využívání experimentálních funkcí HTML5. Ovšem má to jednu zásadní vadu.

Jedná se o experimentální projekt a od dev preview na RC se změnila skoro polovina API, takže projekty, které jsem napsal bych mohl napsat celé znovu a funkce, které mě tolik oslnily se nakonec velmi zkomplikovaly. Důvodem těchto změn je dle vývojářů hlavně výkon a stabilita.

A to mě přivádí k názoru, že možná bude existovat ještě elegantnější řešení.

#Direktivní enginy

Už jsme blízko!

Direktivní enginy pracují na principu pravidel. Šablona je definována v HTML, ovšem view-logic se definuje odděleně v JavaScriptu, což má i své výhody.

Narazil jsem na dva druhy direktivních enginů. Jedni mapují názvy vlastností modelu na třídy a nebo ID elementu. Ti druzí zase mapují CSS selektory na data modelu.

#Mapování modelu na třídy

HTML

1
2
3
4
<div id="container">
<span class="first_name"></span>
<span class="last_name"></span>
</div>

JS

1
2
3
4
$('#container').render({
first_name: "John",
last_name: "Doe"
});

Výsledek

1
2
3
4
<div id="container">
<span class="first_name">John</span>
<span class="last_name">Doe</span>
</div>

#Mapování selectoru na model

HTML

1
2
3
4
<div id="container">
<span class="first_name"></span>
<span class="last_name"></span>
</div>

JS

1
2
3
4
$('#container').render({
".first_name": "John",
".last_name": "Doe"
});

Výsledek

1
2
3
4
<div id="container">
<span class="first_name">John</span>
<span class="last_name">Doe</span>
</div>

Výhody:

  • DOM based
  • Jednoduché, rychlé a efektivní

Nevýhody:

  • View-logic mimo HTML (možná výhoda?)
  • Nepodporují podmínky
  • Málo flexibilní

Příklady:

#Shrnutí

Pro svůj projekt jsem hledal šablonovací engine, který bude jednoduchý, rychlý, v DOMu bude dělat jen nezbytné změny, půjde využít bez frameworku a bude flexibilní.

Potřeboval jsem podporu následujících funkcí:

  • Data-binding
  • Filtry
  • Podmínky
  • Iterace nad poli / objekty
  • Přiřazování podmíněných atributů (když je hodnota atributu false, atribut odeberu a naopak)
  • Optimálně vlastní funkce pro manipulaci s DOMem a práci s hodnotami

Bohužel po měsících občasného zkoušení jsem nenašel nic vyhovujícího. A tak jsem se rozhodl, že si engine zkusím napsat sám.

#MetaJS DOM Template

Jak jsem již zmiňoval v úvodu, pracuji na projektu META Platform a vytvářím knihovnu MetaJS, která je základem webového klienta. A v rámci ní jsem implementoval direktivní šablonovací engine dle vlastních představ.

#Co to umí?

  • Přiřazovat hodnoty (text, html, filtry)
  • Přiřazovat atributy (podmíněné, bez obsahu)
  • Přiřazovat třídy na základě podmínky
  • Přiřazovat vlastnosti objektu elementu
  • Podmínky se zachováním stavu elementu
  • Iterace nad poli a objekty bez překreslování a se zachováním stavu elementů
  • Zápis proměnných v objektovém zápisu (‚customer.first_name‘)
  • Vlastní funkce pro filtrace, porovnávání a přímou manipulaci s domem

Výhody:

  • DOM based
  • Rychlost
  • Flexibilita

Nevýhody:

  • View-logic mimo HTML (možná výhoda?)

Jak dlouho jsem ho psal?

Asi tak dvě noci (bez dokumentace)

Jak je knihovna velká?

Minified verze šablonovacího systému má k dnešnímu dni 12.5kB.

#Ukázka

HTML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div id="tpl">
<p class="project-name"></p>
<p class="budget"></p>
<p class="created"></p>
<p class="active">ACTIVE</p>
<p class="status"></p>
<p class="manager">
<span class="first-name"></span>
<span class="last-name"></span>
</p>

<ul class="tasks">
<li>
<span class="task"></span>
<span class="due-date"></span>
</li>
</ul>

<div class="note"></div>
</div>

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
var tpl = Meta.Template(document.getElementById('tpl'), {
".project_name": "name",
".budget": $__string("$ #{budget}"),
".created": $__date("d. m. Y", "created"),
".active": $__if("active"),
".status": $__fn(function(data){
switch(data.status){
case 0: return "Concept";
case 1: return "In progress";
case 2: return "Closed";
}
}),
".manager" $__with("manager", {
".first-name": "first_name",
".last-name": "last_name"
}),
"ul.tasks": $__repeat("tasks", {
".task": "task",
".due-date": $__date("d. m. Y", "due_date"),
"@": $__attrIf("completed", "complete")
}),
".note": $__html("note")
});

tpl({
name: "My project",
budget: 1000,
created: new Date(2015, 3, 20, 0, 0, 0, 0),
active: true,
status: 1,
manager: {
first_name: "John",
last_name: "Doe"
},
tasks: [
{
task: "Write concept",
due_date: new Date(2015, 3, 22, 0, 0, 0, 0),
complete: true
}, {
task: "Consult with customer",
due_date: new Date(2015, 3, 25, 0, 0, 0, 0),
complete: false
}
],
note: '<p>This will be fun!</p>'
});

Výsledek

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<div id="tpl">
<p class="project-name">My project</p>
<p class="budget">$ 1000</p>
<p class="created">20. 3. 2015</p>
<p class="active">ACTIVE</p>
<p class="status">In progress</p>
<p class="manager">
<span class="first-name">John</span>
<span class="last-name">Doe</span>
</p>

<ul class="tasks">
<li>
<span class="task" completed>Write concept</span>
<span class="due-date">22. 3. 2015</span>
</li>
<li>
<span class="task">Consult with customer</span>
<span class="due-date">25. 3. 2015</span>
</li>
</ul>

<div class="note">
<p>This will be fun!</p>
</div>
</div>

Uznávám, že zápis není zrovna nejkrásnější, ale člověk si rychle zvykne.

Koukněte na stránku projektu MetaJS. Naleznete tam podrobný popis, ukázky, návody a referenční příručku.

Budu rád za případnou kritiku a náměty.