## Продвинутые подходы разработки SPA
Слайды доступны по ссылке
frontend.tech-mail.ru

## Немного истории
Как разрабатывали web раньше?

##
`jQuery`-style —
весь код приложения в одном файле
Zepto, KnokoutJS, etc...

##
Такой способ удобен для украшения сайта, но не для разработки крупных web-приложений

##
MVC —
разделение логики от представления
Backbone.js, Ember.js, etc...

##
Каждый понимал его по-своему. Не было чёткого подхода к декомпозиции

##
Как много вопросов, как мало ответов...

- Как поступать со сложными моделями?
- Как разбивать большие view на части?
- Кто должен отвечать за функциональность?

##
Компонентный подход —
смысл в разделении кода приложения на независимые, слабосвязанные и переиспользуемые компоненты
слабые связи и декомпозиция

##
Компоненты могут быть достаточно сложны внутри, но они должны быть просты для использования снаружи

##
Компонентом может быть вообще всё что угодно, что выполняет какую-то функцию в вашем приложении

## Компоненты делят на два типа

> Dummy-компоненты — компоненты, которые либо вообще не содержат никакой логики (чисто визуальные компоненты), либо содержат логику, которая глубоко инкапсулирована внутри компонента

Например, компонент "Текст", компонент "Ссылка", компонент "Чекбокс"... Посложнее: компонент "Выезжающее меню", Компонент "Роутер".

> Smart-компоненты — компоненты, которые управляют множеством других компонентов, содержат в себе бизнес-логику и хранят какое-то состояние

Компонент "Форма входа". Компонент "Всё приложение"

## Пример dummy-компонента

    		class TextComponent {
    		    constructor(el) { this.el = el || document.createElement('div') }
    		    setText(text) {
    		        this.text = text;
    		        this.el.innerHTML = this.tmpl();
    		    }
    		    tmpl() {
    		        return `
    		            <span>${this.text}</span>
    		        `;
    		    }
    		}

## Пример smart-компонента

    		class CounterComponent {
    		    constructor(el) {
    		        this.el = el || document.createElement('div');
    		        this.count = 0; this.text = new TextComponent();
    		        setInterval(() => {
    		            this.count++;
    		            this.text.setText(this.count);
    		            this.el.innerHTML = this.tmpl();
    		        }, 1000);
    		    }
    		    ...

## Пример smart-компонента

    		    ...
    		    tmpl() {
    		        return `
    		            <h1>Счётчик:</h1>
    		            ${this.text.tmpl()}
    		        `;
    		    }
    		}

## Структура приложения

    		App — smart
    		    Router — dummy
    		        Authorization — smart
    		            Form — dummy
    		                TextInput — dummy
    		                Button — dummy
    		        About — smart
    		            SimpleText — dummy
    		            Link — dummy

## Наивная реализация компонентов

- Компонент — модуль, который содержит в себе стили этого компонента, файл-функцию с шаблоном и класс, который всем этим управляет
- Экземпляр компонента инициализируется с некоторыми данными (состоянием — state ), которые используются при функционировании компонента
- Во время рендера, вызывается функция-шаблонизатор, она создаёт строку с HTML-кодом компонента, которая через `innerHTML` вставляется в DOM документа
- При изменении состояния компонента, выполняется его ререндер и он обновляет своё отображение

##
Посмотрим, как это выглядит на практике

## Недостатки наивной реализации

- Много шаблонного кода
- При изменении одного единственного свойства происходит полная перерисовка компонентов
- Много лишних операций с рендером DOM
- Как следствие, низкая производительность web-приложений (вспоминаем, что всё приложение — это один большой компонент и учитываем два предыдущих пункта вместе)

##
Как можно этого избежать?

##
Самая простая идея —
при изменениях state перерисовывать только то, что действительно необходимо

## Связывание данных (Data Binding)

> Связывание данных — это процесс, который устанавливает соединение между UI приложения и бизнес-логикой

Различают одностороннее и двустороннее связывание данных — пример

## Наивное связывание данных

    		class TextComponent {
    		    constructor(el) {
    		        this.el = el || document.createElement('div');
    		        this.el.innerHTML = this.tmpl();
    		        this.text = '';
    		        this._textEl = this.el.querySelector('.js-text');
    		    }
    		    setText(text) {
    		        this.text = text;
    		        this._textEl.textContent = this.text;
    		    }
    		}

