Готовим свой первый веб-компонент

в 7:01, , рубрики: javascript

Веб-компоненты (здесь и далее — в целях удобства восприятия — «WC») изобрели умные люди, которым надоело готовить спагетти из шаблонов. WC — это [почти] полностью изолированное от среды выполнения DOM-дерево. Я внимательно прочитал правила сайта, поэтому пересказывать википедию не стану. Если вы не знаете, что такое WC, почитать можно здесь.

Tl;dr: демо-страничку компонента, который выводит слайдер с текстом нескольких заметок и кнопками «туда»/«сюда» можно посмотреть прямо сейчас.

WC уже используют многие популярные сайты. Если установить плагин WC, можно посмотреть другими глазами на, например, github:

Готовим свой первый веб компонент

выделенное красненьким пунктиром — на картинке выше — WC.

Пока производители браузеров жуют бумагу (на данный момент поддерживают технологию Chrome/Opera и, с оговорками, FF), неравнодушные создают костыли. Первым был, наверное, x-tags от Mozilla (совсем недавно они выкатили библиотеку компонентов Brick, на которую пока смотреть без слез невозможно). Самым удачным костылем был и остается Polymer.

Эти библиотеки эмулируют WC, предоставляя возможность разработчикам уже сегодня писать код, который когда-нибудь в светлом будущем будет выполняться во всех браузерах нативно.

Например, в этой заметке мы создадим WC, который будет показывать пользователю несколько статей (слайдер для текстового контента). Без перезагрузки страницы, с предупреждающей загрузкой соседей и прочим блэкджеком. Шаблон страницы при этом будет выглядеть вот так (ну, там еще не относящиеся к делу меты будут, IE хелперы и всякая дребедень, наверное; для всех современных браузеров этого шаблона, впрочем, достаточно):

<!doctype html>
<head>
	<script src="bower_components/platform/platform.js"></script>
</head>

<body unresolved>
	<mudasobwa-article id="article" url="⦃url⦄" buttons="both" prefetch stateable></mudasobwa-article>
</body>
</html>

Постановка задачи

Пусть у нас есть сервер, который умеет отдавать json в таком виде:

{
  "href": "demo.html?0", /* html-ссылка для корректной обработки browser history*/
  "caption": "Title 0", /* заголовок окна */
  "prev": null, /* ссылка на json с предыдущей заметкой */
  "next": "data/data1.json", /* ссылка на json со следующей заметкой */
  "text": "<h1>Header 0</h1> <p>Lorem ipsum ...</p>" /* контент */
}

И мы хотим показывать этот контент и навигацию по «предыдущая»/«следующая», поддерживая при этом историю браузера. Для этих целей идеально подойдет WC. Итак, приступим.

Средства достижения цели

WC (как и все нынче) принято готовить из рыбы (шаблона, скелета, котельного листа, бойлерплаты). Для генерации рыбы будем использовать Йомена. Документация на Yeoman существует в сети, да она особо и не требуется: все сделано для домохозяек, как КОБОЛ.

Готовим свой первый веб компонент

Устанавливаем все необходимое (для альтернативных ОС инструкции можно найти у поставщиков), а также генератор шаблонов для полимера:

npm install -g yo bower grunt
npm install -g generator-polymer

Готовые компоненты для своего проекта можно искать на component.kitchen и customelements.io, но мы будем готовить по своему рецепту.

Создаем папку, пусть будет polymer, в ней создаем подпапку с именем нашего компонента (их принято называть в стиле prefix-name), переходим туда и запускаем генератор:

mkdir -p polymer/mudasobwa-slider && cd $_ && yo polymer:seed

Отвечаем на несколько несложных вопросов — и мы почти у цели. Давайте посмотрим, что нам нагенерили.

  • bower.json
  • index.html
  • demo.html
  • mudasobwa-slider.html
  • mudasobwa-slider.css

Отлично. Нам потребуются стандартные компоненты для ajax-вызовов, ну и font-awesome для красивых иконок:

bower install --save Polymer/core-ajax
bower install --save 'polymer-fontawesome=rtbenfield/polymer-fontawesome#master'

