Стандартизация шаблонов на стороне клиента
Введение
Концепция шаблонов не нова для веб-разработки. Фактически, серверные языки/движки шаблонов, такие как Django (Python), ERB/Haml (Ruby) и Smarty (PHP), существуют уже давно. Однако за последние пару лет мы стали свидетелями стремительного роста количества фреймворков MVC. Все они немного отличаются, но большинство из них имеют общую механику рендеринга презентационного уровня (так называемого представления): шаблоны.
Давайте посмотрим правде в глаза. Шаблоны фантастические. Давай, поспрашивай. Даже его определение заставляет чувствовать себя тепло и уютно:
«…не нужно каждый раз пересоздавать…» Не знаю, как вы, но я люблю избегать лишней работы. Почему же тогда веб-платформе не хватает встроенной поддержки того, что явно волнует разработчиков?
Ответом на этот вопрос является спецификация HTML-шаблонов WhatWG . Он определяет новый элемент <template>
, который описывает стандартный подход на основе DOM для создания шаблонов на стороне клиента. Шаблоны позволяют объявлять фрагменты разметки, которые анализируются как HTML, остаются неиспользованными при загрузке страницы, но могут быть созданы позже во время выполнения. Цитирую Рафаэля Вайнштейна :
Это место, куда можно поместить большой кусок HTML, с которым вы вообще не хотите, чтобы браузер связывался… по любой причине.
Рафаэль Вайнштейн (автор спецификации)
Обнаружение функций
Чтобы функция обнаружила <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>
дает нам несколько важных свойств.
Его содержимое фактически инертно до активации . По сути, ваша разметка является скрытым DOM и не отображается.
Любой контент внутри шаблона не будет иметь побочных эффектов. Скрипт не запускается, изображения не загружаются, звук не воспроизводится … пока не будет использован шаблон.
Содержимое считается отсутствующим в документе . Использование
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>
Пример: создание теневого DOM из шаблона
Большинство людей присоединяют Shadow DOM к хосту, устанавливая строку разметки в .innerHTML
:
<div id="host"></div>
<script>
var shadow = document.querySelector('#host').createShadowRoot();
shadow.innerHTML = '<span>Host node</span>';
</script>
Проблема с этим подходом заключается в том, что чем сложнее становится ваш Shadow DOM, тем больше конкатенации строк вы выполняете. Он не масштабируется, все быстро портится, и дети начинают плакать. Именно из-за этого подхода и родился XSS! <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>
Ошибки
Вот несколько ошибок, с которыми я столкнулся при использовании <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
блокирует отображение блока. - Не инертно — даже несмотря на то, что наш контент скрыт, сетевой запрос по-прежнему выполняется для изображения.
- Мучительные стили и темы — страница внедрения должна ставить перед всеми правилами CSS префикс
#mytemplate
, чтобы ограничить стили до шаблона. Это хрупкий подход, и нет никаких гарантий, что мы не столкнемся с конфликтами имен в будущем. Например, нас раздражает, если на странице внедрения уже есть элемент с таким идентификатором.
Способ 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
. - Инертный — браузер не анализирует содержимое скрипта как JS, поскольку для его типа установлено значение, отличное от «текст/javascript».
- Проблемы безопасности — рекомендуется использовать
.innerHTML
. Анализ строк во время выполнения предоставленных пользователем данных может легко привести к уязвимостям XSS.
Заключение
Помните, когда jQuery сделал работу с DOM невероятно простой? В результате на платформу были добавлены querySelector()
/ querySelectorAll()
. Очевидная победа, не так ли? Библиотека, популяризировавшая получение DOM с помощью селекторов и стандартов CSS, позже приняла ее. Это не всегда так работает, но мне нравится , когда это работает.
Я думаю, что <template>
— аналогичный случай. Он стандартизирует способ создания шаблонов на стороне клиента, но, что более важно, устраняет необходимость в хаках 2008 года. Я считаю, что сделать весь процесс веб-разработки более разумным, более удобным в обслуживании и более полнофункциональным — это всегда хорошо.
Дополнительные ресурсы
- Спецификация WhatWG
- Введение в веб-компоненты
- <web>компоненты</web> ( видео ) — фантастически полная презентация, искренне ваш.