Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Web-components

Кирилл Торгашин
Web-разработчик

Некоторые вещи из детства определяют твои предпочтения на всю жизнь. Мне, например, огромное вдохновение давал небезызвестный конструктор торговой марки Lego. Это был всего лишь набор разнообразных маленьких деталей, из которых впоследствии получался автомобиль, а убрав колеса и переосмыслив идею — исследовательская станция. Так, любовь к конструированию привела меня в мир программирования и разработки. Казалось бы, причем здесь это лирическое вступление и web-компоненты?

web-components

Идея web-компонентов зрела в frontend-сообществе уже долгое время, обращая внимание на то, каким темпом сейчас развивается JS-среда. Появляются фреймворки, библиотеки, утилиты, и всё это затем, чтобы клиентский код не превращался в ад кромешный. А он умеет! Учитывая возможности JavaScript.

Делать же сложные вещи – очень плохая затея. Любая попытка масштабировать подобные решения заканчивается бессонными ночами с дебаггером и литрами кофеиносодержащих напитков.

В итоге разработчики пришли к идее инструмента, позволяющего создавать «модули», которые будут встраиваться в DOM, но при этом сохранять независимость в виде инкапсулированных свойств и функций. Так появился стандарт Web-components.

Что такое Custom-elements

Этот стандарт позволяет описывать для новых элементов инкапсулированные свойства, методы, правила поведения в DOM и многое другое. Спецификация требует при регистрации указывать дефис в названии компонента, чтобы не конфликтовать со стандартными тегами HTML. Особенности данного стандарта предполагают жизненный цикл компонента:

  • created — создан экземпляр текущего компонента.
  • attached — компонент зарегистрирован в DOM-дереве.
  • detached — компонент удален из DOM-дерева.
  • attributeChanged — атрибут компонента добавлен, удален или изменен.

Польза от сustom-elements очень высока, так как позволяет:

  1. Делать семантическую разметку, что значительно улучшает понимание кода.
  2. Дает возможность организовать большое приложение маленькими блоками.

HTML-Imports – простое API, позволяющее встраивать фрагменты разметки из других файлов.

Templates – концепция шаблонов проекта. Web-components подразумевают шаблоны как элементы DOM-дерева, которые распознаются браузером, но не отображаются, пока не произойдет регистрация компонента. Это дает очень большие преимущества перед «шаманством» со старыми попытками сделать шаблоны.

  1. Когда выполняться коду решает разработчик, а не событие загрузки страницы.
  2. Нет угроз работы со строками, и как следствие, нет нужды в дополнительном экранировании.

ShadowDOM – позволяет изменять внутренние представления компонентов, инкапсулируя их от внешних. Уровень доступа к ShadowDOM настраивается разработчиком.

Немного сухо, верно? ShadowDOM – очень мощная часть Web-components. Она позволяет прятать все вложенности компонента в специальный элемент ShadowTree, который выступает в роли инкапсулированной обертки всего содержания компонента. Например, имея элемент <modal-window>, который отвечает, как ни странно, за модальное окно, мы будем видеть только тег <modal-window> в нашем коде, хотя он будет иметь различные поля ввода и кнопки. Откуда же они там? Они находятся внутри ShadowTree, в чем можно убедиться, открыв инспектор DevTools.

Одна из фишек ShadowDOM — инкапсулированные стили, закрытые для остальных элементов, что позволяет также писать модульный CSS для элементов.

Пример реализации

Ниже будет представлен пример реализации компонента с помощью нативных технологий, доступных браузерам. А тем, кто ещё не умеет демонстрировать web-компоненты нативным способом, доступны полифилы. Первым делом создается сам web-компонент:

<template>
<style>
/* какие-нибудь стили */
</style>
<div class="container">
<div class="right">
<div class="day-long"></div>
<div class="time"></div>
</div>
</div>
</template>
<script>
(function() {
    "use strict";
    // получаем контент текущего шаблона (_currentScript доступен из web-components)
let template = document._currentScript.ownerDocument.querySelector('template');
let proto = Object.create(HTMLElement.prototype);
    // функция регистрирует событие добавления элемента
proto.createdCallback = function() {
let clone = document.importNode(template.content, true);
this.createShadowRoot().appendChild(clone);

this.$container = this.shadowRoot.querySelector('.container');
this.$dayLong = this.shadowRoot.querySelector('.day-long');
this.$time = this.shadowRoot.querySelector('.time')

    // функция зовется после того, как элемент попадает в DOM-дерево
proto.attachedCallback = function() { }

    // функция зовется после того, как атрибут элемента будет изменен
proto.attributeChangedCallback = function(attrName, oldValue, newValue) { }

// регистрацияэлемента в DOM-дереве
document.registerElement('mr-timer', { prototype: proto });
})(); </script>

Далее компонент следует «подцепить» в основном элементе и разместить новый тег в body страницы:

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Web components!</title>
// полифил для старых браузеров
<script src="https://cdnjs.cloudflare.com/ajax/libs/webcomponentsjs/0.7.22/webcomponents-lite.js"></script>
// импорткомпонента
<link rel="import" href="./components/mr-timer.html">
</head>
<body>
<mr-timer></mr-timer> // вот и он!
</body>
</html>

Если все действия верны, то в DevTools можно увидеть следующее:

1

Компонент успешно добавлен в DOM страницы и имеет свой ShadowDOM, при открытии которого видна вся разметка элемента.

Но пока что на странице пусто. Исправим это недоразумение с помощью JavaScript и CSS.

constdays = ['Воскресение','Понедельник','Вторник','Среда','Четверг','Пятница','Суббота'];
// функция регистрирует событие добавления элемента
proto.createdCallback = function() {
let clone = document.importNode(template.content, true);
this.createShadowRoot().appendChild(clone);
      // выбираем div-элементы, в которые встраиваем значения из функции draw()
this.$container = this.shadowRoot.querySelector('.container');
this.$dayLong = this.shadowRoot.querySelector('.day-long');
this.$time = this.shadowRoot.querySelector('.time')
      // функция отрисовки таймера
this.draw();
let that = this;
      // «Оживление» таймера
setInterval(() => {
that.draw();
}, 1000);
}
    // переменным присваивается текущая дата
proto.draw = function() {
this.date = new Date();
this.$dayLong.innerHTML = days[this.date.getDay()].toUpperCase();
this.$time.innerHTML = this.date.toLocaleTimeString();
}

Так-то лучше. Теперь таймер отображается на странице благодаря функции draw() и отсчитывает время благодаря возможностям языка:

2

Теперь немного супер-силы JavaScript для демонстрации особенностей web-компонентов. Добавлена небольшая функция для смены темы и немного доработана функция-слушатель изменения атрибутов.

proto.attributeChangedCallback = function(attrName, oldValue, newValue) { 
  switch (attrName) {
    case "theme":
this.updateTheme(newValue);
break;
  }
}
// функция для смены темы.
proto.updateTheme = function(theme) {
  let val = "green";
  if (["green", "red", "blue", "gold"].indexOf(theme) > -1) {
val = theme;
  }
this.$container.className = "container " + val;
}

И, вуаля! Теперь мы можем через атрибут-свойства менять что-либо в нашем компоненте, не затрагивая всю страницу.

3 4

Конечно, подобный подход сложен, так как подразумевает кропотливую работу с каждым элементом. Но благодаря Google есть прекрасная библиотека Polymer, которая направлена на работу с web-компонентами.

Читать и комментировать