Эта команда скачает пакеты, с зависимостями, и обновит наш манифест. Выполним bower install и полюбуемся на нашу рыбу (любой простейший сервер запускаем в заглавном каталоге polymer (NB! на уровень выше нашего компонента):

python -m SimpleHTTPServer

Можно сходить браузером по адресу http://localhost:8000 и полюбоваться на дело рук, пока, правда, не наших. Пора немного и покодировать.

Работа с содержимым

Тут все, как мы привыкли: такой же javascript, такой же html. Есть всего несколько вещей, которые стоит знать, чтобы кофмортно себя чувствовать:

  • К свойствам есть доступ через переменную this (из скрипта) и через псевдокод {{ ИМЯ_ПЕРМЕННОЙ }} в html;
  • К дочерним компонентам есть доступ через переменную this.$ (sic!);
  • Публичные свойства — декларируются в свойстве attributes основного тега компонента;
  • У shadow-dom есть дополнительные псевдоэлементы, типа :host

Что это все означает? Давайте разберемся по коду. Наибольший интерес представляет собой компонент, который занимается обменом данными с сервером, своего рода controller из концепции MVC. Модель я описал (это json, который мы волшебным образом получаем с сервера, тут WC не помогут), на view остановимся чуть позже.

Controller

<polymer-element name="mudasobwa-content-loader" attributes="url href article caption prefetch next prev stateable">
	<template>
		<style>
			:host { display: none; }
		</style>

		<core-ajax id="current"
							 url="{{ url }}"
							 on-core-response="{{ contentLoaded }}"
							 handleAs="json">
		</core-ajax>
		<template id="prevnext" if="{{ prefetch }}">
			<mudasobwa-content-loader id="prev" url="{{ prev }}"></mudasobwa-content-loader>
			<mudasobwa-content-loader id="next" url="{{ next }}"></mudasobwa-content-loader>
		</template>
	</template>

Наш WC зовут «mudasobwa-content-loader», у него есть видимые свойства (атрибуты) url href article caption prefetch next prev stateable, он невидим, и использует WC «core-ajax» для (сюрприз) ajax-вызовов. Также он содержит два таких же, как он сам компонента для прелоадинга соседей (с выключенной опцией «prefetch», иначе каждый сосед потянет за собой своих соседей и так далее). Параметр «url» будет обновляться автоматически в пределах компонента, например, если мы вызовем this.url = http://example.com, core-ajax об этом узнает (и если бы у него была включена опция «auto», он бы сам вызвал автообновление, а получив новый ответ от сервера — вызвал бы метод нашего компонента contentLoaded). У нас автообновление отключено, чтобы не слать лишние запросы.

Скрипт тоже не особо заковырист. Посмотрим, например, на handler contentLoader:

	<script>
		Polymer('mudasobwa-content-loader', {
                        .........
			contentLoaded: function() {
				this.article = this.$.current.response.text;
				this.caption = this.$.current.response.caption;
				this.href = this.$.current.response.href;
				this.prev = this.$.current.response.prev;
				this.next = this.$.current.response.next;

				if(!this.initialized) {
					this.initialized = true;
					if(this.prefetch) {
						this.$.prev.reload();
						this.$.next.reload();
						if(typeof this.stateable !== "undefined") {
							this.saveState();
						}
					}
				}
			},

               .................

this.$ — это, как я уже упоминал, способ обращения к дереву элементов. this.$.current — наш ajax-loader (см. template выше). response — ответ, готовый ajax. Мы рассовываем важные элементы ответа по своим переменным и форсируем получение соседей, если это холодный старт и если опция prefetсh включена.

Вот как бы и все.

View

Тоже без особых потрясений. Я намеренно обкарнываю примеры до существенных деталей, весь код доступен, но если бы я разбирал каждую мелочь — заметка получилась бы длиной с «Войну и мир».

<polymer-element name="mudasobwa-slider" attributes="url caption buttons previcon nexticon stateable">
	<template>
		<style>
			:host {
				opacity: 0.8;
				transition: opacity .5s, transform 1s;
			}
                        /* еще много теневого CSS */
			#slider-container {
				display: -webkit-flex;
				display:         flex;
				-webkit-flex-flow: row;
				flex-flow: row;
			@media all and (max-width: 640px) {
                        /* еще много теневого CSS для невнятных устройств */
			#slider-container {
				-webkit-flex-flow: column;
				flex-flow: column;
				flex-direction: column;
			}
		</style>
		<div id="slider-container">
			<div id="slider-content">
				<mudasobwa-content-loader id="content"
					url="{{ url }}"
					article="{{ article }}"
					prefetch
					stateable="{{ stateable }}"></mudasobwa-content-loader>
                <!-- еще некоторое количество html -->
	</template>

Все важное — собственно в конце сниппета. Мы используем loader, чтобы доставать с сервера ajax. Скрипт, который занят заполнением history браузера я тоже намеренно опустил, все есть в исходниках на github.

Встраиваем наш компонент в страницу

Ну, тут все прозрачно, как слеза комсомолки,

В секции head:

  <script src="../platform/platform.js"></script>
  <link rel="import" href="mudasobwa-slider.html">

В секции body:

<mudasobwa-article 
        id="article" 
        url="data/data0.json" 
        buttons="both" 
        prefetch 
        stateable>
</mudasobwa-article>

Получившийся WC сам сходит на сервер за соседними заметками, нарисует (или не нарисует, если текущий элемент — крайний) стрелочки управления «вперед»/«назад», его можно было бы еще обучить предзагружать картинки соседних заметок, но это совсем уже раздует и так немаленькую заметку.

А что с индексированием? — обязательно спросит тут внимательный читатель. Ну, гугл, вроде, умеет. Яндекс, кажется, нет. Найти внятные заявления «да, поддерживаем», или «нет, не поддерживаем» мне не удалось. Поэтому технология пока идеально подходит только для маленьких сниппетов, индексирование которых не важно (см. КДПВ с гитхаба). Но я искренне верю в то, что за WC — недалекое будущее. Уж больно удобно.

Я надеюсь, что основные аспекты создания своего WC с нуля я более-менее осветил. Если есть вопросы — задавайте в комментариях. Если я что-то важное упустил — буду признателен за советы.

The summing up

демо
— весь код примера на github
— на хабре: 1, 2

Автор: mudasobwa

Источник

Поделиться

* - обязательные к заполнению поля