...

четверг, 23 января 2014 г.

[Из песочницы] Web Components — будущее Web



Спустя какое время стало ясно, что основная идея 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






Для Чего?




На данный момент существует три способа работы с шаблонами:

  1. Добавить шаблон в скрытый элемент на странице. Когда он будет нужен,

    скопировать и подставить данные:

    <div hidden data-template="my-template">
    <p>Template Content</p>
    <img></img>
    </div>




    Минусы такого подхода в том, что браузер попытается “выполнить” код шаблона. То есть загрузить картинки, выполнить код скриптов и т.д.

  2. Получать содержимое шаблона в виде строки (запросить AJAXом или из <script type="x-template">).

    <sctipt type="x-template" data-template="my-template">
    <p>Template Content</p>
    <img src="{{ image }}"></img>
    </script>




    Минус в том, что приходится работать со строками. Это создаёт угрозу XSS, нужно уделять дополнительное внимание экранированию.

  3. Компилируемые шаблоны, вроде hogan.js, также работают со строками. Значит имеют тот же изъян, что и шаблоны второго типа.




У <template> нет этих недостатков. Мы работаем с DOM, не со строками. Когда выполнять код, также решать нам.

Shadow DOM




Инкапсуляция. Этого в работе с разметкой мне не хватало больше всего. Что такое Shadow DOM и как он работает проще понять на примере.

Когда мы используем html5 элемент <audio> код выглядит примерно так:



<audio controls src="kings-speech.wav"></audio>


Но на странице это выглядит так:


audio element


Мы видим множество контролов, прогресбар, индикатор длины аудио. Откуда эти элементы и как до них добраться? Ответ — они находятся в Shadow Tree элемента. Мы можем даже увидеть их в DevTools, если захотим.



Чтобы Chrome в DevTools отображал содержимое Shadow DOM, в настройках DevTools, вкладка General, раздел Elements ставим галочку Show Shadow DOM.





Содержимое Shadow DOM тега <audio> в DevTools:

devtools shadow dom


ссылка на пример


Теория 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>




Результат:

custom shadow dom






ссылка на пример

Проекции, тег <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>





Результат:

content demo






ссылка на пример

Стили в 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>




Зелёный фон в примере получит только `` внутри shadow tree. То

есть стили «не вытекут» в основной документ.

Результат:


green






ссылка на пример

Наследуемые стили




По-умолчанию наследуемые стили, такие как 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>




Результат:

inherit styles






ссылка на пример

Авторские стили




Чтобы стили документа влияли на то, как выглядит 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


no to author styles


applyAuthorStyles = true


yes to author styles






ссылка на пример, 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>




Результат:

cat in the hat






ссылка на пример

Зачем нужен 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>




Результат:

x-dog






ссылка на пример

API кастомного элемента




Мы можем определять свойства и методы у нашего элемента. Такие, как метод play() у элемента <audio>.

В жизненный цикл (lifecycle) элемента входит 4 события, на каждое мы можем повесить callback:



  • created — создан инстанс элемента

  • attached — элемент вставлен в DOM

  • detached — элемент удалён из DOM

  • attributeChanged — атрибут элемента добавлен, удалён или изменён




Алгоритм создания кастомного элемента выглядит так:

  1. Создаём прототип элемента.

    Прототип должен наследовать HTMLElement или его наследника,

    например HTMLButtonElement:



    var myElementProto = Object.create(HTMLElement.prototype, {
    // API элемента и его lifecycle callbacks
    });




  2. Регистрируем элемент в 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>




Результат:

cats life






ссылка на пример

Зачем нужны 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>


Результат:


todo widget






ссылка на демо

Заключение




С развитием html5 браузеры стали нативно поддерживать новые медиа-форматы. Также появились элементы вроде <canvas>. Теперь у нас огромное количество возможностей для создания интерактивных приложений на клиенте. Этот стандарт также представил элементы <article>, <header>, и другие. Разметка стала “иметь смысл”, приобрела семантику.

На мой взгляд, Web Components — это следующий шаг. Разработчики смогут создавать интерактивные виджеты. Их легко поддерживать, переиспользовать, интегрировать.


Код страницы не будет выглядеть как набор “блоков”, “параграфов” и “списков”. Мы сможем использовать элементы вроде “меню”, “новостная лента”, “чат”.


Конечно, стандарт сыроват. К примеру, импорты работают не так хорошо, как шаблоны. Их использование рушило Chrome время от времени. Но объём нововведений поражает. Даже часть этих возможностей способна облегчить жизнь web-разработчикам. А некоторые заметно ускорят работу существующих фреймворков.


Некоторые части Web Components можно использовать уже сейчас с помощью полифилов. Polymer Project — это полноценный фреймворк уровня приложения, который использует Web Components.


ƒ


Ссылки


Eric Bidelman, серия статей и видео о Web Components:


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий