Łańcuchy szablonowe

> Dodaj do ulubionych

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawią się w 6. edycji standardu ECMAScript, w skrócie ES6.

W zeszłym tygodniu obiecałem, że nieco zwolnimy. Powiedziałem, że po iteratorach i generatorach czas zająć się czymś prostszym. Czymś, czego zrozumienie nie będzie arcytrudne. Zobaczymy, czy uda mi się dotrzymać tej obietnicy, a póki co zacznijmy od czegoś łatwego.

Odwrotny apostrof – informacje podstawowe

W ES6 wprowadzony został nowy rodzaj literałów łańcuchowych – łańcuchy szablonowe. Wyglądają one jak zwykłe łańcuchy, z jedną różnicą – do ich zapisu zamiast zwykłego apostrofu (') bądź cudzysłowu (") stosuje się apostrof odwrotny (ang. backtick): `. W najprostszej postaci łańcuchy szablonowe to zwykłe łańcuchy:

context.fillText(`Ceci n'est pas une chaîne.`, x, y);

Nie bez powodu zostały one jednak nazwane łańcuchami szablonowymi, a nie „zwykłymi, nudnymi łańcuchami, tyle tylko, że z odwrotnym apostrofem”. Wprowadzają one bowiem do JavaScriptu możliwość interpolacji łańcuchów. Innymi słowy, w wygodny i przyjemny dla oka sposób możemy za ich pośrednictwem wstawić wartości JavaScript do łańcucha.

Można to wykorzystać na wiele sposobów, jednak moim ulubionym jest ten oto skromny komunikat o błędzie:

function authorize(user, action) {
  if (!user.hasPrivilege(action)) {
    throw new Error(
      `Użytkownik ${user.name} nie ma uprawnień do: ${action}.`);
  }
}

W powyższym przykładzie elementy ${user.name} i ${action} to zamienniki szablonowe (ang. template substitutions). JavaScript wstawi wartości zmiennych user.name i action do łańcucha wynikowego. Przykładową wiadomością może być więc coś w stylu: Użytkownik jorendorff nie ma uprawnień do: hokej. (To prawda. Nie mam licencji zawodniczej do gry w hokeja).

Póki co łańcuchy szablonowe wyglądają jak nieco lepsza składniowo wersja operatora +. Szczegóły sposobu ich użycia nie są raczej zaskakujące:

  • Kod w zamienniku szablonowym może być dowolnym wyrażeniem JavaScript, a więc dopuszczalne są wywołania funkcji, działania arytmetyczne itd. (Jeśli bardzo chcesz, możesz nawet zagnieździć jeden łańcuch szablonowy w innym, co nazywam zagnieżdżeniem szablonowym).
  • Jeśli któraś z wartości nie będzie łańcuchem, to zostanie wówczas przekonwertowana na łańcuch w standardowy sposób. Przykładowo, jeśli action jest obiektem to zostanie wywołana jego metoda .toString().
  • Jeśli musisz umieścić odwrotny apostrof wewnątrz łańcucha szablonowego, wówczas należy zapisać go przy pomocy odwrotnego ukośnika – `\`` oznacza to samo co "`".
  • W podobny sposób można wstawić do łańcucha szablonowego znaki ${. Nie chcę wiedzieć po co miałbyś to robić, jednak możesz tego dokonać, zapisując każdy z nich z odwrotnym ukośnikiem: `zapisz \${ lub $\{`.

W przeciwieństwie do zwykłych łańcuchów, łańcuchy szablonowe mogą zajmować więcej niż jedną linijkę:

$("#warning").html(`
  <h1>Uwaga!</h1>
  <p>Gra w hokeja bez pozwolenia skutkować będzie karą
  do ${maxPenalty} minut karnych.</p>
`);

Wszystkie białe znaki z łańcucha szablonowego, w tym nowe wiersze i wcięcia są dokładnie odwzorowane w danych wyjściowych.

OK. Złożyłem poprzednio obietnicę, zatem czuję się odpowiedzialny za stan zdrowia twoich szarych komórek. Dlatego ostrzegam: od tego momentu zajmiemy się nieco bardziej zawiłymi sprawami. Możesz teraz przerwać lekturę, zrobić sobie kawę i cieszyć się z błogiego, niezmąconego stanu twego umysłu. Naprawdę, jeśli chcesz zrezygnować to nie ma się czego wstydzić. Czy Lopes Gonçalves, udowodniwszy, że statki mogą przekroczyć równik i nie spaść z krawędzi ziemi ani nie zostać pożartymi przez morskie potwory, zbadał południową półkulę wzdłuż i wszerz? Nie. Wycofał się, popłynął do domu i zjadł dobry obiad. Nie ma to jak dobry obiad, prawda?

Łańcuchy szablonowe to przyszłość

Pomówmy o kilku rzeczach, których łańcuchy szablonowe nie robią.

  • Nie rozwiążą one za nas problemu znaków specjalnych. Aby uniknąć ryzyka ataku cross-site scripting, w dalszym ciągu należy traktować niezaufane dane z ostrożnością, tak jak w przypadku łączenia zwykłych łańcuchów.
  • Nie do końca wiadomo, jak współpracowałyby z biblioteką do obsługi internacjonalizacji (czyli biblioteką, która pomaga tworzyć kod mówiący do różnych użytkowników w różnych językach). Łańcuchy szablonowe nie obsługują typowego dla różnych języków formatowania liczb i dat, a zwłaszcza form liczby mnogiej.
  • Nie zastąpią bibliotek szablonów takich jak Mustache czy Nunjucks.

    Łańcuchy szablonowe nie mają wbudowanej składni pętli – służącej np. do budowania rzędów tabeli HTML na podstawie tablicy – ani nawet instrukcji warunkowych. (Owszem, można by w tym celu zastosować zagnieżdżenie szablonowe, ale raczej w ramach żartu).

ES6 wprowadza jeszcze jedno usprawnienie łańcuchów szablonowych, dzięki któremu programiści JS i projektanci bibliotek dysponują większymi możliwościami, by uporać się z wymienionymi ograniczeniami. Mowa o szablonach oznakowanych.

Ich składnia jest prosta – to zwykłe łańcuchy szablonowe z dodatkowym znacznikiem przed otwierającym odwrotnym apostrofem. W naszym pierwszym przykładzie posłużymy się znacznikiem SaferHTML, by poradzić sobie z pierwszym wymienionym ograniczeniem, czyli z brakiem możliwości automatycznego wstawiania znaków specjalnych.

Zwróć uwagę, że znacznik SaferHTML nie znajduje się w bibliotece standardowej ES6. Zaimplementujemy go samodzielnie w poniższym kodzie:

var message =
  SaferHTML`<p>${bonk.sender} właśnie wysłał ci podpuchę.</p>`;

Znacznikiem jest tutaj pojedynczy identyfikator SaferHTML, jednak mogłaby to być także własność, np. SaferHTML.escape, bądź nawet wywołanie metody, np. SaferHTML.escape({unicodeControlCharacters: false}). (Ściślej mówiąc, znacznikiem może być dowolne wyrażenie MemberExpression lub CallExpression z ES6).

Jak widzieliśmy, nieoznakowane łańcuchy szablonowe to skrócony sposób na łączenie łańcuchów. Oznakowane łańcuchy szablonowe umożliwiają natomiast skrótowe zapisanie wywołania funkcji.

Powyższy kod oznacza to samo co niniejszy fragment:

var message =
  SaferHTML(templateData, bonk.sender);

W kodzie tym templateData to niemodyfikowalna tablica wszystkich części łańcuchowych szablonu, utworzonych na nasze potrzeby przez JS. W tym przypadku tablica będzie mieć dwa elementy, ponieważ oznakowany szablon składa się z dwóch części łańcucha rozdzielonych zamiennikiem. A zatem tablica templateData będzie odpowiadała wynikowi wywołania metody Object.freeze(["<p>", " właśnie wysłał ci podpuchę.</p>"].

(W rzeczywistości tablica templateData ma jeszcze jedną własność. Nie będziemy z niej korzystać w tym artykule, jednak wyjaśnię o co chodzi: templateData.raw to inna tablica zawierająca wszystkie części łańcucha z oznakowanego szablonu, jednak w tym przypadku wyglądają one zupełnie tak, jak w kodzie źródłowym – np. sekwencja specjalna \n nie powodują przeniesienia do nowego wiersza, lecz pozostaje nieprzetworzona. Z takich surowych łańcuchów korzysta standardowy znacznik String.raw).

Dzięki temu funkcja SaferHTML ma dowolność w interpretacji zarówno łańcucha jak i zamienników na miliony różnych sposobów.

Nim przejdziesz do dalszej części artykułu, możesz spróbować wykombinować, co funkcja SaferHTML powinna robić, a następnie postarać się ją zaimplementować. W końcu to tylko funkcja. Wyniki swojej pracy możesz przetestować w konsoli WWW przeglądarki Firefox.

Oto jedno z możliwych rozwiązań (dostępne również w portalu GitHub).

function SaferHTML(templateData) {
  var s = templateData[0];
  for (var i = 1; i < arguments.length; i++) {
    var arg = String(arguments[i]);

    // użyj sekwencji znaków specjalnych w zamienniku
    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // nie używaj sekwencji specjalnej w szablonie
    s += templateData[i];
  }
  return s;
}

Z taką definicją oznakowany szablon SaferHTML`<p>${bonk.sender} właśnie wysłał ci podpuchę.</p>` można by rozszerzyć do łańcucha „<p>ES6<3er właśnie wysłał ci podpuchę. </p>”. Twoi użytkownicy będą bezpieczni nawet jeśli podpuchę wyśle im użytkownik o złowrogiej nazwie w stylu Hacker Steve <script>alert('xss');</script>. Cokolwiek to znaczy.

(Przy okazji: jeśli odnosisz wrażenie, że sposób w jaki funkcja korzysta z obiektu argumentów jest dosyć niezgrabny, śledź naszą stronę na bieżąco. W ES6 znajdziemy jeszcze jeden nowy składnik, który pewnie przypadnie ci do gustu).

Pojedynczy przykład nie wystarczy, by zaprezentować elastyczność, jaką dają szablony oznakowane. Przypomnijmy sobie naszą wcześniejszą listę ograniczeń łańcuchów szablonowych, by sprawdzić, co jeszcze możemy zrobić.

  • Łańcuchy szablonowe nie rozwiązują automatycznie problemu znaków specjalnych. Jak zdążyliśmy się już przekonać, możemy jednak rozwiązać ten problem samodzielnie za pomocą znacznika, choć jest jeszcze lepsze rozwiązanie.

    Pod względem bezpieczeństwa napisana przeze mnie funkcja SaferHTML jest dosyć słaba. W różnych miejsca kodu HTML znajdują się różne znaki specjalne, które trzeba umieścić w łańcuchu we właściwy sposób. SaferHTML nie rozwiązuje tego problemu w ogóle. Przy odrobinie wysiłku da się jednak napisać o wiele lepszą funkcję SaferHTML, która przetworzy takie fragmenty kodu HTML na łańcuch w tablicy templateData. Dzięki temu będzie wiedzieć, które zamienniki są w formacie HTML, które znajdują się w atrybutach elementów (a zatem muszą pojawić się bez apostrofu ' i cudzysłowu "), a także które znajdują się w łańcuchu zapytania będącego częścią adresu URL (w związku z czym w ich przypadku trzeba zwrócić uwagę nie na zapis znaków specjalnych URL, lecz HTML) itd. W przypadku każdego zamiennika znajdujące się w nim znaki specjalne zostaną odpowiednio zapisane.

    Pewnie wydaje się to nieprawdopodobne, biorąc pod uwagę, że parsowanie kodu HTML jest czasochłonne. Na szczęście łańcuchowe części oznakowanego szablonu nie są zmieniane podczas ponownego przetwarzania szablonu. Funkcja SaferHTML mogłaby przechowywać wyniki parsowania w pamięci podręcznej w celu przyspieszenia późniejszych wywołań. (Za pamięć podręczną mógłby posłużyć obiekt WeakMap, kolejny składnik ES6, który omówimy w przyszłości).

  • Łańcuchy szablonowe nie mają wbudowanych elementów do obsługi internacjonalizacji. Możemy je jednak dodać, korzystając ze znaczników. W jednym z wpisów na swoim blogu Jack Hsu prezentuje jak można się za to zabrać. Oto jeden z przykładów – na zachętę:
    i18n`Hello ${name}, you have ${amount}:c(CAD) in your bank account.`
    // => Hallo Bob, Sie haben 1.234,56 $CA auf Ihrem Bankkonto.

    Zwróć uwagę, że w powyższym przykładzie name i amount to elementy JavaScriptu, a nieznany fragment kodu, :c(CAD) , został umieszczony przez Jacka w łańcuchowej części szablonu. JavaScript jest oczywiście obsługiwany przez silnik JavaScriptu, a części łańcuchowe przez znacznik Jacka, i18n. Jak można dowiedzieć się z dokumentacji i18n, :c(CAD) oznacza, że zmienna amount określa ilość pieniędzy w dolarach kanadyjskich.

    I o to chodzi w szablonach oznakowanych.

  • Nie są one zamiennikami bibliotek szablonów Mustache czy Nunjucks, po części dlatego, że nie mają wbudowanej składni pętli ani instrukcji warunkowych. Teraz jednak widzisz już, jak można się z takimi ograniczeniami uporać, prawda? Jeśli w JS nie ma odpowiedniego elementu funkcjonalności, trzeba napisać znacznik, który go dostarczy.
    // czysto hipotetyczny język szablonowy oparty na
    // szablonach oznakowanych z ES6
    var libraryHtml = hashTemplate`
      <ul>
        #for book in ${myBooks}
          <li><i>#{book.title}</i> autorstwa #{book.author}</li>
        #end
      </ul>
    `;

To jednak nie koniec jeśli chodzi o elastyczność. Zwróć uwagę, że argumenty funkcji znacznika nie są automatyczne konwertowane na łańcuchy. Mogą mieć dowolną postać. To samo tyczy się wartości zwrotnej. Nawet same szablony oznakowane niekoniecznie muszą być łańcuchami! Można więc korzystać z własnych znaczników do tworzenia wyrażeń regularnych, drzew DOM, obrazów, obietnic reprezentujących całe procesy asynchroniczne, struktur danych JS, shaderów GL

Szablony oznakowane dają konstruktorom bibliotek możliwość napisania języków dziedzinowych o dużej funkcjonalności. Języki te mogą w ogóle nie przypominać JS, lecz mimo to będzie je można bez problemu osadzić w JS i będą one inteligentnie współpracować z resztą języka. Tak na poczekaniu nie przypominam sobie, aby coś takiego było dostępne w jakimkolwiek innym języku. Nie wiem, do czego doprowadzą nas szablony oznakowane, lecz przyszłość wydaje się świetlana.

Kiedy będę mógł zacząć korzystać z łańcuchów szablonowych

W przypadku kodu serwerowego łańcuchy szablonowe ES6 są obecnie obsługiwane w środowisku io.js.

W przeglądarkach łańcuchy szablonowe są natomiast obsługiwane przez Firefoksa w wersji 34 i nowszych. Zostały one w nim zaimplementowane zeszłego lata przez Gupthę Rajagopala w ramach projektu stażowego. Łańcuchy szablonowe obsługiwane są także w przeglądarce Chrome w wersji 41 i wzwyż, nie zaś w IE czy Safari. Na tę chwilę, by używać łańcuchów szablonowych w kodzie przeglądarkowym, trzeba skorzystać z kompilatora Babel lub Traceur. A już teraz łańcuchy szablonowe są dostępne w języku TypeScript!

Zaraz, zaraz – a co z językiem Markdown

Hmm?

Ach…dobre pytanie.

(Ta część artykułu właściwie nie dotyczy JavaScriptu. Możesz ją pominąć, jeśli nie korzystasz z języka Markdown).

W przypadku łańcuchów szablonowych zarówno w języku Markdown jak i JavaScript znak ` oznacza coś specjalnego. W Markdown jest to znak oddzielający fragmenty kodu wewnątrz tekstu śródliniowego.

I tu pojawia się mały problem. Jeśli zapiszesz poniższy kod w dokumencie Markdown:

Aby wyświetlić wiadomość, napisz `alert(`Witaj świecie!`)`.

to zostanie on wyświetlony następująco:

Aby wyświetlić wiadomość, napisz alert(Witaj świecie!).

Zauważ, że w tekście wynikowym nie pojawia się odwrotny apostrof. We wszystkich przypadkach został on zinterpretowany przez Markdown jako ogranicznik kodu i zastąpiony znacznikiem HTML.

Aby tego uniknąć, wykorzystamy mało znaną właściwość języka Markdown, która jest w nim dostępna od samego początku jego istnienia – do ograniczenia kodu w Markdown można użyć dwóch odwrotnych apostrofów. Oto przykład:

Aby wyświetlić wiadomość, napisz ``alert(`Witaj świecie!`)``.

Szczegóły dostępne są na platformie GitHub Gist – przykłady zapisane są w języku Markdown, zatem możesz prześledzić kod źródłowy.

W następnym odcinku

W kolejnym artykule przyjrzymy się dwóm elementom, z których programiści innych języków chętnie korzystają od wielu lat: pierwszy z nich to coś dla osób, którym zwykle brakuje argumentów, a drugi dla tych, którzy nigdy nie mają z ich wymyśleniem problemu. Mowa oczywiście o argumentach funkcji. Obydwa składniki przydadzą się tak naprawdę każdemu z nas.

Spojrzymy na nie z perspektywy człowieka, który zaimplementował je w Firefoksie. Odwiedzaj nas zatem regularnie, aby nie przegapić artykułu autorstwa Benjamina Petersona, dokładnie omawiającego parametry domyślne i parametry resztowe (ang. rest parameters) w ES6.

Autor: Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/05/es6-in-depth-template-strings-2/

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji CC BY-SA 3.0