## Наивное связывание данных

    		class TextInputComponent {
    		    constructor(el) {
    		        this.el = el || document.createElement('div');
    		        this.el.innerHTML = this.tmpl();
    		        this.value = '';
    		        this._inputEl = this.el.querySelector('input[type=text]');
    		        this._inputEl.onchange = () => {
    		            this.value = this._inputEl.value.trim();
    		            this.emit('change', {value: this.value});
    		        };
    		    }
    		}

##
Не самый хороший подход:
— много шаблонного кода, если необходимо связывать много элементов
— недекларативное описание связей

## Другой подход к биндингу

    		<div>
    		    <h1>Привет, <span data-bind="textContent:name"></span></h1>
    		    <input
    		        <input type="text"
    		        <input placeholder="Сколько вам лет?"
    		        <input data-bind="value:age"
    		    <input />
    		</div>

## Другой подход к биндингу

    		class AutoBind {
    		    constructor(root) {
    		        this.binded = [...root.querySelectorAll('[data-bind]')]
    		            .map(el => ({
    		                el,
    		                prop: el['data-dind'].split(':')[0],
    		                variable: el['data-dind'].split(':')[1],
    		            }));
    		    }

## Другой подход к биндингу

    		    getVariable(name) {
    		        const entry = this.binded.find(({variable}) => variable === name);
    		        return entry ? entry.el[entry.prop] : undefined;
    		    }
    		    setVariable(name, value) {
    		        this.binded.forEach((entry) => {
    		            if (entry.variable === name) {
    		                entry.el[entry.prop] = value;
    		            }
    		        });
    		    }
    		}

## Реактивное программирование

> Реактивное программирование — парадигма программирования, ориентированная на потоки данных и распространение изменений.

В обычном мире, чтобы посчитать значение функции, необходимо вызвать её с необходимыми аргументами

В модном и молодёжном реактивном программировании функция сама пересчитается, когда её аргументы изменятся (как, например, в excel)

## Добавим чуть-чуть реактивности

    		class AutoReactiveBind {
    		    constructor(root, store) {
    		        this.store = store;
    		        this.binded = ... ;
    		        this.binded.forEach(entry => {
    		            entry.el.addEventListener('change', () => {
    		                this.setVariable(entry.variable, entry.el[entry.prop]);
    		            });
    		        });
    		    }
    		}

## Добавим чуть-чуть реактивности

    		    getVariable(name) {
    		        return this.store[name];
    		    }
    		    setVariable(name, value) {
    		        this.store[name] = value;
    		        this.binded.forEach((entry) => {
    		            if (entry.variable === name) {
    		                entry.el[entry.prop] = value;
    		            }
    		        });
    		    }
    		}

##
А ещё можно по-красивому биндить события

## Биндинг событий

    		<form data-bind-event="submit:formSubmitted">
    		    <input
    		        <input type="text"
    		        <input placeholder="Сколько вам лет?"
    		        <input data-bind-event="input:updateAgeListener"
    		    <input />
    		    <input type="submit" />
    		</form>

##
Посмотрим, как это выглядит на практике

##
А можно ещё круче!

## Virtual DOM

Главная проблема DOM — он никогда не был рассчитан для создания динамического пользовательского интерфейса.

> Virtual DOM — это техника и набор библиотек / алгоритмов, которые позволяют нам улучшить производительность на клиентской стороне, избегая прямой работы с DOM путем создания и работы c абстракцией, имитирующей DOM-дерево

## Virtual DOM

- Вместо того, чтобы взаимодействовать с DOM напрямую, мы работаем с его легковесной копией
- Мы можем вносить изменения в копию, исходя из наших потребностей, а после этого применять изменения к реальному DOM
- При этом происходит сравнение DOM-дерева с его виртуальной копией, определяется разница и запускается перерисовка того, что было изменено

## Virtual DOM

    				// before
    				{
    				    tagName: 'div',
    				    classes: ['header'],
    				    attributes: {
    				        hidden: false
    				    }
    				}

    				// after
    				{
    				    tagName: 'div',
    				    classes: ['header'],
    				    attributes: {
    				        hidden: true
    				    }
    				}

    		// Real DOM change
    		div.setAttribute('hidden', 'true')

