Nowy tag szablonu HTML

Standaryzacja szablonów po stronie klienta

Wprowadzenie

Koncepcja szablonów nie jest nowa w tworzeniu stron internetowych. W rzeczywistości języki i silniki szablonów po stronie serwera, takie jak Django (Python), ERB/Haml (Ruby) i Smarty (PHP), istnieją już od dawna. W ciągu ostatnich kilku lat nastąpił jednak gwałtowny wzrost liczby platform MVC. Wszystkie nieco się różnią, ale większość z nich korzysta z tego samego mechanizmu renderowania warstwy prezentacji (czyli widoku): szablonów.

Spójrzmy prawdzie w oczy. Szablony są fantastyczne. Śmiało, pytaj. Nawet jego definicja sprawia, że czujemy się ciepło i przytulnie:

„…nie trzeba tworzyć za każdym razem…”. Nie wiem, jak Wy, ale ja lubię unikać dodatkowej pracy. Dlaczego więc platforma internetowa nie obsługuje natywnie czegoś, na czym deweloperom wyraźnie zależy?

Odpowiedzią jest specyfikacja szablonów HTML WhatWG. Definiuje on nowy element <template>, który opisuje standardowe podejście oparte na DOM do tworzenia szablonów po stronie klienta. Szablony umożliwiają deklarowanie fragmentów znaczników, które są analizowane jako kod HTML, nie są używane podczas wczytywania strony, ale mogą zostać utworzone później w czasie działania. Cytując Rafaela Weinsteina:

Są miejscem, w którym można umieścić duży fragment kodu HTML, którego przeglądarka nie powinna w żaden sposób modyfikować.

Rafael Weinstein (autor specyfikacji)

Wykrywanie funkcji

Aby wykryć funkcję <template>, utwórz element DOM i sprawdź, czy istnieje właściwość .content:

function supportsTemplate() {
    return 'content' in document.createElement('template');
}

if (supportsTemplate()) {
    // Good to go!
} else {
    // Use old templating techniques or libraries.
}

Deklarowanie treści szablonu

Element HTML <template> reprezentuje szablon w znacznikach. Zawiera „treść szablonu”, czyli zasadniczo nieaktywne fragmenty interfejsu DOM, które można klonować. Szablony to elementy, których możesz używać (i używać wielokrotnie) przez cały okres istnienia aplikacji.

Aby utworzyć treść opartą na szablonie, zadeklaruj znaczniki i umieść je w elemencie <template>:

<template id="mytemplate">
    <img src="" alt="great image">
    <div class="comment"></div>
</template>

Filary

Umieszczenie treści w elemencie <template> daje nam kilka ważnych właściwości.

  1. Jego zawartość jest nieaktywna, dopóki nie zostanie aktywowana. Zasadniczo znaczniki są ukrytym DOM i nie są renderowane.

  2. Treści w szablonie nie będą miały efektów ubocznych. Skrypt nie jest uruchamiany, obrazy się nie wczytują, a dźwięk nie jest odtwarzany… dopóki szablon nie zostanie użyty.

  3. Treści nie są uznawane za część dokumentu. Użycie document.getElementById() lub querySelector() na stronie głównej nie spowoduje zwrócenia węzłów podrzędnych szablonu.

  4. Szablony mogą być umieszczane w dowolnym miejscu w elementach <head>, <body> lub <frameset> i mogą zawierać dowolny typ treści dozwolony w tych elementach. Pamiętaj, że „wszędzie” oznacza, że <template> można bezpiecznie używać w miejscach, które są niedozwolone przez parser HTML… z wyjątkiem elementów podrzędnych modelu treści. Można go też umieścić jako element podrzędny tagu <table> lub <select>:

<table>
  <tr>
    <template id="cells-to-repeat">
      <td>some content</td>
    </template>
  </tr>
</table>

Aktywowanie szablonu

