Эмуляция React и JSX в Vanilla JS

Опубликовано: 2022-03-11

Мало кто не любит фреймворки, но даже если вы один из них, вы должны взять на заметку и принять функции, которые немного облегчают жизнь.

Раньше я был противником использования фреймворков. Однако в последнее время у меня появился опыт работы с React и Angular в некоторых моих проектах. Первые пару раз, когда я открывал свой редактор кода и начинал писать код в Angular, это казалось странным и неестественным; особенно после более чем десяти лет кодирования без использования каких-либо фреймворков. Через некоторое время я решил заняться изучением этих технологий. Очень быстро стала очевидной одна большая разница: было так легко манипулировать DOM, так легко регулировать порядок узлов, когда это необходимо, и не требовалось много страниц кода для создания пользовательского интерфейса.

Хотя я по-прежнему предпочитаю свободу не быть привязанным к фреймворку или архитектуре, я не мог игнорировать тот факт, что создавать элементы DOM в одном гораздо удобнее. Поэтому я начал искать способы эмулировать опыт в vanilla JS. Моя цель — извлечь некоторые из этих идей из React и продемонстрировать, как те же принципы могут быть реализованы в простом JavaScript (часто называемом ванильным JS), чтобы немного облегчить жизнь разработчика. Для этого создадим простое приложение для просмотра проектов GitHub.

Простое приложение поиска GitHub

Приложение, которое мы создаем.

