UI framework за 5 минут


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


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


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


    В свое время на Хабре была серия статей — написать Х за 30 строк кода на js.


    Я подумал — а можно ли написать реакт за 30 строк? Да за 30 строк у меня не получилось, но финальный результат вполне соразмерен с этой цифрой.


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


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


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


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


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


    Поскольку дерево виртуального дома это обычный java-script объект — им довольно легко манипулировать — изменять/сравнивать его узлы, под словом легко тут я понимаю что код сборки виртуальных но довольно простой и может быть частично сгенерирован препроцессором из декларативного языка более высокого уровня JSX.


    Начнем с JSX


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


    const Component = () => (
      <div className="main">
        <input />
        <button onClick={() => console.log('yo')}> 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, часть кода взята оттуда.


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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 22

      +3
      Заглавная картинка и содержание напомнили об этом :)

      image
        +3
        Вы написали не фреймворк, а лишь view компонент, на идее реакта jsx -> vdom -> dom
          –1
          я же определил в статье что понимаю под понятием фреймворк, так что эта библиотека вполне укладывается в это определение
          разумеется фреймворк можно определить иначе, тогда комментарий будет иметь смысл
            +3
            То что вы определили своё понятие термина framework ещё не говорит о том, что это истина. Всё же есть официальное определение термина, это не парадигма, чтобы трактовать как-то иначе. Если я курицу назову орлом, орлом от этого она не станет. У вас именно библиотека, фреймворк задаёт архитектуру приложения и это главная отличительная черта от библиотеки. У вас архитектура никак не задана. Архитектура поразумевает не только отображение данных.
              0
              фреймворк задаёт архитектуру приложения и это главная отличительная черта от библиотеки.

              Мне больше нравиться такое определение: «В случае библиотеки — код вызывает библиотеку. В случае фреймворка — фреймворк вызывает код».
                0
                В случае фреймворка — фреймворк вызывает код

                Хм, странно звучит. Фреймворк ведь надо для начала проинициализировать так или иначе. Так что так или иначе изначально будет вызван код фреймворка из пользовательского кода.
                  0
                  Поинт в том, что в случае фрейма обычное дело, когда твой код вызывается фреймом, и никуда ты от этого не денешься. В случае библиотеки такого просто не происходит, хочешь вызываешь, не хочешь — не вызываешь.
                    +1
                    Читайте дядюшку Боба…
                  0
                  Т.е. если либа просто дергает твой код и что-то вставляет в страницу, то это уже фреймворк?
                  И для такого «фреймворка» нужна еще куча костылей чтобы написать работающее приложение.
                    0
                    Это как «либа просто дергает твой код»? Откуда она узнала, что нужно что-то дернуть? Наверное ты сначала отдал этой либе лямбду, которую она и дернула, правильно? Не отдал бы — не дернула. В случае фреймворка вполне нормальна ситуация, что ты обязан предоставить такие лямбды, иначе он просто не заработает. Это, в общем-то, и есть то, что называют «фреймворк определяет архитектуру».
                      0
                      Еще раз, этот фреймворк умеет делать только эти 2 вещи, но приложение на нем не напишешь, почему вы продолжаете называть «это» фреймворком?
                      Только из-за одного свойства?
                      Тогда что такое .Net Framework? Супер фреймворк? Потому что он умеет не только вызывать ваш код?
                        0
                        Какой «этот» фреймворк? Вы о чем уже? Я перестаю вас понимать.
                        Все, что я сказал — уточнил различие между библиотекой и фреймворком. Абстрактной библиотекой и абстрактным фреймворком. Отличие, наверняка, не единственное, но одно из, как мне кажется, важных. Если вы с этим не согласны, то ок, я не настаиваю. Было бы, конечно, любопытно услышать ваше мнение по поводу того, в чем фундаментальное отличие библиотеки от фреймворка, но если нет, то нет.
                          0
                          Под «это» я подразумеваю пример в статье и Реакт.

                          Тогда по вашей логике, если взять jQuery, научить запускать указанный код при вставке куска HTML в страницу, то можно смело объявить это фреймворком?
                          Условие выполняется, библиотека запускает ваш код, можно объявлять фреймворком?
                            0
                            Тогда по вашей логике, если взять jQuery, научить запускать указанный код при вставке куска HTML в страницу, то можно смело объявить это фреймворком?

                            Зачем вы пытаетесь приписать мне какую-то ерунду? Давайте сойдемся на том, что вы правы, я — нет, ок?
                              0
                              Я просто хочу понять логику, почему люди начинают называть что-то чем-то, если оно таким не является.

                              Или тогда уже надо исправить формулировку, что фреймворк предоставляет Широкий функционал, тогда уже это будет правильно.
                                0
                                Тогда, наверное, вы ошиблись веткой, поскольку я ничего ничем не называл. Dixi.
            0
            +1 думающий что манипуляция с элементами страницы, без набора хелперов и компонентов, это уже фреймворк(Реакт туда же).
              0
                +2
                В Web все не так — frameworks выходят чуть ли не каждую неделю, лидеры меняются — почему так происходит?

                Этот абзац вы еще в 2015 году писать начали? За последние два года никаких новых лидеров у нас не появилось.


                А вообще статья полезная, объясняет, что делают фреймворки под капотом, что никакой магии в них нет.

                  0
                  Быстро устаревает это как? Потому что ангулар вышел в 2010, реакт не помню точно, но вроде в 2013, как бы в мире где еще в 2001 все верстали под IE6 это очень много времени.
                    –1
                    Думаю главная причина в том — что резко снизилась сложность написания UI библиотек.

                    Да нет, просто в контексте веба под "гуи фреймворком" понимают тоненький рендер слой, а в контексте десктопа — полноценную библиотеку компонент с архитектурной обвязкой. Фактически, единственный продукт, который хоть с натяжкой тянет на полноценный гуи-фреймворк — это ExtJs. Все остальное — ну, наколеночные поделки по меркам десктопа. Конечно же, лепить наколеночные поделки можно легко и быстро.

                      0
                      роза пахнет розой, хоть розой назови хоть нет ))

                      Only users with full accounts can post comments. Log in, please.