Главная » Хабрахабр » UI framework за 5 минут

UI framework за 5 минут

Я довольно давно в IT и не помню чтоб UI библиотеки на других платформах рождались и умирали с такой же скоростью как в WEB. Некоторое время назад я задумался, почему так много UI frameworks для web? — были монстрами, которые развивались годами и не имели большого количества альтернатив. Библиотеки для настольных OS, такие как: MFC, QT, WPF, и т.д. В Web все не так — frameworks выходят чуть ли не каждую неделю, лидеры меняются — почему так происходит?

Да, для того чтобы написать библиотеку которой многие будут пользоваться — по прежнему требуется значительное время и экспертиза, но чтобы написать прототип — который будучи обернутый в удобный API — будет готов к использованию — требуется совсем немного времени. Думаю главная причина в том — что резко снизилась сложность написания UI библиотек. Если интересно как это можно сделать — читайте дальше.

Зачем эта статья?

Да за 30 строк у меня не получилось, но финальный результат вполне соразмерен с этой цифрой.
Вообще, цель статьи чисто образовательная. В свое время на Хабре была серия статей — написать Х за 30 строк кода на js.
Я подумал — а можно ли написать реакт за 30 строк? В этой статье я хочу показать как довольно просто сделать еще один UI Framework на основе виртуального дома. Она может помочь немного глубже понять принцип работы UI framework на основе виртуального дома.

Например некоторые считаю
что Angular и Ember это UI framework а React — это всего лишь библиотека которая позволят легче работать с view частью приложении В начале хочу сказать что я понимаю под UI framework — потому как у многих разное мнения на этот счет.

Определим UI framework так — это библиотека которая помогает создавать/обновлять/удалять страницы либо отдельные элементы страницы
в этом смысле довольно широкий спектр обертка над DOM API может оказаться UI framework, вопрос лишь в вариантах абстракции
(API) которые предоставляет эта библиотека для манипуляции с DOM и в эффективности этих манипуляций

В предложенной формулировке — React вполне является UI framework.

В упрощенном виде она заключается в том что узлы (node) реального DOM
строятся в четком соответствии с узлами предварительно построенного дерева виртуального DOM. Что ж, давайте посмотрим как написать свой React c блэкджеком и прочим.
Известно что React использует концепцию виртуального дома. Прямая манипуляция с реальным DOM
не приветствуется, в случае если необходимо внести изменения в реальным DOM, изменения вносятся в виртуальный DOM, потом новая
версия виртуальный DOM сравнивается со старой, собираются изменения которые необходимо применить к реальному DOM и они применяются
таким образом минимизируется взаимодействие с реальным DOM — что делает работу приложения более оптимальной
Поскольку дерево виртуального дома это обычный java-script объект — им довольно легко манипулировать — изменять/сравнивать его
узлы, под словом легко тут я понимаю что код сборки виртуальных но довольно простой и может быть частично сгенерирован
препроцессором из декларативного языка более высокого уровня JSX.

Начнем с JSX

так выглядит пример JSX кода

const Component = () => ( <div className="main"> <input /> <button onClick=> Submit </button> </div>
) export default Component

нам нужно сделать так чтобы при вызове функции Component создавался такой виртуальный DOM

const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] }

Он использует jsx-transform, который преобразует JSX примерно так: Конечно мы не будем писать это преобразование вручную, воспользуемся этим плагином, плагин устарел, но он достаточно прост, чтобы помочь нам понять как все работает.

jsx.fromString('<h1>Hello World</h1>', { factory: 'h'
});
// => 'h("h1", null, ["Hello World"])'

Ниже примитивная реализация такой функции так, все что нам нужно — реализовать конструктор vdom узлов h — функцию которая будет рекурсивно создавать узлы виртуального DOM
в случае реакт этим занимается функция React.createElement.

export function h(type, props, ...stack) { const children = (stack || []).reduce(addChild, []) props = props || {} return typeof type === "string" ? { type, props, children } : type(props, children)
} function addChild(acc, node) { if (Array.isArray(node)) { acc = node.reduce(addChild, acc) } else if (null == node || true === node || false === node) { } else { acc.push(typeof node === "number" ? node + "" : node) } return acc
}

конечно рекурсия здесь немного усложняет код, но надеюсь он понятен,
теперь с помощью этой функции мы можем собрать vdom

'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}

и так для узлов любой вложенности

Отлично теперь наша функция Component — возвращает узел vdom.
Теперь будет сложная часть, нам нужно написать функцию patch которая берет на вход корневой DOM элемент приложения, старый vdom, новый vdom — и осуществляет обновление узлов реального DOM в соответствии с новым vdom

возможно можно написать этот код проще, но получилось так
я взял за основу код из пакета picodom

export function patch(parent, oldNode, newNode) { return patchElement(parent, parent.children[0], oldNode, newNode)
}
function patchElement(parent, element, oldNode, node, isSVG, nextSibling) { if (oldNode == null) { element = parent.insertBefore(createElement(node, isSVG), element) } else if (node.type != oldNode.type) { const oldElement = element element = parent.insertBefore(createElement(node, isSVG), oldElement) removeElement(parent, oldElement, oldNode) } else { updateElement(element, oldNode.props, node.props) isSVG = isSVG || node.type === "svg" let childNodes = [] ; (element.childNodes || []).forEach(element => childNodes.push(element)) let oldNodeIdex = 0 if (node.children && node.children.length > 0) { for (var i = 0; i < node.children.length; i++) { if (oldNode.children && oldNodeIdex <= oldNode.children.length && (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type || (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex])) ) { patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG) oldNodeIdex++ } else { let newChild = element.insertBefore( createElement(node.children[i], isSVG), childNodes[oldNodeIdex] ) patchElement(element, newChild, {}, node.children[i], isSVG) } } } for (var i = oldNodeIdex; i < childNodes.length; i++) { removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {}) } } return element
}

Эта наивная реализация, она ужасно не оптимальна, не принимает во внимание идентификаторы элементов (key, id) — чтобы корректно обновлять нужные элементы в списках, но в примитивных случаях она работает норм

Реализацию функций createElement updateElement removeElement я тут не привожу она приметивна, кого заинтересует можно посмотреть исходники тут
Там есть единственный нюанс — когда обновляются свойства value для input элементов то сравнение нужно делать не со старой vnodе а с атрибутом value в реальном доме — это предотвратит обновление этого свойства у активного элемента (поскольку оно там уже и так обновлено) и предотвратит проблемы с курсором и выделением.

Ну вот и все теперь нам осталось только собрать эти кусочки вместе и написать UI Framework
Уложимся в 5 строк.

  1. как в React чтобы собрать приложение нам нужно 3 параметра
    export function app(selector, view, initProps) {
    selector — корневой селектор dom в который будет смонтировано приложение (по умолчанию 'body')
    view — функция которая конструирует корневой vnode
    initProps — начальные свойства приложения
  2. берем корневой элемент в DOM
    const rootElement = document.querySelector(selector || 'body')
  3. собираем vdom c начальными свойствами
    let node = view(initProps)
  4. монтируем полученный vdom в DOM в качестве старой vdom берем null
    patch(rootElement, null, node)
  5. возвращаем функцию обновления приложения с новыми свойствами
    return props => patch(rootElement, node, (node = view(props)))

Framework готов!

‘Hello world’ на этом Framework будет выглядеть таким образом

import { h, app } from "../src/index" function view(state) { return ( <div> <h2>{`Hello ${state}`}</h2> <input value={state} oninput={e => render(e.target.value)} /> </div> )
} const render = app('body', view, 'world')

Эта библиотека так же как React поддерживает композицию компонент, добавление, удаление компонент в момент исполнения, так что ее можно считать полноценным UI Framework
Чуть более сложный пример использования можно посмотреть тут ToDo example
Конечно в этой библиотеке много чего нет: событий жизненного цикла (хотя их не трудно прикрутить, мы же сами управляем созданием/обновлением/удалением узлов), отдельного обновления дочерних узлов по типу this.setState (для этого нужно сохранять ссылки на DOM элементы для каждого узла vdom — это немного усложнит логику), код patchElement ужасно неоптимальный, будет плохо работать на большом количестве элементов, не отслеживает элементы с идентификатором и т.д.
В любом случае, библиотека разрабатывалась в образовательных целях — не используйте ее в продакшене 🙂

PS: на эту статья меня вдохновила великолепная библиотека Hyperapp, часть кода взята оттуда

Удачного кодинга!


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Сетевой дайджест: 20 экспертных материалов о протоколах, стандартах и информационной безопасности

В эту подборку мы включили свежие посты, подготовленные специалистами компании VAS Experts. Главные темы подборки — сетевые протоколы, 5G и информационная безопасность. Под катом вы также найдете ряд рекомендаций по построению сетей операторов связи. / Pxhere / PD Про ИБ ...

[Перевод] Забудьте о мегаструктурах инопланетян: новые наблюдения объясняют поведение звезды Табби одной только пылью

Художественное изображение KIC 8462852, яркость которой за последние несколько лет менялась необычным образом Когда планета проходит перед её родительской звездой, если смотреть с нашей точки зрения, часть света звезды на некоторое время исчезает. Научная охота за планетами в XXI веке ...