Вчера вышла [весьма достойная статья про Web Components, и я понял, что не могу не поделиться тем опытом, что я накопил за последние восемь месяцев. Я не буду рассказывать о том, как работать с Веб Компонентами, я расскажу, почему. Поэтому тем, кто не знает об этом стеке технологий, стоит прочитать статью по ссылке выше.
Дело в том, что в июне вышла первая стабильная версия моей библиотеки CornerJS. Начавшись как автономная реализация директив из AngularJS, она постепенно превратилась в более простой — как в использовании, так и в реализации — аналог (неточный, да, я знаю) Пользовательских Элементов из спецификации Веба Компонентов.
Я прошел через большое количество узких мест, а сейчас эта библиотека используется в нескольких больших и не очень проектах в реальных условиях.
Это замечательная, на мой взгляд, в реализации библиотека — я действительно считал ее основным инструментом в разработке, и в последнем проекте (видеопортале) она оказалась важнее jQuery: от него я мог бы отказаться, а от нее нет.
Но я, как человек, внедривший ее в нашу команду, и отказываюсь от нее — мы переходим на Mozilla X-tags для IE9+ сайтов и Polymer для IE10+ сайтов. К счастью, на моей памяти у нас был всего один или два проекта IE8+.
И в этом посте я расскажу, почему веб-компоненты — это не просто будущее веба. А единственное будущее веба.
Как все началось
Предпосылки к подобной архитектуре появлись четыре года назад в Google и Яндекс, при этом сохранился национальный колорит.
Так, в Google появился AngularJS — фреймворк, в котором большая часть логики опиралась на директивы. Почему это так важно? Потому что директивы представляли из себя модули, которые привязывались к html-тэгам, и выполнялись в изолированном окружении этой ноды. Колорит тут заключался именно в том, что Google сохранил верность Java-подобному подходу, и построил все вокруг не-нативных сеттеров и геттеров. С одной стороны это правильно, это был единственный способ сделать «почти честный» колбэк для изменения Представлений, с другой — это привнесло некое неудобство в использовании библиотеки. Впрочем, о чем это мы, мы уже ко всему привыкли, можно сказать. Но в итоге появилось главное: автономные визуальные и не очень элементы, которые можно легко переносить из проекта в проект. Кстати, говорят — я, к сожалению, последние несколько месяцев не отслеживаю прогресс AngularJS — что они уже переехали на почти нативный Object.observe.
А в Яндексе появился БЭМ, Национальная русская идея с JSON и верстальщицами. Я где-то в комментариях выразил мою позицию по отношению к нему: «БЭМ это попытка влететь в светлое будущее, размахивая костылями. С одной стороны, это действительно получилось, с другой — смотреть без смеха или слез невозможно». Впрочем, неделя рОзжыга на тему БЭМ уже прошла, так что не надо в комментариях обсуждать эту тему, прошу. Я просто хотел сказать то, что сам по себе подход действительно достоин восхищения: понять и сделать механику, которая обеспечивает инкапсуляцию и стилей, и скриптов (да, а в ангуляре только скрипты инкапсулируются) — это гениально, а то, как именно это сделано — это дело второе, и те, кому это действительно нужно, давно смирились и пользуются.
В итоге...
… мы имеем то, что имеем: большие команды пришли к осознанию того, что инкапсулироваться и подключаться должны не только библиотеки (привет, requireJS и мой личный фаворит browserify), но в первую очередь — визуальные компоненты на каждой из страниц сайта.
Почему? Да потому что они не относятся к основному потоку программы. Если у вас веб-приложение, как к нему должен относиться рендеринг диаграммы на главной странице? Никак, идеальный для вас вариант это продекларировать в шаблоне, в нужном HTML-тэге входные данные, а дальше оно пусть само работает. Вы правда не хотите думать о том, когда запустить рендер данных, а если вам внезапно захочется динамически обновлять эти данные — тем более не хотите, вам куда удобнее просто добавить какой-нибудь
<value at=100 is=500>
Вы уже пользуетесь таким подходом, смотрите:
<video width="320" height="240" poster="poster.jpg" controls="controls" preload="none">
<source type="video/mp4" src="myvideo.mp4" />
<source type="video/webm" src="myvideo.webm" />
<source type="video/ogg" src="myvideo.ogv" />
</video>
А ядро вашего веб-приложения должно заниматься действительно важными вещами: рендеринг страниц с содержимым, их динамическое обновление, работа с сетью, фоновые процессы, или ИИ и распознавание речи (а почему бы и нет?). Да даже и почти все это в общем-то можно сделать отдельными веб-компонентами.
Модульный веб
Со временем, когда ты начинаешь активно пользоваться библиотеками для веб-компонентов (или БЭМ, с чем согласятся сторонники этого подхода, хотя я чувствую, что они меня уже возненавидели) внезапно приходит осознание того, что единое приложение, как таковое, почти никогда не нужно. Каждая веб-страница — автономная сущность, изолированная от внешнего мира, ей хорошо самой по себе, а приложение — это та сущность, которая остается при переходе между страницами, и ее почти никогда нет. Дело в том, что переходы между страницами могут быть частичными, и часть Пользовательских Элементов может легко и непринужденно остаться на ней, например, плеер или чат.
В итоге получается очень простая вещь: 90-95% веб-сайтов может быть построено только из автономных компонентов, представляющих из себя модули, сочетающие в себе визуальное представление, логику, а так же внешние методы и свойства. Ну и, конечно, они могут содержать в себе другие модули.
Те реализации этого подхода, которые есть сейчас, на самом деле ужасны в части визуальной составляющей. Если в плане логики вы можете спокойно создать комбинированный AMD/npm/безмодульный компонент с неплохим в общем-то набором инструментов, то с визуальным представлением все сложнее: приходится иметь внутри модуля шаблонизатор, пользоваться inline-стилями или вписываться в document.styleSheets, и все равно не можете быть уверенным, что веб-разработчик не заденет своими стилями модуль. Вдруг у него есть какой-нибудь footer a, и он вставляет туда ваш блок лайков?
Привет, компоненты
На базе опыта команд google, mozilla и многих других у них совместно возникло понимание того, что нужно создать возможность реализовать изолированные логиковизуальные модули. Опыт же подсказал им, что действительно очень важно:
Инкапсуляция css
Это возможность полной изоляции css-стилей для того, чтобы стили документа не влияли на отображение модуля. Почему это важно — и так понятно.
Пользовательские методы и свойства
Это возможность создавать внешние интерфейсы, как методы, так и значения. К счастью, в js это довольно просто сделать, а геттеры и сеттеры уже сравнительно давно появились. Спасибо Object.defineProperty. Это важно, потому что у кастомных элементов должны быть новые интерфейсы. Как бы вы реализовывали тэг video без
HTMLVideoElement.prototype.play()
?
Куча колбэков на каждый чих
конструкторы, деструкторы, и колбэки для изменения атрибутов. Неочевидный момент: есть два колбэка «конструктора». Первый из них отвечает за создание элемента (document.createElement('x-tag')), а второй — за добавление его в DOM-дерево. В случае, если вы просто добавили элемент в верстку — выстрелят оба. Зачем это нужно? Затем, что некоторые действия (например, запуск событий) должны выполняться по добавлению элемента в DOM-дерево. Более того, они должны выполняться по каждому добавлению элемента в DOM-дерево, в том числе если вы перенесете элемент, передав его через appendChild другому родителю.
Шаблоны без выполнения содержимого
Их важность не очень очевидна на первый взгляд, вроде бы «да, все круто, теперь картиночки не будут подгружаться», но идет отрыв от контекста Пользовательских Элементов: люди забывают о том, что они тоже запускаются по добавлению в DOM, и если раньше в каком-нибудь MVVM фреймворке можно было спокойно вставить что-то в качестве шаблона и забыть — с ним ничего не случится, то теперь же нужно учитывать то, что элементы, объявленные в шаблоне, имеют свойство инициироваться, и какой-нибудь
<x-video-with-subtitle source="{{video.source}}">
будет запущен с {{video.source}} в качестве source, что совершенно не устраивает никого.
Инкапсуляция верстки
Это тоже не полностью очевидный момент. Дело в том, что точно так же стили, на верстку Пользовательского Элемента не должно быть никакого влияния: она должна быть абсолютно автономной, к ней можно «достучаться» только особой, уличной магией — именно для того, чтобы стандартные библиотеки не умели работать с ним.
Однако зачастую атрибутов для конфигурирования элемента бывает недостаточно, и для какой-нибудь диаграммы самым правильным решением будет создать что-нибудь вроде
<x-diagram type="histogram">
<value at=100 is=500>
<value at=200 is=300>
<value at=300 is=200>
<value at=300 is=700>
</x-diagram>
Поэтому нужно было сделать две вещи: инкапсуляцию реальной верстки и поддержку простейшей (на уровне тэгов) ее шаблонизации. Обе вещи неплохо удались: Shadow DOM представляет собой одновременно как способ инкапсуляции одновременно стилей и структурной верстки, так и простой, но довольно удобный в некоторых местах шаблонизатор.
В той статье не объяснялось, но я попытаюсь прояснить про шаблонизацию: в действительности шаблонизатор Shadow DOM не ограничивается применением тэга
<content>
в лоб. У него есть атрибут select, и с ним он выглядит примерно так:
<content select=".first">
. Этот тэг «вынимает» из host DOM компонента все элементы, попадающие под действие тэга. Соответственно, второй раз этот поиск уже ничего не найдет. Таким образом, написание
<content></content><content></content>
в реальности просто перенесет содержимое host DOM в composed DOM, а второй тэг уже ничего не сможет перенести, потому что элементов уже нет.
Во многих публикациях он подвергался резкой критике из-за того, что не обладал какими-либо сверхспособностями вроде итераторов или фильтров, или даже просто возможности вставить элемент дважды. К сожалению, никто особо не тратил на оппонентов время, а надо было бы. Дело в том, что shadow DOM не шаблонизатор в привычном смысле слова: он не создает элементы из шаблонов. Он занимается прямым пробросом элементов. Если вы поместили элемент в host DOM, и изменили его атрибуты, они будут доступны и изменены и в shadow DOM. Если вы поместили один Пользовательский Элемент в другой Пользовательский Элемент, конструктор для вложенного элемента будет вызван всего один раз, и у него не случится внезапного осознания того, что его куда-то перенесли, и надо заново запускать конструктор. Это был единственный возможный разумный вариант для реализации.
Объявление Пользовательского Элемента
Для декларирования подобных элементов не подходил тэг script: фактически, большая часть тэга представляла из себя все же верстку. В некоторых — я бы даже сказал, многих — случаях скриптинга можно в принципе избежать. Поэтому самым разумным решением было расширить спецификацию самого HTML еще одним элементом с немного «особенным» синтаксисом.
Но тут возникает потребность в еще одном решении: как эти элементы кэшировать? Мы не можем подключить их через script, потому что это не тэг script, а значит, нужно создавать спецификацию для другого способа подключения элементов. Так появились HTML Imports.
Подробнее о них я рассказывать не буду, это, конечно, интересно, но статья и так выходит довольно большой.
Сам же выбор подхода с созданием новых тэгов появился по ряду причин, но одной из основных я бы назвал невозможность привязки двух конструкторов к одному и тому же тэгу. В свое время я потратил полдня на то, чтобы отладить директиву лицензионного соглашения. Выглядел тэг как-то так:
<div class="directive-agreement directive-scrollbox"> Текст соглашения по умолчанию, который бы заменялся в случае, если для данного конкурса задан другой текст </div>
Я долго не мог понять, почему у меня не появляется скроллбокс. Причем дебаггер показывал, что он создается… а потом пропадает.
Дело оказалось именно в том, что directive-scrollbox отрабатывает первой. А directive-agreement — второй, и тоже преобразовывает innerHTML.
В итоге
Спецификация Веб Компонентов очень хорошо продумана, и построена из отдельных модулей, которые вместе направлены на одну простую задачу: предоставить действительно удобный способ создания новых HTML-тэгов, в которых инкапсулированы визуальная часть и логика. Это способно в разы ускорить создание веб-сайтов за счет того, что их становится легче подключить, и во многих случаях это можно выполнять верстальщику самостоятельно, без привлечения фронтэнд-разработчиков. Фронтэнд-разработчикам жизнь тоже облегчается, их работа разделяется фактически на две составных части: создание автономных модулей с логикой и большим удобным набором внешних интерфейсом, и создание ядра веб-приложения. При этом вторая деятельность больше относится к senior разработчикам, которые уже устали от создания «клевых анимированных эффектов», а первая гораздо легче парралелится, и гораздо легче тестируется: тестирование переходит от BDD, который довольно долго был основным для веба, к модульному тестированию, в котором тестируется по отдельности каждый компонент.
Механика HTML Imports особенно важна для развития веба, так как почти гарантированно приведет к созданию онлайн-репозитория/CDN с большим количеством библиотек веб-компонентов, а, как мы знаем, централизованные репозитории положительно влияют на качество кода, создаваемое сообществом: все стараются поднять свою репутацию, выдавая качественный код, само сообщество активно развивается, а необходимые модули и библиотеки становится проще найти и подключить. Централизованный же CDN способен обеспечить и более быструю загрузку веб-страниц (это бич наших дней, размер страниц увеличивается быстрее, чем увеличивается пропускная скорость интернета у конечного пользователя).
В итоге почти наверняка через буквально 2-3 года большая часть того, что сейчас представлено библиотеками, будет реализовано в качестве Пользовательских Элементов, потому что это банально удобнее в использовании и поддержки. Слайдеры, responsive изображения, WYSIWYG и Markdown-редакторы, видео с поддержкой караоке, генератор мурлыкающих котиков на canvas, в общем почти все, что есть в вебе, может быть обернуто в Пользовательский Элемент.
Самое смешное, что элемент не обязательно должен быть визуальным: почему бы не сделать что-то вроде такого?
var $ = document.createElement('x-library-jQuery')
Веб Компонентов дает вам удобный интерфейс для создания пользовательских библиотек, который по крайней мере стандартизирован, и это действительно удобно.
Начните использовать их сегодня.
Попробуйте, вам понравится.
Если вы разрабатываете для IE9+ — вы можете использовать Mozilla X-tags (сейчас у них УПС, пользуйтесь репозиторием)
Если для IE10+ — подключите Polymer и используйте честные Пользовательские Элементы или пользуйтесь более легким синтаксисом и расширением возможностей Веба Компонентов в polymer.js. Искреннее спасибо Google за то, что они очень сильно вложились в Polymer и создали ie10+ (а некоторые даже IE9+) полифиллы для всех составных частей Веб Компонентов.
Я надеюсь, мне удалось рассказать вам, почему все было сделано именно так: я прошел это на своем опыте, с каждой версией библиотеки все больше приближаясь к X-tags, потому что подобный синтаксис является единственно возможной реализацией «реактивного» подхода к работе с визуальными компонентами, и приближаясь к осознанию причин некоторых странных и сложных на первый взгляд вещей.
Послесловие
В последнее время ведется все больше разговоров о том, что веб становится все более фрагментирован. Если вам нужно подключить browserify-модуль и AMD модуль, это вызовет… некоторые проблемы. Некоторые библиотеки адаптированы для TypeScript, некоторые для CoffeeScript (ну ладно, ладно, я знаю, с ними можно работать, но многих CoffeeScript легко может напугать, если нужно будет исправить небольшие баги в какой-нибудь маленькой библиотеке или добавить свои методы). Dart — это вообще отдельный язык, который далеко не идеально взаимодействует с обычным JS. Веб Компоненты могут убрать эту фрагментированность: все компоненты будут совместимы друг с другом, потому что это будет просто HTML. Пусть и с новыми элемнтами. Google может спокойно создавать компоненты на их Dart, вы сможете пользовать icedCoffee, в Facebook продолжат пугать всех своим JSX, а в Одессе таки будут развивать модули на liveScript и дальше.
Вы все равно сможете пользоваться этими библиотеками, потому что у всех их будет один и тот же способ взаимодействовать.
Пожалуйста, начните использовать их сегодня, и вы сделаете веб лучше: любой разработчик сможет использовать вашу библиотеку, вне зависимости, пишет ли он на чистом JS, или на любом из препроцессоров, или вообще не пишет — задумайтесь о том, что слайдер, например, не нужно инициализировать из JS, ему достаточно того, что он будет сконфигурирован через атрибуты и внутреннее содержимое. В конце концов, две трети, наверное, библиотек для jQuery — это просто что-то вроде $().mediaelement(config) и все.
P.S. Предлагаю объявить неделю Веб Компонентов открытой
Автор: Jabher