## Virtual DOM

Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM. Но только если мы делаем это правильно. Есть две проблемы:

- когда именно делать повторную перерисовку DOM?
- как это сделать эффективно?

## Когда?

Когда данные изменяются и нуждается в обновлении. Есть два варианта узнать, что данные изменились:

- Первый из них — «dirty checking» (грязная проверка) заключается в том, чтобы опрашивать данные через регулярные промежутки времени и рекурсивно проверять все значения в структуре данных
- Второй вариант — «observable» (наблюдаемый) заключается в наблюдении за изменением состояния. Если ничего не изменилось, мы ничего не делаем. Если изменилось, мы точно знаем, что нужно обновить

## Как?

Что делает этот подход действительно быстрым:

- Эффективные алгоритмы сравнения
- Группировка операций чтения/записи при работе с DOM
- Эффективное обновление под-деревьев

## Virtual DOM

## Как работает Virtual DOM?

## Пишем свой Virtual DOM

https://github.com/8coon/tp-vdom-example

## Какие задачи должен решать
Virtual DOM?

- Создавать дерево элементов на странице по шаблону
- Изменять дерево элементов согласно обновлённому шаблону

##
Создание ноды

### Функция vdom.create()

    		function create(vnode) {

    				// Если создаём текстовый узел
    				if (typeof vnode === 'string') {
    					return document.createTextNode(vnode);
    				}

    				const node = document.createElement(vnode.tag);

    				// Добавляем атрибуты
    				if (vnode.attrs) {

    						for (const [name, value] of Object.entries(vnode.attrs)) {
    							node.setAttribute(name, value);
    						}

    				}

## Функция create() - продолжение

    				// Создаём детей
    				if (vnode.children) {

    						for (const child of vnode.children) {
    							node.appendChild(create(child));
    						}

    				}

    				// Связываем виртуальный и реальный DOM
    				node._vnode = vnode;

    				return node;
    			}

### Создание ноды - пример

    		const node = vdom.create({
    			tag: 'DIV',
    			attrs: {
    				style: 'background-color: red;',
    				'tab-index': 0

    				}
    			});

    			document.body.appendChild(node);

##
Успех!

##
Обновление ноды

### Функция vdom.update()

## Что могло поменяться?

- Количество и значения атрибутов
- Любой из потомков

##
Обновление атрибутов ноды

## Функция updateAttrs()

    		function updateAttrs(node, prevAttrs, attrs) {

    				// Задаём новые значения атрибутов
    				if (attrs) {

    						for (const [name, value] of Object.entries(attrs)) {
    							node.setAttribute(name, value);
    						}

    				}

    				// Удаляем старые атрибуты, которых не встретилось в новых
    				if (prevAttrs) {

    						for (const name of Object.keys(prevAttrs)) {

    								if (!attrs || !attrs.hasOwnProperty(name)) {
    									node.removeAttribute(name);
    								}

    						}

    				}

    		}

## Что могло поменяться?

- Количество и значения атрибутов
- Любой из потомков

Для обновления потомков будем использовать сравнение по ключам.

> Ключ — специальный строковый атрибут, семантически идентифицирующий элемент среди соседних.

##
Обновление дочерних нод
(Возможная реализация)

### Обновление дочерних нод

- 1 создание
- 1 удаление
- 2 обновления

## Функция updateChildren()

    		function updateChildren(node, prevChildren, children) {

    				const childLength = children ? children.length : 0;

    			for (let i = 0, j = 0; i < childLength; i++, j++) {

    					const prevVchild = prevChildren && prevChildren[j];
    					const vchild = children[i];

    					const fromString = typeof prevVchild === 'string';
    					const toString = typeof vchild === 'string';

    					if (fromString && toString) {

    							// Если старый и новый элемент -- строки
    							// Внимание! .childNodes, не .children!
    							node.childNodes[i].textContent = vchild;

    					} // else if ( -- Прололжение на следующем слайде

## Функция updateChildren()
(продолжение)

    					} else if (
    						!fromString && !toString && prevVchild && prevVchild.key === vchild.key
    					) {

    							// Ключи совпадают -- можем обновить оптимально
    							update(node.childNodes[i], vchild);

    					} else {

    							// Если строка превращается в элемент или наоборот
    							// Либо у элементов не совпали ключи
    							// Вставляем новую ноду перед текущей, старая нода при этом "всптывает" в конец
    							node.insertBefore(create(vchild), node.childNodes[i]);

    								// Сдвигаем индекс сравнения массива старых чайлдов
    								j--;

    					}
    				}