Aby użyć szablonu, musisz go aktywować. W przeciwnym razie jego zawartość nigdy nie zostanie wyrenderowana. Najprostszym sposobem jest utworzenie głębokiej kopii .content za pomocą document.importNode(). Właściwość .content to właściwość DocumentFragment tylko do odczytu, która zawiera treść szablonu.

var t = document.querySelector('#mytemplate');
// Populate the src at runtime.
t.content.querySelector('img').src = 'logo.png';

var clone = document.importNode(t.content, true);
document.body.appendChild(clone);

Po utworzeniu szablonu jego zawartość „staje się aktywna”. W tym konkretnym przykładzie treść jest klonowana, wysyłane jest żądanie obrazu i renderowane są ostateczne znaczniki.

Prezentacje

Przykład: skrypt obojętny

Ten przykład pokazuje bezczynność zawartości szablonu. <script> działa tylko wtedy, gdy przycisk jest naciśnięty, i wybija szablon.

<button onclick="useIt()">Use me</button>
<div id="container"></div>
<script>
  function useIt() {
    var content = document.querySelector('template').content;
    // Update something in the template DOM.
    var span = content.querySelector('span');
    span.textContent = parseInt(span.textContent) + 1;
    document.querySelector('#container').appendChild(
      document.importNode(content, true)
    );
  }
</script>

<template>
  <div>Template used: <span>0</span></div>
  <script>alert('Thanks!')</script>
</template>

Przykład: tworzenie Shadow DOM na podstawie szablonu

Większość osób dołącza Shadow DOM do hosta, ustawiając ciąg znaczników na .innerHTML:

<div id="host"></div>
<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.innerHTML = '<span>Host node</span>';
</script>

Problem z tym podejściem polega na tym, że im bardziej złożony jest Shadow DOM, tym więcej konkatenacji ciągów znaków trzeba wykonać. Nie jest to skalowalne, szybko robi się bałagan, a dzieci zaczynają płakać. W ten sposób powstały też ataki XSS. <template> na ratunek.

Bardziej rozsądnym rozwiązaniem byłoby bezpośrednie korzystanie z DOM przez dołączenie zawartości szablonu do korzenia shadow:

<template>
<style>
  :host {
    background: #f8f8f8;
    padding: 10px;
    transition: all 400ms ease-in-out;
    box-sizing: border-box;
    border-radius: 5px;
    width: 450px;
    max-width: 100%;
  }
  :host(:hover) {
    background: #ccc;
  }
  div {
    position: relative;
  }
  header {
    padding: 5px;
    border-bottom: 1px solid #aaa;
  }
  h3 {
    margin: 0 !important;
  }
  textarea {
    font-family: inherit;
    width: 100%;
    height: 100px;
    box-sizing: border-box;
    border: 1px solid #aaa;
  }
  footer {
    position: absolute;
    bottom: 10px;
    right: 5px;
  }
</style>
<div>
  <header>
    <h3>Add a Comment
  </header>
  <content select="p"></content>
  <textarea></textarea>
  <footer>
    <button>Post</button>
  </footer>
</div>
</template>

<div id="host">
  <p>Instructions go here</p>
</div>

<script>
  var shadow = document.querySelector('#host').createShadowRoot();
  shadow.appendChild(document.querySelector('template').content);
</script>

Gotchas

Oto kilka pułapek, na które natknąłem się podczas korzystania z <template>:

  • Jeśli używasz modpagespeed, uważaj na ten błąd. Szablony, które definiują wbudowane <style scoped>, można przenieść do nagłówka za pomocą reguł przepisywania CSS w PageSpeed.
  • Nie ma możliwości „wstępnego renderowania” szablonu, co oznacza, że nie można wstępnie wczytywać zasobów, przetwarzać JS, pobierać początkowego CSS itp. Dotyczy to zarówno serwera, jak i klienta. Szablon jest renderowany tylko wtedy, gdy jest aktywny.
  • Uważaj na zagnieżdżone szablony. Nie działają one zgodnie z oczekiwaniami. Na przykład:

    <template>
      <ul>
        <template>
          <li>Stuff</li>
        </template>
      </ul>
    </template>
    

    Aktywowanie szablonu zewnętrznego nie spowoduje aktywowania szablonów wewnętrznych. Oznacza to, że szablony zagnieżdżone wymagają ręcznej aktywacji szablonów podrzędnych.

