Спустя какое время стало ясно, что основная идея Prototype вошла в противоречие с миром. Создатели браузеров ответили на возрождение Javascript добавлением новых API, многие из которых конфликтовали с реализацией Prototype.
— Sam Stephenson, создатель Prototype.js, You Are Not Your Code
Создатели браузеров поступают гармонично. Решение о новых API принимают с учётом текущих трендов в opensource сообществах. Так prototype.js способствовал появлению Array.prototype.forEach()
, map()
и т.д., jquery вдохновил разработчиков на HTMLElement.prototype.querySelector()
и querySelectorAll()
.
Код на стороне клиента становится сложнее и объёмнее. Появляются многочисленные фреймворки, которые помогают держать этот хаус под контролем. Backbone, ember, angular и другие создали, чтобы помочь писать чистый, модульный код. Фреймворки уровня приложения — это тренд. Его дух присутствует в JS среде уже какое-то время. Не удивительно, что создатели браузеров решили обратить на него внимание.
Web Components — это черновик набора стандартов. Его предложили и активно продвигают ребята из Google, но инициативу уже поддержали в Mozilla. И Microsoft. Шучу, Microsoft вообще не при делах. Мнения в комьюнити противоречивые (судя по комментариям, статьям и т.д.).
Основная идея в том, чтобы позволить программистам создавать “виджеты”. Фрагменты приложения, которые изолированы от документа, в который они встраиваются. Использовать виджет возможно как с помощью HTML, так и с помощью JS API.
Я пару недель игрался с новыми API и уверен, что в каком-то виде, рано или поздно эти возможности будут в браузерах. Хотя их реализация в Chrome Canary иногда ставила меня в тупик (меня, и сам Chrome Canary), Web Components кажется тем инструментом, которого мне не хватало.
Стандарт Web Components состоит из следующих частей:
- Templates
Фрагменты HTML, которые программист собирается использовать в будущем.
Содержимое тегов
<template>
парсится браузером, но не вызывает выполнение скриптов и загрузку дополнительных ресурсов (изображений, аудио…) пока мы не вставим его в документ. - Shadow DOM
Инструмент инкапсуляции HTML.
Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным. Отличный пример — элементы
<audio>
и<video>
. В коде мы размещаем один тег, а браузер отображает несколько элементов (слайдеры, кнопки, окно проигрывателя). В Chrome эти и некоторые другие элементы используют
Shadow DOM. - Custom Elements
Custom Elements позволяют создавать и определять API собственных HTML элементов. Когда-нибудь мечтали о том, чтобы в HTML был тег
<menu>
или<user-info>
? - Imports
Импорт фрагментов разметки из других файлов.
В Web Components больше частей и маленьких деталей. Некоторые я ещё буду
упоминать, до каких-то пока не добрался.
Templates
Концепция шаблонов проста. Хотя под этим словом в стандарте подразумевается не то, к чему мы привыкли.
В современных web-фреймворках шаблоны — это строки или фрагменты DOM, в которые мы подставляем данные перед тем как показать пользователю.
В web components шаблоны — это фрагменты DOM. Браузер парсит их содержимое, но не выполняет до тех пор, пока мы не вставим его в документ. То есть браузер не будет загружать картинки, аудио и видео, не будет выполнять скрипты.
К примеру, такой фрагмент разметки в документе не вызовет загрузку изображения.
<template id="tmpl-user">
<h2 class="name">Иван Иваныч</h2>
<img src="photo.jpg">
</template>
Хотя браузер распарсит содержимое <template>
. Добраться до него можно с помощью js:
var tmpl = document.querySelector('#tmpl-user');
// содержимое <template>
var content = tmpl.content;
var imported;
// Подставляю данные в шаблон:
content.querySelector('.name').innerText = 'Акакий';
// Чтобы скопировать содержимое и сделать его частью документа,
// используйте document.importNode()
//
// Это заставит браузер `выполнить` содержимое шаблона,
// в данном случае начнёт грузится картинка `photo.jpg`
imported = document.importNode(content);
// Результат импорта вставляю в документ:
document.body.appendChild(imported);
Пример работы шаблонов можно посмотреть здесь.
Все примеры в статье следует смотреть в Chrome Canary со включенными флагами:
- Experimental Web Platform features
- Enable HTML Imports
Для Чего?
На данный момент существует три способа работы с шаблонами:
- Добавить шаблон в скрытый элемент на странице. Когда он будет нужен,
скопировать и подставить данные:<div hidden data-template="my-template"> <p>Template Content</p> <img></img> </div>
Минусы такого подхода в том, что браузер попытается “выполнить” код шаблона. То есть загрузить картинки, выполнить код скриптов и т.д.
- Получать содержимое шаблона в виде строки (запросить AJAXом или из
<script type="x-template">
).<sctipt type="x-template" data-template="my-template"> <p>Template Content</p> <img src="{{ image }}"></img> </script>
Минус в том, что приходится работать со строками. Это создаёт угрозу XSS, нужно уделять дополнительное внимание экранированию.
- Компилируемые шаблоны, вроде hogan.js, также работают со строками. Значит имеют тот же изъян, что и шаблоны второго типа.
У <template>
нет этих недостатков. Мы работаем с DOM, не со строками. Когда выполнять код, также решать нам.
Shadow DOM
Инкапсуляция. Этого в работе с разметкой мне не хватало больше всего. Что такое Shadow DOM и как он работает проще понять на примере.
Когда мы используем html5 элемент <audio>
код выглядит примерно так:
<audio controls src="kings-speech.wav"></audio>
Но на странице это выглядит так:
Мы видим множество контролов, прогресбар, индикатор длины аудио. Откуда эти элементы и как до них добраться? Ответ — они находятся в Shadow Tree элемента. Мы можем даже увидеть их в DevTools, если захотим.
Чтобы Chrome в DevTools отображал содержимое Shadow DOM, в настройках DevTools, вкладка General, раздел Elements ставим галочку Show Shadow DOM.
Содержимое Shadow DOM тега <audio>
в DevTools:
Теория Shadow DOM
Shadow Tree — это поддерево, которое прикреплено к элементу в документе. Элемент в этом случае называется shadow host, на его месте браузер показывает содержимое shadow tree, игнорируя содержимое самого элемента.
Именно это происходит с <audio>
тегом в примере выше, на его месте браузер рендерит содержимое shadow tree.
Фишка shadow dom в том, что стили, определённые в нём с помощью <style>
, не распространяются на родительский документ. Также у нас есть возможность ограничить влияние стилей родительского документа на содержимое shadow tree. Об этом позже.
Посадить теневое дерево
Shadow DOM API позволяет пользователям самостоятельно создавать и
манипулировать содержимым shadow tree.
<div class="shadow-host">
Этот текст пользователь не увидит.
</div>
<script>
var shadowHost = document.querySelector('.shadow-host');
var shadowRoot = shadowHost.createShadowRoot();
shadowRoot.innerText = 'Он увидит этот текст.'
</script>
Результат:
Проекции, тег <content>
Проекция — это использование содержимого хоста в shadow tree. Для этого в стандарте есть тег <content>
.
Важно, что
<content>
проецирует содержимое хоста, а не переносит его из хоста в shadow tree. Потомки хоста остаются на своём месте, на них распространяются стили документа (а не shadow tree).<content>
это своего рода окно между мирами.
<template id="content-tag">
<p>
Это содержимое
<strong>shadow tree</strong>.
</p>
<p>
Ниже проекция содержимого
<strong>shadow host</strong>:
</p>
<content></content>
</template>
<div class="shadow-host">
<h1 class="name">Варлам</h1>
<img src="varlam.png">
<p class="description">Бодрый Пёс</p>
</div>
<script>
var host = document.querySelector('.shadow-host'),
template = document.querySelector('#content-tag'),
shadow = host.createShadowRoot();
shadow.appendChild(template.content);
</script>
Результат:
Стили в Shadow DOM
Инкапсуляция стилей — основная фишка shadow DOM. Стили, которые определёны в shadow tree имеют силу только внутри этого дерева.
Досадная особенность — использовать в shadow tree внешние css файлы нельзя. Надеюсь, это поправят в будущем.
<template id="color-green">
<style>
div { background-color: green; }
</style>
<div>зелёный</div>
</template>
<div class="shadow-host"></div>
<script>
var host = document.querySelector('.shadow-host'),
template = document.querySelector('#color-green'),
shadow = host.createShadowRoot();
shadow.appendChild(template.content);
</script>
Зелёный фон в примере получит только `<div>` внутри shadow tree. То
есть стили «не вытекут» в основной документ.
Результат:
Наследуемые стили
По-умолчанию наследуемые стили, такие как color
, font-size
и другие, влияют на содержимое shadow tree. Мы избежим этого, если установим shadowRoot.resetStyleInheritance = true
.
<template id="reset">
<p>В этом примере шрифты сброшены.</p>
<content></content>
</template>
<div class="shadow-host">
<p>Host Content</p>
</div>
<script>
var host = document.querySelector('.shadow-host'),
template = document.querySelector('#reset'),
shadow = host.createShadowRoot();
shadow.resetStyleInheritance = true;
shadow.appendChild(template.content);
</script>
Результат:
Авторские стили
Чтобы стили документа влияли на то, как выглядит shadow tree, используйте свойство applyAuthorStyles
.
<template id="no-author-st">
<div class="border">div.border</div>
</template>
<style>
/* В стилях документа */
.border {
border: 3px dashed red;
}
</style>
<div class="shadow-host"></div>
<script>
var host = document.querySelector('.shadow-host'),
template = document.querySelector('#no-author-st'),
shadow = host.createShadowRoot();
shadow.applyAuthorStyles = false; // значение по-умолчанию
shadow.appendChild(template.content);
</script>
Изменяя значение applyAuthorStyles
, получаем разный результат:
applyAuthorStyles = false
applyAuthorStyles = true
ссылка на пример, applyAuthorStyles=false
ссылка на пример, applyAuthorStyles=true
Селекторы ^ и ^^
Инкапсуляция это здорово, но если мы всё таки хотим добраться до shadow tree и изменить его представление из стилей документа, нам понадобится молоток. И кувалда.
Селектор div ^ p
аналогичен div p
с тем исключением, что он пересекает одну теневую границу (Shadow Boundary).
Селектор div ^^ p
аналогичен предыдущему, но пересекает ЛЮБОЕ количество теневых границ.
<template id="hat">
<p class="shadow-p">
Это красный текст.
</p>
</template>
<style>
/* В стилях документа */
.shadow-host ^ p.shadow-p {
color: red;
}
</style>
<div class="shadow-host"></div>
<script>
var host = document.querySelector('.shadow-host'),
template = document.querySelector('#hat'),
shadow = host.createShadowRoot();
shadow.appendChild(template.content);
</script>
Результат:
Зачем нужен Shadow DOM?
Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным.
Возможное применение — альтернатива iframe
. Последний чересчур изолирован. Чтобы взаимодействовать с внешним документом, приходится изобретать безумные способы передачи сообщений. Изменение внешнего представления с помощью css просто невозможно.
В отличие от iframe
, Shadow DOM — это часть вашего документа. И хотя shadow tree в некоторой степени изолировано, при желании мы можем изменить его представление с помощью стилей, или расковырять скриптом.
Custom Elements
Custom Elements — это инструмент создания своих HTML элементов. API этой части Web Components выглядит зрело и напоминает директивы
Angular. В сочетании с Shadow DOM и шаблонами, кастомные элементы дают возможность создавать полноценные виджеты вроде <audio>
, <video>
или <input type="date">
.
Чтобы избежать конфликтов, согласно стандарту, кастомные элементы должны содержать дефис в своём названии. По-умолчанию они наследуют HTMLElement
. Таким образом, когда браузер натыкается на разметку вида <my-super-element>
, он парсит его как HTMLElement
. В случае <mysuperelement>
, результат будет HTMLUnknownElement
.
<dog></dog>
<x-dog></x-dog>
<dl>
<dt>dog type</dt>
<dd id="dog-type"></dd>
<dt>x-dog type</dt>
<dd id="x-dog-type"></dd>
</dl>
<script>
var dog = document.querySelector('dog'),
dogType = document.querySelector('#dog-type'),
xDog = document.querySelector('x-dog'),
xDogType = document.querySelector('#x-dog-type');
dogType.innerText = Object.prototype.toString.apply(dog);
xDogType.innerText = Object.prototype.toString.apply(xDog);
</script>
Результат:
API кастомного элемента
Мы можем определять свойства и методы у нашего элемента. Такие, как метод play()
у элемента <audio>
.
В жизненный цикл (lifecycle) элемента входит 4 события, на каждое мы можем повесить callback:
- created — создан инстанс элемента
- attached — элемент вставлен в DOM
- detached — элемент удалён из DOM
- attributeChanged — атрибут элемента добавлен, удалён или изменён
Алгоритм создания кастомного элемента выглядит так:
- Создаём прототип элемента.
Прототип должен наследовать
HTMLElement
или его наследника,
напримерHTMLButtonElement
:var myElementProto = Object.create(HTMLElement.prototype, { // API элемента и его lifecycle callbacks });
- Регистрируем элемент в DOM с помощью
document.registerElement()
:var myElement = document.registerElement('my-element', { prototype: myElementProto });
<x-cat></x-cat>
<div>
<strong>Cat's life:</strong>
<pre id="cats-life"></pre>
</div>
<script>
var life = document.querySelector('#cats-life'),
xCatProto = Object.create(HTMLElement.prototype, {
nickName: 'Cake', writable: true
});
xCatProto.meow = function () {
life.innerText += this.nickName + ': meown';
};
xCatProto.createdCallback = function () {
life.innerText += 'createdn';
};
xCatProto.attachedCallback = function () {
life.innerText += 'attachedn';
};
xCatProto.detachedCallback = function () {
life.innerText += 'detachedn';
};
xCatProto.attributeChangedCallback = function (name, oldVal, newVal) {
life.innerText += (
'Attribute ' + name +
' changed from ' + oldVal +
' to ' + newVal + 'n');
};
document.registerElement('x-cat', { prototype: xCatProto });
document.querySelector('x-cat').setAttribute('friend', 'Fiona');
document.querySelector('x-cat').meow();
document.querySelector('x-cat').nickName = 'Caaaaake';
document.querySelector('x-cat').meow();
document.querySelector('x-cat').remove();
</script>
Результат:
Зачем нужны Custom Elements?
Custom Elements это шаг к семантической разметке. Программистам важно создавать абстракции. Семантически-нейтральные <div>
или <ul>
хорошо подходят для низкоуровневой вёрстки, тогда как Custom Elements позволят писать модульный, удобочитаемый код на высоком уровне.
Shadow DOM и Custom Elements дают возможность создавать независимые от контекста виджеты, с удобным API и инкапсулированным внутренним представлением.
HTML Imports
Импорты — простое API, которому давно место в браузерах. Они дают возможность вставлять в документ фрагменты разметки из других файлов.
<link rel="import" href="widget.html">
<sctipt>
var link = document.querySelector('link[rel="import"]');
// Доступ к импортированному документу происходит с помощью свойства
// *import*.
var importedContent = link.import;
importedContent.querySelector('article');
</sctipt>
Object.observe()
Ещё одно приятное дополнение и часть Web Components (кажется), это API для отслеживания изменений объекта Object.observe()
.
Этот метод доступен в Chrome, если включить флаг Experimental Web Platform features.
var o = {};
Object.observe(o, function (changes) {
changes.forEach(function (change) {
// change.object содержит изменённую версию объекта
console.log('property:', change.name, 'type:', change.type);
});
});
o.x = 1 // property: x type: add
o.x = 2 // property: x type: update
delete o.x // property: x type: delete
При изменении объекта o
вызывается callback, в него передаётся массив
свойств, которые изменились.
TODO widget
Согласно древней традиции, вооружившись этими знаниями, я решил
сделать простой TODO-виджет. В нём используются части Web Components, о которых я рассказывал в статье.
Добавление виджета на страницу сводится к одному импорту и одному тегу в теле документа.
<html>
<head>
<link rel="import" href="todo.html">
</head>
<body>
<x-todo></x-todo>
</body>
</html>
<script>
// JS API виджета:
var xTodo = document.querySelector('x-todo');
xTodo.items(); // список задач
xTodo.addItem(taskText); // добавить
xTodo.removeItem(taskIndex); // удалить
</script>
Результат:
Заключение
С развитием html5 браузеры стали нативно поддерживать новые медиа-форматы. Также появились элементы вроде <canvas>
. Теперь у нас огромное количество возможностей для создания интерактивных приложений на клиенте. Этот стандарт также представил элементы <article>
, <header>
, и другие. Разметка стала “иметь смысл”, приобрела семантику.
На мой взгляд, Web Components — это следующий шаг. Разработчики смогут создавать интерактивные виджеты. Их легко поддерживать, переиспользовать, интегрировать.
Код страницы не будет выглядеть как набор “блоков”, “параграфов” и “списков”. Мы сможем использовать элементы вроде “меню”, “новостная лента”, “чат”.
Конечно, стандарт сыроват. К примеру, импорты работают не так хорошо, как шаблоны. Их использование рушило Chrome время от времени. Но объём нововведений поражает. Даже часть этих возможностей способна облегчить жизнь web-разработчикам. А некоторые заметно ускорят работу существующих фреймворков.
Некоторые части Web Components можно использовать уже сейчас с помощью полифилов. Polymer Project — это полноценный фреймворк уровня приложения, который использует Web Components.
ƒ
Ссылки
- Web Components Intro, W3C Working Draft
- Shadow DOM, W3C Editor’s Draft
- Примеры к этой статье
- Bug 811542 — Implement Web Components, Bugzilla@Mozilla
Eric Bidelman, серия статей и видео о Web Components:
- HTML’s New Template Tag: standardizing client-side
templating - Shadow DOM 101
- Shadow DOM 201: CSS and Styling
- Shadow DOM 301: Advanced Concepts & DOM APIs
- Custom Elements: defining new elements in HTML
- HTML Imports: #include for the web
- <web>components</web> (видео)
Автор: filipovskii_off