• Время чтения ~5 мин
  • 06.07.2023

Я лично большой поклонник JSX и мне нравится то, как он позволяет мне разделять и компоновать мой код. Несмотря на то, что JSX существовал до React, он не был бы настолько популярен, если бы React не поднял его. Однако на самом деле мы можем использовать JSX без React, и это тоже не сложно.

React работает, настраивая свой упаковщик для преобразования JSX в вызовы функции createElement. Так, например:

const foo = (
    <div className="cool">
        <p>Hello there!</p>
    </div>
)
// Would become this:
React.createElement(
    'div',
    { className: 'cool' },
    React.createElement('p', null, 'Hello there!')
)

Однако большинство транспилеров позволяют вам выбрать свою собственную JSX pragma (функция, которая будет вместо React.createElement). Например, если вы используете Babel, вы можете указать, какую функцию использовать, с помощью простого комментария:  

/** @jsx myJsxFunction */
const foo = (
    <div className="cool">
        <p>Hello there!</p>
    </div>
)

И теперь Babel передаст некоторые параметры myJsxFunction. Теперь все, что нам нужно сделать, это создать функцию, которая принимает эти параметры и создать реальные DOM-узлы, которые мы можем добавить в нашу DOM. Итак, начнем. 

DOM-узлы создаются с использованием функции document.createNode(), и для этого требуется только тег, так что хорошо было бы начать именно с этого:

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    return element
}

Теперь, когда у нас есть DOM-узел, мы должны фактически добавить предоставленные нам атрибуты. Это может быть что угодно вроде class или style. Таким образом, мы просто пройдемся по всем предоставленным атрибутам (используя Object.entries и просто установим их на нашем DOM-узле):  

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        element.setAttribute(name, value.toString())
    })
    return element
}

Однако у этого подхода есть одна проблема. Например, как мы обработаем события, если у меня есть этот JSX:

const SayHello = (
    <div>
        <button onClick={() => console.log("hello there!")}>Say Hello</button>
    </div>
)

Наша функция установит onClick как обычный атрибут с обратным вызовом как фактический текст. Вместо этого мы можем проверить, начинается ли наш атрибут с «on» и находится ли он в области видимости окна. Это покажет нам, является ли это событием или нет. Например onclick, в области видимости окна, однако onfoo, нет. Если это так, то мы можем зарегистрировать обработчик событий на этом узле, используя часть имени без 'on'.

Вот как это выглядит:

export const createElement = (tag, props, ...children) => {
  const element = document.createElement(tag)
  Object.entries(props || {}).forEach(([name, value]) => {
      if (name.startsWith('on') && name.toLowerCase() in window)
          element.addEventListener(name.toLowerCase().substr(2), value)
      else element.setAttribute(name, value.toString())
  })
  return element
}

Прекрасно! Теперь все, что осталось сделать, это добавить все дочерние элементы к родителю. Однако вы не можете добавить строку к узлу DOM, поэтому в случае, если дочерний элемент также не является узлом, мы можем создать текстовый узел и добавить его вместо этого:

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        if (name.startsWith('on') && name.toLowerCase() in window)
            element.addEventListener(name.toLowerCase().substr(2), value)
        else element.setAttribute(name, value.toString())
    })
    children.forEach(child => {
        element.appendChild(
            child.nodeType === undefined
                ? document.createTextNode(child.toString())
                : child
        )
    })
    return element
} 

Однако это быстро приводит к проблемам с глубоко вложенными элементами, а также с элементами, которые создаются с использованием карт массивов. Поэтому вместо этого давайте заменим эту часть рекурсивным методом appendChild:  

const appendChild = (parent, child) => {
  if (Array.isArray(child))
    child.forEach(nestedChild => appendChild(parent, nestedChild));
  else
    parent.appendChild(child.nodeType ? child : document.createTextNode(child));
};

И теперь мы можем использовать это вместо нашего старого метода:

export const createElement = (tag, props, ...children) => {
    const element = document.createElement(tag)
    Object.entries(props || {}).forEach(([name, value]) => {
        if (name.startsWith('on') && name.toLowerCase() in window)
            element.addEventListener(name.toLowerCase().substr(2), value)
        else element.setAttribute(name, value.toString())
    })
    children.forEach(child => {
          appendChild(element, child);
      });
    return element
}

Это работает! Попробуйте. Теперь мы можем сделать базовый JSX для DOM:

import { createElement } from "./Vanilla"
/** @jsx createElement */
const App = (
    <div>
        <p>My awesome app :)</p>
    </div>
)
document.getElementById("root").appendChild(App)

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

К счастью, это довольно просто реализовать. Все, что нам нужно сделать, это проверить, является ли тег функцией, а не строкой. Если это так, мы не делаем ничего другого, а просто вызываем функцию. Вот как это выглядит:

export const createElement = (tag, props, ...children) => {
    if (typeof tag === "function") return tag(props, children)
    {...}
}

А теперь давайте попробуем это:

import { createElement } from "./Vanilla"
/** @jsx createElement */
const SayHello = props => (
    <div>
        <h3>Hello {props ? props.name : "world"}</h3>
        <p>I hope you're having a good day</p>
    </div>
)
/* <Component /> === Component() */
document.getElementById("root").appendChild(<SayHello name="foo" />)

Как видите, реализация позволила нам использовать реквизит! Вы можете сказать, что мы закончили, но есть еще одна функция, которую я хочу реализовать, и это фрагменты. Для тех, кто не знаком с фрагментами, это способ иметь пустые контейнеры в JSX, и они используют пустые теги. Пример:

/** @jsx createElement */
/** @jsxFrag createFragment */
const UsingFragment = () => (
    <div>
        <p>This is regular paragraph</p>
        <>
            <p>This is a paragraph in a fragment</p>
        </>
    </div>
)

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

const createFragment = (props, ...children) => {
  return children;
}

Это работает из-за нашего рекурсивного метода appendChild.

И это все! Мы сделали это. Супер простая функция JSX to DOM, которая позволяет нам использовать мощь OSX без необходимости специально использовать react. Вы можете найти исходный код здесь.

Я надеюсь, что вы нашли этот пост полезным, и я также надеюсь, что вы найдете несколько классных способов использовать всю мощь JSX. На самом деле я узнал обо всем этом, работая над Dhow, который является генератором статического сайта на JSX для Node.js. Он в основном позволяет вам писать код в стиле Next.js, но преобразует его в статический HTML без проблем с гидратацией. Проверьте это и дайте мне знать, что вы думаете. 

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297