Всем привет!
Помните эту статью? Раньше мы могли быстро собрать статичную HTML-страничку в каком-нибудь FrontPage и сайт был готов. С этим мог справится любой студент. В более сложном случае, мы писали пару строк на PHP и получали уже целый портал, собранный из разных элементов шаблона на сервере. Затем мы захотели, чтобы наш сайт как-то выделялся на общем фоне и умел чуть-чуть больше. Трон занял его-величество jQuery. Теперь же, мы оказались погребены под завалами фреймворков и библиотек, инструментов сборки, менеджеров зависимостей, препроцессоров и постпроцессоров, особых форматов, языков и стилей программирования, чтобы иметь возможность стряпать простые лэндинги. Все стало слишком сложно. Спикеры на конференциях стали соревноваться в изощренности того, каким еще образом можно сломать нам
Для того, чтобы поэкспериментировать с практической частью, вам понадобится любой удобный редактор кода (например Visual Studio Code) и актуальная версия браузера Chrome. Для начала этого будет вполне достаточно. Впоследствии (я планирую целый цикл публикаций на эту тему), все неминуемо усложнится, но мы будем стараться оставаться «в рамках» — это наша цель.
Предпосылки и решение
Когда я делал свои первые сайты (в конце 90-х — начале 2000-х), первое, что мне показалось странным и ужасно неудобным в обычном HTML — невозможность описать «заголовок», «подвал» и «главное меню» сайта в одном месте для всех страниц сразу. Я мог вставить одну картинку или один скрипт во многих местах, но не банальный кусок разметки. Также, я не мог описать общий макет страницы, без необходимости повторять его в каждом отдельном HTML-файле. Я думаю, многих эти-же причины подтолкнули к первым экспериментам с серверными технологиями. Но для всего серверного нужен соответствующий сервер, а это новый уровень усложнения задачи, казавшейся сперва такой простой. Так или иначе, эта проблема решалась множество раз и множеством способов. Мы пытались использовать iframe, пытались динамически управлять видимостью фрагментов, содержащихся на одной странице; как только не издевались над собой и здравым смыслом. В итоге, мы пришли к современному набору мета-платформ типа React или Vue.js, которые, среди прочего, позволяют нам создать структуру модулей, отражающую структуру того, что мы видим на экране. Но и сама веб-платформа не стоит на месте и, о чудо, теперь у нас есть нативная возможность создавать больше чем просто многократно используемые куски разметки: теперь мы можем создавать свои собственные настоящие HTML-теги! Причем, каждый такой тег может быть как примитивным контейнером, содержащим только необходимое оформление (или быть интерактивным UI-элементом), так и макетом всей страницы. Он даже может содержать в себе целое сложное приложение с кучей, необходимой вам, клиент-серверной логики. Да, я говорю о новом стандарте Custom Elements (Living Standard). И он действительно многое меняет.
Пробуем на вкус
Для первого знакомства давайте воспроизведем вышеописанный кейс с общим макетом, хедером, футером и навигационным меню, в самом примитивном виде:
Файл index.html:
<html>
<head>
<script src="elements/my-layout.js"></script>
<script src="elements/my-header.js"></script>
<script src="elements/my-footer.js"></script>
<script src="elements/my-menu.js"></script>
</head>
<body>
<my-layout>
<my-header slot="header">Hi, I am your header.</my-header>
<div slot="content">Hello World! This is content.</div>
<my-menu slot="menu"></my-menu>
<my-footer slot="footer">And I am your footer.</my-footer>
</my-layout>
</body>
</html>
Обратите внимание на именование кастомных тегов: по стандарту оно обязательно должно содержать дефис (один или более). Также, вы, наверное, заметили атрибут «slot» — о нем немного позже.
Теперь перейдем к файлу, описывающему основной макет страницы elements/my-layout.js:
class MyLayout extends HTMLElement {
constructor() {
// Сперва вызываем конструктор суперкласса HTMLElement:
super();
// Cоздаем ShadowDOM элемента - его локальную,
// закрытую от внешнего мира область разметки, но доступную для JS:
this.attachShadow({
mode: 'open',
});
// присваеваем нашему элементу свой шаблон:
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 40px;
line-height: 40px;
padding-left: 20px;
padding-right: 20px;
background-color: #000;
color: #fff;
}
.menu {
position: fixed;
top: 0;
bottom: 0;
right: 0;
width: 240px;
padding: 20px;
background-color: #eee;
box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);
z-index: 1000;
}
.content {
padding-top: 60px;
padding-bottom: 60px;
}
.footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 40px;
line-height: 40px;
padding-left: 20px;
padding-right: 20px;
background-color: #eee;
border-top: 2px solid #000;
}
</style>
<div class="header"><slot name="header"></slot></div>
<div class="content"><slot></slot></div>
<div class="menu"><slot name="menu"></slot></div>
<div class="footer"><slot name="footer"></slot></div>
`;
}
}
// Регистируем созданный элемент:
window.customElements.define('my-layout', MyLayout);
// Теперь браузер знает о существовании нового тега <my-layout>.
Прошу прощения за избыток стилей в данном примере: они нужны только для наглядности при отображении результата в браузере.
В части шаблона, где находится сама разметка, мы снова встречаем слово «slot» — это специальный тег, который работает в сочетании с ShadowDOM — он определяет позиции в разметке для частей контента нашего элемента. Соответствие определяется тем самым атрибутом «slot», который я упомянул ранее. Если у тега «slot» нет атрибута «name» — в него попадет контент «по умолчанию», который, в свою очередь, не имеет атрибута «slot». Это очень простой, но очень мощный нативный инструмент шаблонизации на «клиент-сайде».
Создадим остальные элементы. Файл elements/my-menu.js:
class MyMenu extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
});
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
a {
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
cursor: pointer;
height: 40px;
text-decoration: none;
color: currentColor;
font-size: 1.2em;
margin-bottom: 4px;
border: 1px solid #fff;
}
a:hover {
border: 1px solid currentColor;
}
</style>
<a href="pages/page1.html">Page 1</a>
<a href="pages/page2.html">Page 2</a>
<a href="pages/page3.html">Page 3</a>
`;
}
}
window.customElements.define('my-menu', MyMenu);
Файл elements/my-header.js:
class MyHeader extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
});
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
span {
font-size: 0.8em;
opacity: 0.6;
}
</style>
<span>Text inside ShadowDOM. </span>
<slot></slot>
`;
}
}
window.customElements.define('my-header', MyHeader);
И последний файл нашего нано-проекта elements/my-footer.js:
class MyFooter extends HTMLElement {
constructor() {
super();
this.attachShadow({
mode: 'open',
});
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
::slotted(*) {
display: inline-block;
}
span {
color: #f00;
font-size: 0.8em;
}
</style>
<slot></slot>
<span> © 2018, Vasya Pupkin</span>
`;
}
}
window.customElements.define('my-footer', MyFooter);
Та-дам! Можно открывать наш index.html в браузере. Внимательный читатель заметил, что стили для тега «span» были напрямую определены сразу в двух местах (для разных элементов), однако они не повлияли друг-на-друга и отобразились правильно.
Что мы увидели?
Мы увидели пример настоящей модульной разработки без подключения каких-либо внешних библиотек, без настройки окружения, без ожидания сборки проекта, даже без необходимости запускать локальный сервер. В инструментах разработчика браузера мы видим непосредственно свой JS-код и свою собственную разметку, а не результат работы скриптов, создающих за нас DOM. Контент нашего главного HTML-файла находится в нем-же и сразу доступен, опять-же, без какого-либо предварительного рендера. Мы можем повторно использовать наши кастомные теги в этом-же проекте или в любых других. Мы не загрузили ничего лишнего и отобразили страницу практически мгновенно. Объем дополнительного JS-кода, который нам понадобился для этого — микроскопический. При этом, каждый наш новый элемент — это такой-же полноправный DOM-элемент как и любой стандартный div. Для него доступны те-же самые стандартные атрибуты, свойства, события и методы типа addEventListener, appendChild, remove и т. д. Это то, чего мы так долго хотели? По моему, да.
Дальше — больше
В дальнейшем мы рассмотрим следующие темы (не обязательно в указанном порядке):
- Жизненный цикл Custom Element в DOM
- Тег template
- Оптимизация производительности, динамическое обновление
- Custom Elements и ООП
- HTML-атрибуты, работа с общими данными
- ShadowDOM и CSS: инкапсуляция, селекторы, нативные CSS-переменные, нативные CSS-выражения
- SVG и Custom Elements — практические советы
- Организация и структура кода: модули, точки подключения, динамическая загрузка, http/2 Server Push
- Новый подход к прототипированию, созданию, тестированию и внедрению дизайна
- Нестандартные стандарты: браузеры, полифиллы, ShadyDOM, HTML-imports
- Существующая экосистема: библиотеки и готовые компоненты
- Custom Elements и серверный рендеринг
- Custom Elements и SEO
- PWA
Спасибо за внимание!
Автор: i360u