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> 

Зелёный фон в примере получит только `<div>` внутри 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:

ссылка на оригинал статьи http://habrahabr.ru/post/210058/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *