Технопарк, осень, 2024 г.
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. Но только если мы делаем это правильно. Есть две проблемы:
Когда данные изменяются и нуждается в обновлении. Есть два варианта узнать, что данные изменились:
Что делает этот подход действительно быстрым:
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);
}
}
// Создаём детей
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);
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);
}
}
}
}
Ключ — специальный строковый атрибут, семантически идентифицирующий элемент среди соседних.
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 ( -- Прололжение на следующем слайде
} else if (
!fromString && !toString && prevVchild && prevVchild.key === vchild.key
) {
// Ключи совпадают -- можем обновить оптимально
update(node.childNodes[i], vchild);
} else {
// Если строка превращается в элемент или наоборот
// Либо у элементов не совпали ключи
// Вставляем новую ноду перед текущей, старая нода при этом "всптывает" в конец
node.insertBefore(create(vchild), node.childNodes[i]);
// Сдвигаем индекс сравнения массива старых чайлдов
j--;
}
}
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() {}
}
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);
}
}
}
Веб-компоненты — технология, которая позволяет создавать многократно используемые компоненты в веб-приложениях. Веб-компоненты поддерживаются веб-браузерами напрямую и не требуют дополнительных библиотек для работы . Модель веб-компонентов подразумевает инкапсуляцию и совместимость отдельных элементов
На данный момент частичная поддержка существует в браузерах Chrome, Firefox, Opera и Safari. Для браузеров не поддерживающих веб-компоненты реализованы полифиллы
Веб-компоненты включают в себя 3 технологии:
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'];
}
});