Привет всем любителям покопаться в технологиях фронтенда! В этой статье я расскажу про то, как можно встроить 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 за редактуру и код-ревью.