Droga do standardu

Nie zapominajmy, skąd pochodzimy. Droga do szablonów HTML opartych na standardach była długa. Przez lata opracowaliśmy kilka sprytnych sztuczek, które pozwalają tworzyć szablony wielokrotnego użytku. Poniżej przedstawiam 2 typowe scenariusze, z którymi się spotkałem. Uwzględniam je w tym artykule dla porównania.

Metoda 1. DOM poza ekranem

Jednym ze sposobów, który jest stosowany od dawna, jest utworzenie modelu DOM „poza ekranem” i ukrycie go za pomocą atrybutu hidden lub display:none.

<div id="mytemplate" hidden>
  <img src="logo.png">
  <div class="comment"></div>
</div>

Ta technika działa, ale ma kilka wad. Podsumowanie tej techniki:

  • Korzystanie z DOM – przeglądarka zna DOM. Jest w tym dobry. Możemy go łatwo sklonować.
  • Nic nie jest renderowane – dodanie hidden uniemożliwia wyświetlanie bloku.
  • Nie jest obojętny – mimo że nasze treści są ukryte, nadal wysyłane jest żądanie sieciowe dotyczące obrazu.
  • Uciążliwe stylizowanie i tworzenie motywów – strona osadzająca musi poprzedzać wszystkie reguły CSS ciągiem #mytemplate, aby ograniczyć zakres stylów do szablonu. To rozwiązanie jest niestabilne i nie gwarantuje, że w przyszłości nie wystąpią kolizje nazw. Na przykład jeśli strona, na której osadzono element, ma już element o tym identyfikatorze, wystąpi błąd.

Metoda 2. Skrypt przeciążający

Inną techniką jest przeciążanie <script> i manipulowanie jego zawartością jako ciągiem znaków. John Resig był prawdopodobnie pierwszym, który pokazał to w 2008 roku za pomocą swojego narzędzia Micro Templating. Teraz jest ich o wiele więcej, w tym kilka nowych, takich jak handlebars.js.

Na przykład:

<script id="mytemplate" type="text/x-handlebars-template">
  <img src="logo.png">
  <div class="comment"></div>
</script>

Podsumowanie tej techniki:

  • Nic nie jest renderowane – przeglądarka nie renderuje tego bloku, ponieważ domyślnie <script> to display:none.
  • Inert – przeglądarka nie analizuje zawartości skryptu jako JS, ponieważ jego typ jest ustawiony na inny niż „text/javascript”.
  • Problemy dotyczące bezpieczeństwa – zachęca do korzystania z .innerHTML. Analizowanie ciągów znaków dostarczonych przez użytkownika w czasie działania może łatwo prowadzić do luk w zabezpieczeniach XSS.

Podsumowanie

Pamiętasz, jak jQuery uprościło pracę z DOM? W rezultacie na platformie pojawiło się querySelector()/querySelectorAll(). To oczywista wygrana, prawda? Biblioteka spopularyzowała pobieranie DOM za pomocą selektorów CSS, a później standardy to przyjęły. Nie zawsze tak jest, ale uwielbiam, gdy tak się dzieje.

Myślę, że <template> to podobny przypadek. Standaryzuje sposób tworzenia szablonów po stronie klienta, ale co ważniejsze, eliminuje potrzebę stosowania naszych hacków z 2008 roku. Moim zdaniem ułatwienie całego procesu tworzenia stron internetowych, zwiększenie możliwości ich utrzymania i rozszerzenie funkcjonalności to zawsze dobra rzecz.

Dodatkowe materiały