Продвинутые подходы разработки SPA

Технопарк, осень, 2019 г.

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

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

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

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

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

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

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

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

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

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

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

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 перерисовывать только то, что действительно необходимо

Связывание данных (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
		        type="text"
		        placeholder="Сколько вам лет?"
		        data-bind="value:age"
		    />
		</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
		        type="text"
		        placeholder="Сколько вам лет?"
		        data-bind-event="input:updateAgeListener"
		    />
		    <input type="submit" />
		</form>
		 
	

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

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

Virtual DOM

Virtual DOM

Virtual DOM

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

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

Virtual DOM

Virtual DOM

Virtual 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. Но только если мы делаем это правильно. Есть две проблемы:

Когда?

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

Как?

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

Virtual DOM

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

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

Перерыв

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

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

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

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

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

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

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 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'];
		    }
		});
		 
	

HTML Imports

HTML Imports — позволяют импортировать фрагменты разметки из других файлов без использования AJAX и похожих способов

HTML Imports

HTML Imports

		<!-- imported.html -->
		<div id="loader">
		    <span>Loading...</span>
		</div>
		<script>
		    window.Module = class M { ... }
		</script>
		 
	

HTML Imports

		<!-- index.html -->
		<head>
		    <link rel="import" href="imported.html">
		</head>
		 
	
		const link = document.querySelector('link[rel=import]');
		const content = link.import.querySelector('#loader');
		document.body.appendChild(content.cloneNode(true));
		const module = new Module();
		 
	

HTML Imports

		<!-- imported.html -->
		<style scoped>
		    div { font-weight: 900; }
		</style>
		<div id="loader">
		    <span>Loading...</span>
		</div>
		 
	

Пример

Polymer

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

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