Haunted — это современная библиотека для создания веб-компонентов с использованием функционального подхода, вдохновлённого React Hooks, но встроенного в нативные веб-компоненты. Она позволяет писать компоненты как чистые функции с реактивным состоянием и жизненным циклом, полностью интегрируясь с DOM без необходимости виртуального дерева.
Компоненты Haunted создаются с помощью функции
component, которая принимает функцию с логикой компонента и
возвращает класс веб-компонента. Основной принцип работы — реактивное
обновление DOM при изменении состояния.
import { component, html, useState } from 'haunted';
function Counter() {
const [count, setCount] = useState(0);
return html`
<div>
<p>Счёт: ${count}</p>
<button @click=${() => setCount(count + 1)}>Увеличить</button>
</div>
`;
}
customElements.define('my-counter', component(Counter));
Ключевые моменты:
html — функция-шаблонизатор для создания DOM.useState — хук состояния, аналогичный React.Haunted предоставляет несколько встроенных хуков:
Пример использования useEffect:
import { component, html, useState, useEffect } from 'haunted';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => setSeconds(s => s + 1), 1000);
return () => clearInterval(interval);
}, []);
return html`<p>Прошло секунд: ${seconds}</p>`;
}
customElements.define('my-timer', component(Timer));
Изоморфизм в контексте Haunted означает возможность использовать один
и тот же компонентный код как на сервере, так и на клиенте, с корректной
генерацией HTML и поддержкой интерактивности после гидратации. Haunted
поддерживает серверный рендеринг через функцию render из
пакета haunted/ssr.
import { render } from 'haunted/ssr';
import { Counter } from './components/counter.js';
const htmlString = render(Counter);
console.log(htmlString);
Особенности:
render возвращает строку HTML, которая может быть
вставлена в шаблон сервера.Чтобы сделать серверный HTML интерактивным на клиенте, используется
обычная регистрация компонента через
customElements.define:
import { component } from 'haunted';
import { Counter } from './components/counter.js';
customElements.define('my-counter', component(Counter));
После загрузки страницы браузер «гидрирует» серверный HTML, связывая его с функциональностью Haunted-компонента. Важно, что структура DOM, сгенерированная сервером, должна точно соответствовать структуре, которую создаст клиентский компонент.
window, document или сторонними библиотеками
должны выполняться только на клиенте внутри useEffect.Пример условной работы с браузерными объектами:
import { component, html, useEffect, useState } from 'haunted';
function WindowWidth() {
const [width, setWidth] = useState(0);
useEffect(() => {
function updateWidth() {
setWidth(window.innerWidth);
}
updateWidth();
window.addEventListener('resize', updateWidth);
return () => window.removeEventListener('resize', updateWidth);
}, []);
return html`<p>Ширина окна: ${width}</p>`;
}
customElements.define('window-width', component(WindowWidth));
На сервере значение width останется 0, а на
клиенте произойдет корректная гидратация и последующее обновление при
изменении размера окна.
Haunted-компоненты легко интегрируются с серверными фреймворками, такими как Express, Next.js или Fastify. Основной подход:
render для генерации HTML на сервере.Пример с Express:
import express from 'express';
import { render } from 'haunted/ssr';
import { Counter } from './components/counter.js';
const app = express();
app.get('/', (req, res) => {
const htmlContent = render(Counter);
res.send(`
<html>
<body>
${htmlContent}
<script type="module" src="/client.js"></script>
</body>
</html>
`);
});
app.listen(3000);
В /client.js будет код:
import { component } from 'haunted';
import { Counter } from './components/counter.js';
customElements.define('my-counter', component(Counter));
Haunted обеспечивает мощный функциональный подход к созданию веб-компонентов с полной поддержкой изоморфизма, позволяя строить приложения с серверным рендерингом, сохраняющими интерактивность на клиенте без дублирования кода.