Каким бы способом мы ни создавали интерфейс с помощью JavaScript, мы будем получать доступ к DOM и манипулировать им. Для нашего приложения нам нужно будет создать представление каждого репозитория (миниатюра, имя и описание) и добавить его в DOM как элемент списка. Мы будем использовать GitHub Search API для получения наших результатов. И, поскольку мы говорим о JavaScript, давайте поищем в репозиториях JavaScript. Когда мы запрашиваем API, мы получаем следующий ответ JSON:

 { "total_count": 398819, "incomplete_results": false, "items": [ { "id": 28457823, "name": "freeCodeCamp", "full_name": "freeCodeCamp/freeCodeCamp", "owner": { "login": "freeCodeCamp", "id": 9892522, "avatar_url": "https://avatars0.githubusercontent.com/u/9892522?v=4", "gravatar_id": "", "url": "https://api.github.com/users/freeCodeCamp", "site_admin": false }, "private": false, "html_url": "https://github.com/freeCodeCamp/freeCodeCamp", "description": "The https://freeCodeCamp.org open source codebase "+ "and curriculum. Learn to code and help nonprofits.", // more omitted information }, //... ] }

Подход React

React позволяет очень просто записывать HTML-элементы на страницу, и это одна из функций, которые я всегда хотел иметь при написании компонентов на чистом JavaScript. React использует JSX, который очень похож на обычный HTML.

Однако это не то, что читает браузер.

Под капотом React преобразует JSX в набор вызовов функции React.createElement . Давайте рассмотрим пример JSX с использованием одного элемента из GitHub API и посмотрим, во что он транслируется.

 <div className="repository"> <div>{item.name}</div> <p>{item.description}</p> <img src={item.owner.avatar_url} /> </div>;
 ; React.createElement( "div", { className: "repository" }, React.createElement( "div", null, item.name ), React.createElement( "p", null, item.description ), React.createElement( "img", { src: item.owner.avatar_url } ) );

JSX очень прост. Вы пишете обычный HTML-код и вводите данные из объекта, добавляя фигурные скобки. Код JavaScript в квадратных скобках будет выполнен, и значение будет вставлено в результирующий DOM. Одним из преимуществ JSX является то, что React создает виртуальный DOM (виртуальное представление страницы) для отслеживания изменений и обновлений. Вместо того, чтобы переписывать весь HTML, React изменяет DOM страницы всякий раз, когда информация обновляется. Это одна из основных проблем, для решения которой был создан React.

jQuery-подход

Разработчики часто использовали jQuery. Я хотел бы упомянуть его здесь, потому что он по-прежнему популярен, а также потому, что он довольно близок к решению на чистом JavaScript. jQuery получает ссылку на узел DOM (или набор узлов DOM), запрашивая DOM. Он также оборачивает эту ссылку различными функциями для изменения ее содержимого.

В то время как jQuery имеет свои собственные инструменты построения DOM, то, что я чаще всего вижу в дикой природе, — это просто конкатенация HTML. Например, мы можем вставить HTML-код в выбранные узлы, вызвав функцию html() . Согласно документации jQuery, если мы хотим изменить содержимое узла div с помощью demo-container класса, мы можем сделать это следующим образом:

 $( "div.demo-container" ).html( "<p>All new content.<em>You bet!</em></p>" );

Такой подход упрощает создание элементов DOM; однако, когда нам нужно обновить узлы, нам нужно запросить нужные нам узлы или (чаще) вернуться к воссозданию всего фрагмента всякий раз, когда требуется обновление.

Подход DOM API

JavaScript в браузерах имеет встроенный DOM API, который дает нам прямой доступ к созданию, изменению и удалению узлов на странице. Это отражено в подходе React, и, используя DOM API, мы на один шаг приближаемся к преимуществам этого подхода. Мы изменяем только те элементы страницы, которые действительно требуют изменения. Однако React также отслеживает отдельный виртуальный DOM. Сравнивая различия между виртуальным и реальным DOM, React затем может определить, какие части требуют модификации.

Эти дополнительные шаги иногда полезны, но не всегда, и прямое управление DOM может быть более эффективным. Мы можем создавать новые узлы DOM, используя функцию _document.createElement_ , которая вернет ссылку на созданный узел. Отслеживание этих ссылок дает нам простой способ изменить только те узлы, которые содержат часть, которую необходимо обновить.

Используя ту же структуру и источник данных, что и в примере JSX, мы можем построить DOM следующим образом:

 var item = document.createElement('div'); item.className = 'repository'; var nameNode = document.createElement('div'); nameNode.innerHTML = item.name item.appendChild(nameNode); var description = document.createElement('p'); description.innerHTML = item.description; item.appendChild(description ); var image = new Image(); Image.src = item.owner.avatar_url; item.appendChild(image); document.body.appendChild(item);

Если вы думаете только об эффективности выполнения кода, то этот подход очень хорош. Однако эффективность измеряется не только скоростью выполнения, но и простотой обслуживания, масштабируемостью и пластичностью. Проблема с этим подходом заключается в том, что он очень многословен и иногда запутан. Нам нужно написать кучу вызовов функций, даже если мы просто создаем базовую структуру. Второй большой недостаток — огромное количество создаваемых и отслеживаемых переменных. Допустим, компонент, над которым вы работаете, содержит 30 элементов DOM, вам потребуется создать и использовать 30 различных элементов и переменных DOM. Вы можете повторно использовать некоторые из них и немного подтасовывать за счет ремонтопригодности и пластичности, но это может стать очень грязным, очень быстро.

Еще один существенный недостаток связан с количеством строк кода, которые необходимо написать. Со временем становится все труднее и труднее перемещать элементы от одного родителя к другому. Это одна вещь, которую я действительно ценю в React. Я могу просмотреть синтаксис JSX и за несколько секунд узнать, какой узел содержится, где и при необходимости изменить. И хотя поначалу может показаться, что это не имеет большого значения, в большинстве проектов есть постоянные изменения, которые заставят вас искать лучший способ.

Предложенное решение

Работа напрямую с DOM работает и выполняет свою работу, но также делает построение страницы очень подробным, особенно когда нам нужно добавить атрибуты HTML и вложенные узлы. Итак, идея состоит в том, чтобы использовать некоторые преимущества работы с такими технологиями, как JSX, и сделать нашу жизнь проще. Мы пытаемся воспроизвести следующие преимущества:

  1. Пишите код в синтаксисе HTML, чтобы создание элементов DOM было легко читать и изменять.
  2. Поскольку мы не используем эквивалент виртуального DOM, как в случае с React, нам нужен простой способ указывать и отслеживать интересующие нас узлы.

Вот простая функция, которая выполняет это с помощью фрагмента HTML.

 Browser.DOM = function (html, scope) { // Creates empty node and injects html string using .innerHTML // in case the variable isn't a string we assume is already a node var node; if (html.constructor === String) { var node = document.createElement('div'); node.innerHTML = html; } else { node = html; } // Creates of uses and object to which we will create variables // that will point to the created nodes var _scope = scope || {}; // Recursive function that will read every node and when a node // contains the var attribute add a reference in the scope object function toScope(node, scope) { var children = node.children; for (var iChild = 0; iChild < children.length; iChild++) { if (children[iChild].getAttribute('var')) { var names = children[iChild].getAttribute('var').split('.'); var obj = scope; while (names.length > 0) { var _property = names.shift(); if (names.length == 0) { obj[_property] = children[iChild]; } else { if (!obj.hasOwnProperty(_property)){ obj[_property] = {}; } obj = obj[_property]; } } } toScope(children[iChild], scope); } } toScope(node, _scope); if (html.constructor != String) { return html; } // If the node in the highest hierarchy is one return it if (node.childNodes.length == 1) { // if a scope to add node variables is not set // attach the object we created into the highest hierarchy node // by adding the nodes property. if (!scope) { node.childNodes[0].nodes = _scope; } return node.childNodes[0]; } // if the node in highest hierarchy is more than one return a fragment var fragment = document.createDocumentFragment(); var children = node.childNodes; // add notes into DocumentFragment while (children.length > 0) { if (fragment.append){ fragment.append(children[0]); }else{ fragment.appendChild(children[0]); } } fragment.nodes = _scope; return fragment; }

Идея проста, но мощна; мы отправляем функции HTML, который мы хотим создать, в виде строки, в строку HTML мы добавляем атрибут var для узлов, которые мы хотим создать для нас. Второй параметр — это объект, в котором будут храниться эти ссылки. Если не указано, мы создадим свойство «узлы» для возвращаемого узла или фрагмента документа (в случае, если самый высокий узел иерархии больше одного). Все выполняется менее чем в 60 строках кода.

Функция работает в три этапа:

  1. Создайте новый пустой узел и используйте innerHTML в этом новом узле, чтобы создать всю структуру DOM.
  2. Переберите узлы и, если атрибут var существует, добавьте свойство в объект области видимости, указывающее на узел с этим атрибутом.
  3. Возвращает верхний узел в иерархии или фрагмент документа, если их несколько.

Итак, как теперь выглядит наш код для рендеринга примера?

 var UI = {}; var template = ''; template += '<div class="repository">' template += ' <div var="name"></div>'; template += ' <p var="text"></p>' template += ' <img var="image"/>' template += '</div>'; var item = Browser.DOM(template, UI); UI.name.innerHTML = data.name; UI.text.innerHTML = data.description; UI.image.src = data.owner.avatar_url;

Во-первых, мы определяем объект (UI), где мы будем хранить ссылки на созданные узлы. Затем мы составляем HTML-шаблон, который будем использовать, в виде строки, отмечая целевые узлы атрибутом «var». После этого вызываем функцию Browser.DOM с шаблоном и пустым объектом, в котором будут храниться ссылки. Наконец, мы используем сохраненные ссылки для размещения данных внутри узлов.

Этот подход также разделяет построение структуры DOM и вставку данных на отдельные шаги, что помогает поддерживать организованность и хорошую структуру кода. Это позволяет нам отдельно создавать структуру DOM и заполнять (или обновлять) данные, когда они становятся доступными.

Заключение

Хотя некоторым из нас не нравится идея перехода на фреймворки и передачи управления, важно осознавать преимущества, которые приносят эти фреймворки. Есть причина, по которой они так популярны.

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