Технопарк, весна, 2019 г.
jQuery-style —
Dummy-компоненты — компоненты, которые либо вообще не содержат никакой логики (чисто визуальные компоненты), либо содержат логику, которая глубоко инкапсулирована внутри компонента
Например, компонент "Текст", компонент "Ссылка", компонент "Чекбокс"... Посложнее: компонент "Выезжающее меню", Компонент "Роутер".
Smart-компоненты — компоненты, которые управляют множеством других компонентов, содержат в себе бизнес-логику и хранят какое-то состояние
Компонент "Форма входа". Компонент "Всё приложение"
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>`;}}
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);}...
...tmpl() {return `<h1>Счётчик:</h1>${this.text.tmpl()}`;}}
App — smartRouter — dummyAuthorization — smartForm — dummyTextInput — dummyButton — dummyAbout — smartSimpleText — dummyLink — dummy
innerHTML вставляется в DOM документа
Связывание данных — это процесс, который устанавливает соединение между 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>
Главная проблема DOM — он никогда не был рассчитан для создания динамического пользовательского интерфейса.
Virtual DOM — это техника и набор библиотек / алгоритмов, которые позволяют нам улучшить производительность на клиентской стороне, избегая прямой работы с DOM путем создания и работы c абстракцией, имитирующей DOM-дерево
// before{tagName: 'div',classes: ['header'],attributes: {hidden: false}}
// after{tagName: 'div',classes: ['header'],attributes: {hidden: true}}
// Real DOM changediv.setAttribute('hidden', 'true')
Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM. Но только если мы делаем это правильно. Есть две проблемы:
Когда данные изменяются и нуждается в обновлении. Есть два варианта узнать, что данные изменились:
Что делает этот подход действительно быстрым:
Веб-компоненты — технология, которая позволяет создавать многократно используемые компоненты в веб-приложениях. Веб-компоненты поддерживаются веб-браузерами напрямую и не требуют дополнительных библиотек для работы . Модель веб-компонентов подразумевает инкапсуляцию и совместимость отдельных элементов
На данный момент частичная поддержка существует в браузерах Chrome, Firefox, Opera и Safari. Для браузеров не поддерживающих веб-компоненты реализованы полифиллы
Веб-компоненты включают в себя четыре технологии:
Shadow DOM — инструмент инкапсуляции HTML. Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным
<input type="range">
// Shadow DOM v0const root = element.createShadowRoot();// Shadow DOM v1const root = element.attachShadow({ mode: 'open' });// const root = element.attachShadow({ mode: 'close' });element.shadowRoot === root; // trueroot.innerHTML = ' ... ';
<!-- 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>`;
<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>
<!--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>`;
<div id="element2"><section><h1> <span>Header</span> </h1><p> <span>Lorem ipsum dolor sit.</span> </p></section></div>
#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 — это механизм для отложенного рендера клиентского контента, который не отображается во время загрузки, но может быть инициализирован при помощи JavaScript. Содержимое тегов
<template>парсится браузером, но отрабатывает только в момент вставки шаблона в DOM
<!--html--><template id="tmpl"><h3>Заголовок: <slot name="title"></slot></h3><img src="image.png" alt="My Image"><script>alert(1);</script></template>
<!--html--><div id="source"><span slot="title">Hello, World!</span></div>
const target = document.getElementById('source');const tmpl = document.getElementById('tmpl');// two waytarget.appendChild(tmpl.content.cloneNode(true));/* or */target.appendChild(document.importNode(tmpl.content, true));
Ссылка на — пример
<!-- 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>
const target = document.getElementById('source');const tmpl = document.getElementById('tmpl');/* or */const root = target.attachShadow({mode:'open'});root.appendChild(tmpl.content.cloneNode(true));
Ссылка на — пример
<!-- 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 — позволяют создавать и определять API собственных HTML элементов
customElements.define(tagName, constructor, options);class MyHTMLElement extends HTMLElement {}window.customElements.define('my-element', MyHTMLElement);
<!-- PROFIT --><my-element></my-element>
// "super" is not a valid custom element namedocument.createElement('super')instanceof HTMLUnknownElement; // true// "x-super" is a valid custom element namedocument.createElement('x-super')instanceof HTMLElement; // true
x-super:defined {display: block;}x-super:not(:defined) {display: none;}x-super {color: black;}
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">
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!');});
customElements.define('my-element', class extends HTMLElement {constructor() {super();}connectedCallback() { ... }disconnectedCallback() { ... }adoptedCallback() { ... }...
...get disabled() {return this.hasAttribute('disabled');}set disabled(value) {if (value) {return this.setAttribute('disabled', '');}this.removeAttribute('disabled');}...
...attributeChangedCallback(attrName, oldVal, newVal) { ... }static get observedAttributes() {return ['open', 'disabled'];}});
HTML Imports — позволяют импортировать фрагменты разметки из других файлов без использования AJAX и похожих способов
<!-- imported.html --><div id="loader"><span>Loading...</span></div><script>window.Module = class M { ... }</script>
<!-- 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();
<!-- imported.html --><style scoped>div { font-weight: 900; }</style><div id="loader"><span>Loading...</span></div>