## Функция updateChildren()
(продолжение 2)

    			const curChildLength = node.childNodes.length;

    				// Удаляем "лишние" узлы
    				for (let i = childLength; i < curChildLength; i++) {
    					node.removeChild(node.childNodes[childLength]);
    				}
    			}

## Обновление ноды - пример

    		const node = vdom.create({

    				tag: 'DIV',
    			});

    			document.body.appendChild(node);

    			dom.update(node, {
    				tag: 'DIV',
    				attrs: {

    					style: 'background-color: black;',
    					'tab-index': 2

    				}
    			});

##
Всё отлично?

##
Нет!

### При компонентном подходе
возникает ряд вопросов:

- Как эффективно обновлять дочерние компоненты, находящиеся в шаблоне родителя?
- Как управлять дочерним компонентом из родителя?
- Как не сломать этим поведение дочернего компонента?

##
Надо программировать

## Создание ноды с компонентом

    			if (typeof vnode.tag === 'function') {
    				// Создаём инстанс компонента
    				vnode._instance = new vnode.tag(vnode.attrs, vnode.children);
    				// Выполняем его шаблон
    				const componentVnode = vnode._instance.render();
    				// Создаём DOM по шаблону
    				const node = create(componentVnode);
    				// Записываем ссылку на элемент в компонент
    				vnode._instance.el = node;
    				// Сохраняем шаблон компонента для эффективного обновления
    				node._originalVnode = node._vnode;
    				// Записываем vdom инстанса компонента
    				node._vnode = vnode;
    				return node;
    			}

## Создание компонента - пример

    			const node = vdom.create({
    				tag: MyComponentClass,

    					// Эти атрибуты попадут прямо в инстанс компонента
    					attrs: {
    						value: 1
    					}

    			});

## Обновление ноды с компонентом

    			let resultVnode = vnode;
    			if (isComponent) {
    				// Выполняем шаблон компонента
    				vnode = prevVnode._instance.render();
    				// Переносим инстанс со старой ноды на новую
    				resultVnode._instance = prevVnode._instance;
    				// Дальше сравнивать будет именно со старым vdom шаблона, а не компонента
    				prevVnode = node._originalVnode;
    				// Обновляем шаблон компонента
    				node._originalVnode = vnode;
    			}
    			updateAttrs(node, prevVnode.attrs, vnode.attrs);
    			updateChildren(node, prevVnode.children, vnode.children);
    			// Обновлем связь с virtual DOM
    			node._vnode = resultVnode;

## Обновление ноды с компонентом
(продолжение)

    			updateAttrs(node, prevVnode.attrs, vnode.attrs);
    			updateChildren(node, prevVnode.children, vnode.children);

    			// Обновлем связь с virtual DOM
    			node._vnode = resultVnode;

    				// Вызываем метод жизненного цикла компонента
    				if (isComponent) {
    					resultVnode._instance.didUpdate();
    				}

### При компонентном подходе
возникает ряд вопросов:

- Как эффективно обновлять дочерние компоненты, находящиеся в шаблоне родителя?
- Как управлять дочерним компонентом из родителя?
- Как не сломать этим поведение дочернего компонента?

##
Решение:
Жизненный цикл компонента

## Жизненный цикл компонента

    			class Component {

    					constructor(attrs, children) {
    						this.attrs = attrs;
    						this.children = children;
    					}

    					/** Вызывается после создания компонента */
    					didCreate() {}

    					/** Вызывается перед обновлением компонента */
    					willUpdate(attrs, children) { /* ... */ }

    					/** Вызывается после обновления компонента */
    					didUpdate() {}

    					/** Вызывается перед уничтожением компонента */
    					willDestroy() {}

    			}

##
willDestroy() ?

