Почти Ванильный Фронтэнд
- суббота, 5 апреля 2025 г. в 00:00:08
Почти — потому что используется всего две функции из библиотеки:
Создать
элемент DOM
Обновить
элемент DOM
Эта библиотека упрощает использование нативных функций DOM, таких как createElement
и replaceChild
. Библиотека Fusor направлена на то, чтобы сделать эти функции проще и компактнее.
Ниже приведены примеры распространенных проблем. Попробуйте воспроизвести их с использованием инструментов, которые вы сейчас используете. Вы можете быть удивлены, обнаружив, что разработка с Fusor может быть наиболее компактным, гибким, легким и производительным способом создания фронтенд-приложений.
npm install @fusorjs/dom
Или:
JSX стартовый проект для: JavaScript или TypeScript
CDN: https://esm.sh/@fusorjs/dom или https://cdn.skypack.dev/@fusorjs/dom
import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';
let count = 0;
const block = section(
{class: () => (count % 2 ? 'odd' : 'even')},
div('Seconds ', () => count, ' elapsed'),
div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);
document.body.append(getElement(block));
setInterval(() => {
count++;
update(block);
}, 1000);
Только крохотные части у block
дерева DOM обновляются, если они отличаются от текущих значений.
import {getElement, update} from '@fusorjs/dom';
let count = 0;
const block = (
<section class={() => (count % 2 ? 'odd' : 'even')}>
<div>Seconds {() => count} elapsed</div>
<div>Minutes {() => Math.floor(count / 60)} elapsed</div>
</section>
);
document.body.append(getElement(block));
setInterval(() => {
count++;
update(block);
}, 1000);
import {getElement, update} from '@fusorjs/dom';
import {section} from '@fusorjs/dom/html';
let count = 0;
const block = section(
{
id: 'set attribute or property automatically',
title_a: 'set attribute',
style_p: 'set property',
focus_e: () => 'set bubbling event handler',
blur_e_capture_once: () => 'set capturing event handler once',
// update dynamic values in this DOM node:
click_e_update: () => count++, // same as
click_e: () => {count++; update(block);}, // same as
click_e: (event, self) => {count++; update(self);},
class: count % 2 ? 'odd' : 'even', // static
class: () => (count % 2 ? 'odd' : 'even'), // dynamic
},
'Static child ', count, ' never changes.',
'Dynamic child ', () => count, ' is wrapped in a function.',
);
document.body.append(getElement(block));
import {getElement} from '@fusorjs/dom';
import {button, div} from '@fusorjs/dom/html';
const ClickCounter = (count = 0) =>
button({click_e_update: () => count++}, 'Clicked ', () => count, ' times');
const App = () => div(ClickCounter(), ClickCounter(22), ClickCounter(333));
document.body.append(getElement(App()));
import {getElement} from '@fusorjs/dom';
const ClickCounter = ({count = 0}) => (
<button click_e_update={() => count++}>Clicked {() => count} times</button>
);
const App = () => (
<div>
<ClickCounter />
<ClickCounter count={22} />
<ClickCounter count={333} />
</div>
);
document.body.append(getElement(<App />));
Компоненты в обеих версиях совместимы.
import {button} from '@fusorjs/dom/html';
const ClickCounter = (count = 0) => {
const self = button(
{click_e: () => {count++; update(self);}},
'Clicked ', () => count, ' times',
);
return self;
};
const ClickCounter = (count = 0) =>
button(
{click_e: (event, self) => {count++; update(self);}},
'Clicked ', () => count, ' times',
);
const ClickCounter = (count = 0) =>
button(
{click_e_update: () => count++},
'Clicked ', () => count, ' times',
);
Или Controlled Input в терминах React.
import {getElement} from '@fusorjs/dom';
import {input, div} from '@fusorjs/dom/html';
const UppercaseInput = (value = '') =>
input({
value: () => value.toUpperCase(),
input_e_update: (event) => (value = event.target.value),
});
document.body.append(
getElement(
div(UppercaseInput(), UppercaseInput('two'), UppercaseInput('three')),
),
);
import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';
let count = 0;
const seconds = div('Seconds ', () => count, ' elapsed');
const block = section(
seconds,
div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);
document.body.append(getElement(block));
setInterval(() => {
count++;
update(seconds); // not minutes
}, 1000);
Это обновит только секунды, а не минуты.
import {getElement, update} from '@fusorjs/dom';
import {section, div} from '@fusorjs/dom/html';
let count = 0;
const seconds = div('Seconds ', () => count, ' elapsed');
const block = section(
() => seconds, // wrapped in a function to escape
div('Minutes ', () => Math.floor(count / 60), ' elapsed'),
);
document.body.append(getElement(block));
setInterval(() => {
count++;
update(block);
}, 1000);
Это обновит только минуты, но не секунды.
Только компоненты (seconds
, block
) обновляются рекурсивно. () => seconds
— это функция, а не компонент.
Каждая функция из @fusorjs/dom/html
возвращает компонент, если он содержит динамические значения. То же самое относится и к определениям JSX.
Создать компонент
Подключить к DOM
Обновить DOM
Отключить от DOM
import {getElement, update} from '@fusorjs/dom';
import {div} from '@fusorjs/dom/html';
const IntervalCounter = (count = 0) => {
console.log('1. Create component');
return div(
{
mount: (self) => {
console.log('2. Connect to DOM');
const timerId = setInterval(() => {
count++;
update(self);
console.log('3. Update DOM');
}, 1000);
const unmount = () => {
clearInterval(timerId);
console.log('4. Disconnect from DOM');
};
return unmount;
},
},
'Since mounted ', () => count, ` seconds elapsed`,
);
};
const instance = IntervalCounter(); // 1. Create component
const element = getElement(instance);
document.body.append(element); // 2. Connect to DOM
setTimeout(() => element.remove(), 15000); // 4. Disconnect from DOM
Автоматические/реактивные обновления в больших фреймворках — это не что иное, как реализация паттерна Observable. Это включает в себя State в React, Signals в Solid, Redux, MobX и многие другие. В Fusor вы можете использовать любую из этих библиотек.
Здесь мы обсуждаем универсальное решение:
import {update} from '@fusorjs/dom';
import {Observable} from 'Any/Observable/Signal/Redux/Mobx...';
// Modern routing handling
const observable = new Observable();
const read = () => location.hash.substring(1); // omit "#"
let route = read();
window.addEventListener(
'popstate',
() => {
const next = read();
if (route === next) return;
route = next;
observable.notify();
},
false,
);
// Fusor integration
export const getRoute = () => route;
export const mountRoute = (self) => {
const callback = () => update(self);
observable.subscribe(callback);
return () => observable.unsubscribe(callback);
};
Переключение компонентов, когда выбран текущий маршрут.
import {span, a} from '@fusorjs/dom/html';
import {getRoute, mountRoute} from './router';
export const RouteLink = (title, route) =>
span({mount: mountRoute}, () =>
getRoute() === route
? title // when selected
: a({href: `#${route}`}, title),
);
import {getElement} from '@fusorjs/dom';
import {ul, li} from '@fusorjs/dom/html';
import {RouteLink} from './RouteLink';
const block = ul(
[...Array(10)].map((v, i) =>
li(RouteLink(`${i + 1}. Section`, `url-to-${i + 1}-section`)),
),
);
document.body.append(getElement(block));
Тяжелый компонент создается только один раз.
import {div, br} from '@fusorjs/dom/html';
let isVisible = true; // can change
const block = div(
(
(cache = HeavyComponent()) =>
() =>
isVisible && cache
)(),
br(),
() => RecreatedEveryUpdate(),
);
import {section, p} from '@fusorjs/dom/html';
const Value = (value) => {
if (value === undefined) throw new Error(`provide a value`);
return p(value);
};
const block = section(
p('Before'),
(() => {
try {
return [
Value(1),
Value(), // will throw
Value(3),
];
} catch (error) {
if (error instanceof Error) return p('Exception: ', error.message);
return p('Exception: unknown');
}
})(),
p('After'),
);
Теперь вы знаете все, что нужно для начала разработки современных фронтенд-приложений с Fusor.
Насколько мне известно, разработка с Fusor — это самый лаконичный, гибкий, легковесный и производительный способ создания фронтенд-приложений.