Ванильный JSX

от автора

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

  • Работали с React, но не знаете, что у него под капотом.

  • Интересуетесь всей теорией, связанной с фронтендом.

  • Гик, создающий проекты на ванильном JS/TS.

Зачем? По большей части ради веселья! Вряд ли описанная идея может быть использована в реальных проектах без дополнительных переусложнений и создания нового фронтенд-фреймворка. Так что откройте в соседней вкладке GitHub-репозиторий с кодом проекта и устройтесь поудобнее. Впереди – глубокое погружение в JSX.

Что такое JSX?

JSX – это синтаксическая обертка над JS. Его нет в стандартах ECMAScript, так что инструменты вроде Babel и React занимаются его транспиляцией в обычный JavaScript код. Рассмотрим классический пример JSX:

const profile = (   <div>     <img src="avatar.png" className="profile" />     <h3>{[user.firstName, user.lastName].join(" ")}</h3>   </div> );

После прогона @babel/plugin-transform-react-jsx, код превратится в уже понятный браузерам JS:

const profile = React.createElement(   "div",   null,   React.createElement("img", { src: "avatar.png", className: "profile" }),   React.createElement("h3", null, [user.firstName, user.lastName].join(" ")) );

Как можно заметить, Babel успешно превратил JSX в аккуратную функцию React.createElement, состоящую из тега-обертки, его свойств (в примере – null) и дочерних элементов, которые тоже, в свою очередь, создаются с помощью этой функции.

Фреймворки React, Vue, Solid умеют обрабатывать JSX, но делают это по-разному. Все потому что у них разные реализации функции createElement, которую по-другому называют JSX Pragma. Узнав об этом, я сразу захотел создать свою прагму.

Парсинг JSX

Прежде чем прыгать в создание прагмы, нужно научиться парсить JSX. Для небольшого и современного проекта, где не требуется поддержка старых браузеров, не хочется использовать Babel. Благо развернуть легкий и быстрый проект всегда можно с Vite и TypeScript. Ну, или с чем-то одним.

Vite – современный сборщик проектов. Его особенность заключается в том, что, в отличие от Webpack, он поставляет исходный код через ES модули. Чтобы развернуть проект на Vite и TypeScript, нужно лишь выполнить команду:

npm create vite@latest

По умолчанию и Vite, и TypeScript, увидев файлы .jsx или .tsx, будут парсить JSX внутри них и подставлять функцию React.createElement. Чтобы направить их на кастомную функцию, нужно поменять настройки в tsconfig.json.

{   "compilerOptions": {     "jsx": "preserve",     "jsxFactory": "h",     "jsxFragmentFactory": "Fragment"   } }

Или, если вы пишете проект без TypeScript, настройте vite.config.js.

import { defineConfig } from 'vite';  export default defineConfig({   esbuild: {     jsxFactory: 'h',     jsxFragment: 'Fragment'   } });

Эти свойства скажут парсеру, что при обработке JSX должна использоваться функция h (от слова hyperscript, hypertext + javascript), если в JSX только один родительский элемент, и Fragment, если их несколько.

JSX Pragma

Настроив парсер на обработку функции h, попробуем создать ее в src/pragma.ts.

// Тег может быть как строкой, так и функцией – если парсим  // функциональный компонент type Tag = string | ((props: any, children: any[]) => JSX.Element);  // Атрибуты элемента – объект либо null type Props = Record<string, string> | null;  // Дети элемента – возвращаемое значение функции h() type Children = (Node | string)[];  export const h = (tag: Tag, props: Props, ...children: Children) => {   // Если тег – компонент, вызываем его   if (typeof tag === 'function') {     return tag({ ... props }, children);   }    // Создаем html-элемент с переданными атрибутами   const el = document.createElement(tag);   if (props) {     Object.entries(props).forEach(([key, val]) => {       if (key === 'className') {         el.classList.add(...(val as string || '').trim().split(' '));         return;       }        el.setAttribute(key, val);     });   }    // Добавляем дочерние элементы к родительскому   children.forEach((child) => {     el.append(child);   });    return el; };

Функция h, как и createElement, принимает название тега (или функциональный компонент), атрибуты и результат выполнения функции h для дочерних элементов.

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

import { h } from '../pragma'; import { LikeComponent } from './like';  export const App = (   <main className="hello">     <h1>       Hello JSX!     </h1>     <LikeComponent big />   </main> );

Осталось лишь отобразить наше приложение в HTML:

import { App } from './components/app';  const app = document.querySelector<HTMLDivElement>('#app')! app.append(App);

Готово! Теперь TypeScript парсит JSX, а прагма правильно формирует DOM для отображения простейшей верстки, разбитой по JSX-компонентам!

Практическое применение

Как было сказано в начале статьи, это приложение не предназначено для реальных проектов. Оно лишь показывает, как просто можно обрабатывать JSX без использования рантайм-библиотек, буквально работая в ванильном JS.

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

Заключение

Как выяснилось, такой нестандартный концепт как JSX Pragma довольно легко можно собрать своими руками в домашних условиях. Я призываю вас экспериментировать со всеми технологиями, до которых доходят руки, углубляться в них.

Также предлагаю вам подписаться на мой телеграм-канал, где я стараюсь регулярно освещать новые технологии во фронтенде. Будет интересно даже искушенным читателям.

Спасибо @illright за редактуру и код-ревью.


ссылка на оригинал статьи https://habr.com/ru/post/659483/


Комментарии

Добавить комментарий

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