## Прибираемся за компонентом

    			function destroy(node) {
    				const vnode = node._vnode;

    					if (vnode) {

    							const isComponent = typeof vnode.tag === 'function';
    							// Если удаляем компонент, вызовем метод жизненного цикла
    							if (isComponent) {
    								vnode._instance.willDestroy();
    							}

    							// Удаляем детей
    							for (const child of node.childNodes) {
    								destroy(child);
    							}

    					}
    				}

##
Теперь всё отлично?

##
Да.
Но можно лучше!

## JSX

## Возможные улучшения

- Настроить JSX, Поддержать функцию createElement. https://youtu.be/t-Bd2RKC3Oo?t=5286
- Улучшить работу с DOM-атрибутами. Например, поддержать style или className.
- Автоматически генерировать ключи там, где можем.
- Сделать метод жизненного цикла didMount(), позволяющий перехватить момент, когда нода была вставлена на страницу.
- Поддержать события.

## Поддержка событий.

## Демо-приложение

## Перерыв

## Веб-компоненты

## Современные веб-компоненты

> Веб-компоненты — технология, которая позволяет создавать многократно используемые компоненты в веб-приложениях. Веб-компоненты поддерживаются веб-браузерами напрямую и не требуют дополнительных библиотек для работы . Модель веб-компонентов подразумевает инкапсуляцию и совместимость отдельных элементов

> На данный момент частичная поддержка существует в браузерах Chrome, Firefox, Opera и Safari. Для браузеров не поддерживающих веб-компоненты реализованы полифиллы

## Современные веб-компоненты

Веб-компоненты включают в себя 3 технологии:

- Shadow DOM v1 — developers.google.com
- HTML Templates — MDN
- Custom Elements API v1 — описание

## Shadow DOM v1

> Shadow DOM — инструмент инкапсуляции HTML. Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным

## Shadow DOM v1

## `<input type="range">`

## Shadow DOM

    		// Shadow DOM v0
    		const root = element.createShadowRoot();

    		// Shadow DOM v1
    		const root = element.attachShadow({ mode: 'open' });
    		// const root = element.attachShadow({ mode: 'close' });

    		element.shadowRoot === root; // true

    		root.innerHTML = ' ... ';

## Shadow DOM (пример 1)

    		<!-- html -->
    		<div id="element1">
    		    <h2>Subheader h2</h2>   <p>lorem ipsum</p>
    		    <h2>Subheader h2</h2>   <p>Lorem ipsum dolor sit.</p>
    		</div>

    		const root1 = element1.attachShadow({ mode: 'open' });
    		root1.innerHTML = `
    		    <section>
    		        <h1>Main header</h1>
    		        <slot></slot>
    		    </section>
    		`;

## Shadow DOM (пример 1)

    		<div id="element1">
    		    <section>
    		        <h1>Main header</h1>
    		        <h2>Subheader h2</h2>   <p>Lorem ipsum.</p>
    		        <h2>Subheader h2</h2>   <p>Lorem ipsum dolor sit.</p>
    		    </section>
    		</div>

## Shadow DOM (пример 2)

    		<!--html-->
    		<div id="element2">
    		    <span slot="header">Header</span>
    		    <span slot="content">Lorem ipsum dolor sit.</span>
    		</div>

    		const root2 = element2.attachShadow({ mode: 'open' });
    		root2.innerHTML = `
    		    <section>
    		        <h1> <slot name="header"></slot> </h1>
    		        <p> <slot name="content"></slot> </p>
    		    </section>
    		`;

## Shadow DOM (пример 2)

    		<div id="element2">
    		    <section>
    		        <h1> <span>Header</span> </h1>
    		        <p> <span>Lorem ipsum dolor sit.</span> </p>
    		    </section>
    		</div>

## Shadow DOM

    		#shadow-root
    		<style>
    		    :host {
    		        opacity: 0.4;
    		        will-change: opacity;
    		        transition: opacity 300ms ease-in-out;
    		    }
    		    :host(:hover) {
    		        opacity: 1;
    		    }
    		    ::slotted(<selector>) { ... }
    		</style>

## HTML Templates

> HTML Templates — это механизм для отложенного рендера клиентского контента, который не отображается во время загрузки, но может быть инициализирован при помощи JavaScript. Содержимое тегов `<template>` парсится браузером, но отрабатывает только в момент вставки шаблона в DOM

