![](http://habr.habrastorage.org/post_images/4f6/ba9/2dc/4f6ba92dc07661849c8bae06645315f1.png)
Спустя какое время стало ясно, что основная идея 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 + ': meow\n'; }; xCatProto.createdCallback = function () { life.innerText += 'created\n'; }; xCatProto.attachedCallback = function () { life.innerText += 'attached\n'; }; xCatProto.detachedCallback = function () { life.innerText += 'detached\n'; }; 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:
ссылка на оригинал статьи http://habrahabr.ru/post/210058/
Добавить комментарий