Технопарк, весна, 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 — smart
Router — dummy
Authorization — smart
Form — dummy
TextInput — dummy
Button — dummy
About — smart
SimpleText — dummy
Link — 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 change
div.setAttribute('hidden', 'true')
Такой подход работает быстрее, потому как не включает в себя все тяжеловесные части реального DOM. Но только если мы делаем это правильно. Есть две проблемы:
Когда данные изменяются и нуждается в обновлении. Есть два варианта узнать, что данные изменились:
Что делает этот подход действительно быстрым:
Веб-компоненты — технология, которая позволяет создавать многократно используемые компоненты в веб-приложениях. Веб-компоненты поддерживаются веб-браузерами напрямую и не требуют дополнительных библиотек для работы . Модель веб-компонентов подразумевает инкапсуляцию и совместимость отдельных элементов
На данный момент частичная поддержка существует в браузерах Chrome, Firefox, Opera и Safari. Для браузеров не поддерживающих веб-компоненты реализованы полифиллы
Веб-компоненты включают в себя четыре технологии:
Shadow DOM — инструмент инкапсуляции HTML. Shadow DOM позволяет изменять внутреннее представление HTML элементов, оставляя внешнее представление неизменным
<input type="range">
// 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 = ' ... ';
<!-- 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 way
target.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 name
document.createElement('super')
instanceof HTMLUnknownElement; // true
// "x-super" is a valid custom element name
document.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>