## HTML Templates

    		<!--html-->
    		<template id="tmpl">
    		    <h3>Заголовок: <slot name="title"></slot></h3>
    		    <img src="image.png" alt="My Image">
    		    <script>
    		        alert(1);
    		    </script>
    		</template>

## HTML Templates

    		<!--html-->
    		<div id="source">
    		    <span slot="title">Hello, World!</span>
    		</div>

## HTML Templates

    		const target = document.getElementById('source');
    		const tmpl = document.getElementById('tmpl');

    		// two way
    		target.appendChild(tmpl.content.cloneNode(true));
    		/*  or  */
    		target.appendChild(document.importNode(tmpl.content, true));

Ссылка на — пример

## HTML Templates

    		<!-- result -->
    		<div id="source">
    		    <span slot="title">Hello, World!</span>

    		    <h3>Заголовок: <slot name="title"></slot></h3>
    		    <img src="image.png" alt="My Image">
    		    <script>
    		        alert(1);
    		    </script>
    		</div>

## HTML Templates

    		const target = document.getElementById('source');
    		const tmpl = document.getElementById('tmpl');

    		/*  or  */
    		const root = target.attachShadow({mode:'open'});
    		root.appendChild(tmpl.content.cloneNode(true));

Ссылка на — пример

## HTML Templates

    		<!-- result -->
    		<div id="source">
    		    <h3>Заголовок: <slot name="title">Hello, World!</slot></h3>
    		    <img src="image.png" alt="My Image">
    		    <script>
    		        alert(1);
    		    </script>
    		</div>

## Custom Elements API v1

> Custom Elements API — позволяют создавать и определять API собственных HTML элементов

## Custom Elements API v1

## Custom Elements

    		customElements.define(tagName, constructor, options);

    		class MyHTMLElement extends HTMLElement {}
    		window.customElements.define('my-element', MyHTMLElement);

    		<!-- PROFIT -->
    		<my-element></my-element>

## Custom Elements

    		// "super" is not a valid custom element name
    		document.createElement('super')
    		                instanceof HTMLUnknownElement; // true

    		// "x-super" is a valid custom element name
    		document.createElement('x-super')
    		                instanceof HTMLElement; // true

## Custom Elements styling

    		x-super:defined {
    		    display: block;
    		}
    		x-super:not(:defined) {
    		    display: none;
    		}
    		x-super {
    		    color: black;
    		}

## Custom Elements

    		customElements.define('bigger-img', class extends Image {
    		    constructor(width=50, height=50) {
    		        super(width * 10, height * 10);
    		    }
    		}, {extends: 'img'});

    		<!-- This <img> is a bigger img. -->
    		<img is="bigger-img" width="15" height="20">

## Custom Elements

    		const BiggerImage = customElements.get('bigger-img');
    		const image = new BiggerImage(15, 20);
    		console.assert(image.width === 150);
    		console.assert(image.height === 200);

    		customElements.whenDefined('bigger-img')
    		    .then(() => {
    		        console.log('`bigger-img` ready!');
    		    });

## Custom Elements

    		customElements.define('my-element', class extends HTMLElement {
    		    constructor() {
    		        super();
    		    }
    		    connectedCallback() { ... }
    		    disconnectedCallback() { ... }
    		    adoptedCallback() { ... }
    		    ...

## Custom Elements

    		    ...
    		    get disabled() {
    		        return this.hasAttribute('disabled');
    		    }
    		    set disabled(value) {
    		        if (value) {
    		            return this.setAttribute('disabled', '');
    		        }
    		        this.removeAttribute('disabled');
    		    }
    		    ...

## Custom Elements

    		    ...
    		    attributeChangedCallback(attrName, oldVal, newVal) { ... }
    		    static get observedAttributes() {
    		        return ['open', 'disabled'];
    		    }
    		});

## Пример

## Polymer

## Полезные ссылки

- Custom Elements v1 на developers.google.com
- Shadow DOM на frontender.info и на developers.google.com
- Статьи на хабре про веб-компоненты, про Shadow DOM (немного устаревшие)
- Веб-компоненты: взгляд в будущее на learn.javascript.ru (устарело)
- webcomponents.org
- Easy Two-Way Data Binding in JavaScript
- Описание принципа работы VDOM и статья на медиуме

## Всем спасибо!