Стандартизация клиентских шаблонов
Введение
Концепция шаблонов не нова для веб-разработки. На самом деле, серверные языки/движки шаблонов, такие как Django (Python), ERB/Haml (Ruby) и Smarty (PHP), существуют уже давно. Однако за последние пару лет мы увидели взрывной рост MVC-фреймворков. Все они немного отличаются, но большинство из них используют общий механизм для рендеринга своего презентационного слоя (он же представление): шаблоны.
Давайте посмотрим правде в глаза. Шаблоны – это здорово. Спросите у кого угодно. Даже его определение вызывает ощущение тепла и уюта:
"…не нужно создавать заново каждый раз…" Не знаю, как вы, а я люблю избегать лишней работы. Почему же тогда веб-платформе не хватает встроенной поддержки того, что явно волнует разработчиков?
Решением этой проблемы стала спецификация шаблонов HTML WhatWG. Он определяет новый элемент <template>, который описывает стандартный подход на основе DOM для клиентского создания шаблонов. Шаблоны позволяют объявлять фрагменты разметки, которые анализируются как HTML, не используются при загрузке страницы, но могут быть созданы позже во время выполнения. Как сказал Рафаэль Вайнштейн:
Они представляют собой место для размещения большого объема HTML-кода, который браузер не должен обрабатывать ни по какой причине.
Рафаэль Вайнштейн (Rafael Weinstein), автор спецификации
Распознавание объектов
Чтобы определить, поддерживается ли элемент <template>, создайте элемент DOM и проверьте, существует ли свойство .content:
function supportsTemplate() {
return 'content' in document.createElement('template');
}
if (supportsTemplate()) {
// Good to go!
} else {
// Use old templating techniques or libraries.
}
Объявление содержимого шаблона
Элемент HTML <template> представляет собой шаблон в разметке. Он содержит "содержимое шаблона", то есть инертные фрагменты клонируемой DOM.
Шаблоны можно сравнить со строительными лесами, которые вы можете использовать (и использовать повторно) на протяжении всего жизненного цикла вашего приложения.
Чтобы создать шаблонный контент, объявите разметку и заключите ее в элемент <template>:
<template id="mytemplate">
<img src="" alt="great image">
<div class="comment"></div>
</template>
Столбы
Обертывание контента в <template> дает нам несколько важных свойств.
Его содержимое неактивно до тех пор, пока не будет активировано. По сути, ваша разметка – это скрытая объектная модель документа, которая не отображается.
Контент в шаблоне не будет иметь побочных эффектов. Скрипт не выполняется, изображения не загружаются, аудио не воспроизводится, пока не будет использован шаблон.
Контент считается отсутствующим в документе. Если вы используете на главной странице
document.getElementById()илиquerySelector(), дочерние узлы шаблона не будут возвращены.Шаблоны можно размещать в любом месте внутри элементов
<head>,<body>или<frameset>. Они могут содержать любой тип контента, разрешенный в этих элементах. Обратите внимание, что "в любом месте" означает, что<template>можно безопасно использовать в местах, которые запрещает синтаксический анализатор HTML, за исключением дочерних элементов модели контента. Его также можно разместить как дочерний элемент<table>или<select>:
<table>
<tr>
<template id="cells-to-repeat">
<td>some content</td>
</template>
</tr>
</table>
Как активировать шаблон
Чтобы использовать шаблон, его нужно активировать. В противном случае его контент не будет отображаться.
Проще всего это сделать, создав глубокую копию .content с помощью document.importNode(). Свойство .content – это доступный только для чтения элемент DocumentFragment, содержащий внутренние элементы шаблона.
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);
После создания шаблона его контент становится доступным. В этом примере контент клонируется, выполняется запрос изображения и отрисовывается окончательная разметка.
Деморолики
Пример: инертный скрипт
Этот пример демонстрирует инертность содержимого шаблона. <script> работает только при нажатии кнопки, штампуя шаблон.
<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>
Пример создания Shadow DOM на основе шаблона
Большинство разработчиков прикрепляют Shadow DOM к хосту, задавая строку разметки для .innerHTML:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
Проблема с этим подходом заключается в том, что чем сложнее становится ваш Shadow DOM, тем больше конкатенации строк вы делаете. Она не масштабируется, всё быстро
запутывается, и дети начинают плакать. Именно так и появились межсайтовые скрипты. <template> спешит на помощь.
Более разумным решением будет работать с DOM напрямую, добавляя содержимое шаблона в теневой корень:
<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
Вот несколько подводных камней, с которыми я столкнулся при использовании <template>:
- Если вы используете modpagespeed, обратите внимание на эту ошибку. Шаблоны, определяющие встроенные элементы
<style scoped>, могут быть перемещены в заголовок с помощью правил перезаписи CSS PageSpeed. - Шаблон нельзя предварительно отрисовать, то есть вы не можете предварительно загрузить объекты, обработать JS, скачать исходный CSS и т. д. Это относится как к серверу, так и к клиенту. Шаблон отрисовывается только при публикации.
Будьте осторожны с вложенными шаблонами. Они работают не так, как вы могли бы ожидать. Пример:
<template> <ul> <template> <li>Stuff</li> </template> </ul> </template>Активация внешнего шаблона не приведет к активации внутренних. Это означает, что вложенные шаблоны требуют, чтобы их дочерние шаблоны также были активированы вручную.
Путь к стандарту
Давайте не будем забывать, откуда мы пришли. Путь к стандартизированным HTML-шаблонам был долгим. За эти годы мы придумали несколько довольно умных приемов для создания многоразовых шаблонов. Ниже приведены два распространенных, с которыми я столкнулся. В этой статье они приводятся для сравнения.
Способ 1. DOM за пределами экрана
Один из подходов, который используется уже давно, – создание DOM за пределами экрана и его скрытие с помощью атрибута hidden или display:none.
<div id="mytemplate" hidden>
<img src="logo.png">
<div class="comment"></div>
</div>
Однако у этого подхода есть ряд недостатков. Вот как это работает:
- Использование DOM. Браузер распознает DOM. и у него это хорошо получается. Мы можем легко его клонировать.
- Ничего не отображается. Добавление
hiddenпредотвращает показ блока. - Не инертный – несмотря на то, что наш контент скрыт, сетевой запрос для изображения все равно выполняется.
- Сложная настройка стилей и тем. Чтобы ограничить область действия стилей шаблоном, на странице встраивания необходимо добавить префикс
#mytemplateко всем правилам CSS. Это ненадежно, и нет никаких гарантий, что в будущем мы не столкнемся с конфликтами имен. Например, если на странице уже есть элемент с таким идентификатором, то у нас будут проблемы.
Способ 2. Перегрузка скрипта
Другой способ – перегрузка <script> и манипулирование его содержимым как строкой. Джон Резиг, вероятно, был первым, кто показал это в 2008 году со своей утилитой Micro Templating.
Сейчас их гораздо больше, включая новые, например handlebars.js.
Пример:
<script id="mytemplate" type="text/x-handlebars-template">
<img src="logo.png">
<div class="comment"></div>
</script>
Вот как это работает:
- Ничего не отображается – браузер не отображает этот блок, потому что по умолчанию для
<script>задано значениеdisplay:none. - Inert Браузер не выполняет синтаксический анализ содержимого скрипта как JS, поскольку его тип не text/javascript.
- Проблемы безопасности. Поощряется использование
.innerHTML. Синтаксический анализ строк, предоставленных пользователями, во время выполнения может легко привести к уязвимостям XSS.
Заключение
Помните, как jQuery упростил работу с DOM? В результате на платформу было добавлено querySelector()/querySelectorAll(). Очевидная победа, верно? Библиотека, которая популяризировала получение DOM с помощью селекторов CSS и стандартов, принятых позже. Это не всегда работает, но когда работает, это прекрасно.
Думаю, что <template> – это похожий случай. Она стандартизирует способ создания клиентских шаблонов, но, что более важно, устраняет необходимость в наших хаках 2008 года.
На мой взгляд, это делает процесс веб-разработки более понятным, удобным и функциональным.
Дополнительные ресурсы
- Спецификация WhatWG
- Введение в веб-компоненты
- <web>components</web> (видео) – фантастически подробная презентация от вашего покорного слуги.