Привет!
Сегодня я расскажу о своём опыте в создании фреймворка для фронтенд-разработки. Цель была ясна, как день: сделать так, чтобы всё можно было выучить за 5 минут, с расчётом на то, что человек уже знает React, Vue или Angular.
Как создать компонент
Вариантов тут много. В React это просто функция. В Vue это файл. Мне лично нравится возможность в React создавать несколько вспомогательных компонентов внутри файла, поэтому мы решили, что компонент будет функцией и объявляется он следующим образом:
export const MyComponent = component(() => {
// код тут
});
Реактивные состояния
Для сохранения состояния были придуманы переменные, и мы будем их использовать.
Правила просты:
Если название переменной начинается с
$— значит, она будет реактивной.Если название переменной не начинается с
$— значит, мы её не меняем.
Если нам нужен derived/computed state, то мы описываем константу с нужным значением. Даже если использовать let, то некоторые линтеры автоматически будут менять его на const, поэтому константа — это канон.
Пример кода:
export const MyComponent = component(() => {
let $a = 2;
let $b = 3;
const $sum = $a + $b;
const $sum2 = sum($a, $b);
});
Эффекты
Сама функция компонента выполняется всего 1 раз, поэтому для того чтобы управлять эффектами, есть следующие функции:
watchвыполняет функцию каждый раз, когда меняются реактивные данные внутри.beforeMountвыполняет функцию после инициализации данных и перед тем, как начать обновлять DOM.afterMountвыполняет код после того, как DOM был обновлён.beforeDestroyвыполняет код до того, как удалить ноды из DOM.
Следующий код
export const MyComponent = component(() => {
let $state = 'init';
watch(() => { console.log($state) });
beforeMount(() => { $state = "before" });
afterMount(() => { $state = "after" });
});
будет выводить в консоль:
init
before
after
DOM
Для описания узлов DOM используется HTML-код, прописанный напрямую в функцию, исключение только для событий: onclick, onpress и т.д., они получают функцию в качестве значения. Всё это работает через JSX.
Описание узлов
Пример кнопки со счётчиком:
export const MyComponent = component(() => {
let $count = 0;
function inc() {
$count++;
}
<button class="btn" onclick={inc}>You clicked {$count} times</button>;
});
Для class есть возможность передать массив строк для удобства, а для style — объект свойств. Но это уже плюшки.
Обратная связь
Для того чтобы вручную что-то менять/создавать, подключать сторонние библиотеки, используется обратная связь — функция, которая вызывается, когда узел и все его дочерние элементы добавлены в DOM.
Пример использования обратной связи:
export const MyComponent = component(() => {
function sideEffect(input: HTMLInputElement) {
input.showPicker();
}
<input type="date" callback={sideEffect}/>;
});
Передача данных между компонентами
Данные можно передать между компонентами следующими путями:
от родителя к дочернему компоненту через свойства;
от дочернего к родителю через обратную связь;
от дочернего к родителю и обратно через слоты.
Передача данных через свойства
Свойства — это объект, к названию полей применяются такие же правила, как к названию переменных: то есть если поле начинается с $, то оно передаёт реактивные данные, иначе это обычное поле.
Пример передачи данных через свойства:
interface Props {
userId: string;
$userName: string;
}
const Child = component(({userId, $userName}: Props) => {
<div>{userId} is named {$userName}</div>;
});
const Parent = component(() => {
const id = 1;
let $name = "First";
// Когда мы здесь обновляем имя,
// оно будет автоматически обновлено в дочернем элементе
<Child userId={id} $userName={$name}/>;
});
Передача данных через обратную связь
Компонент, как функция, может что-то возвращать, это значение передаётся родительскому компоненту через обратную связь.
Пример использования в качестве альтернативы forwardRef из React:
const Child = component(() => {
let input: HTMLInputElement | null = null;
<input callback={element => input = element}/>;
return input;
});
const Parent = component(() => {
<Child callback={input => { console.log(input) }}/>;
});
Передача данных через слоты
Слоты от дочернего элемента к родителю передают свойства, а от родителя к дочернему — DOM-представление:
interface Props {
$title: string;
slot?(props: { $name: string }): void;
}
const Child = component(({$title, slot}: Props) => {
<div>
<Slot model={slot} $name={`${$title} is amazing`}/>
</div>;
});
const Parent = component(() => {
let $title = "MyApp";
<Child $title={$title} slot={(($name) => {
<span>{$name}</span>;
})}/>;
});
В случаях когда дочерний компонент ничего не передаёт родителю, содержимое можно написать внутри тега: <Child><span>Text</span></Child>.
Также внутри тега Slot можно добавить содержимое, которое будет отображаться, если родитель не заполнил слот.
Слот — это не просто функция, а полноценный маленький компонент, то внутри него можно добавить derived/computed состояния, эффекты через watch, beforeMount, afterMount и даже beforeDestroy.
Логика и циклы
Это всё работает через специальные встроенные компоненты If, Else, ElseIf и For.
Пример условного текста:
const MyComponent = component(() => {
let $count = 0;
<If $condition={$count > 2}>
Count is too big!
</If>;
});
Пример цикла:
const MyComponent = component(() => {
const arr = [1, 2, 3];
<For model={arr} slot={number => {
Number is {number}
}}/>;
});
Правила про названия свойств относятся и к встроенным компонентам. То есть If будет реагировать на изменения в $condition, а For не будет реагировать на изменения модели — это значит, что модель надо обновлять через push, pull и т.д.
Выводы
Тут есть необходимый минимум, чтобы создать SPA. Это всё уже работает, и дальше — больше: есть стили для компонентов, скрипты для сборки как приложения, так и библиотек под фреймворк. Последнее, что добавили, — это SSG и условия в стиле React как альтернатива тегам If/Else. Проблема в том, что TypeScript иногда ругается.
Проект с открытым исходным кодом, но не знаю, будет ли публикация его названия считаться рекламой. Если хотите помочь, можете заполнить опрос под CustDev: https://docs.google.com/forms/d/e/1FAIpQLSej4oupzzzN1Iy2Yk9gMe4lJyhdAkUJS_WnkRgqW9BzdQo8jA/viewform?usp=publish-editor
Спасибо за внимание!
Комментарии (25)

m6atom
07.11.2025 22:23Как мы пытались сделать фреймворк для фронтенда которого можно выучить за 5 минут и что из этого вышло
Что-то с падежом напутали

vasille Автор
07.11.2025 22:23Русский не мой родной язык, так что может быть ошибки, я пока что не понимаю какой падеж напутал.

goldexer
07.11.2025 22:23Кстати, если вам сложно, используйте профридеры - приложения, которые подсвечивают ошибки в тексте и исправляют по нажатию кнопки. В основном они умеют объяснять причины исправлений. Я вот, например, английские тексты так правлю. А то знаний мало и пишу как селянин, меня очень выручает.

grammidin4eg
07.11.2025 22:23Очередной Фреймворк, который ничего нового не делает. Можно было взять существующий и не тратить зря деньги бизнеса.

isumix
07.11.2025 22:23Оставлю тут пример компонента счетчика нажатий на своей библиотеке. Если ваш фреймворк имеющий стейт-менеджер и шаблонизатор для циклов и условий и который учится за 5 минут, то моя библиотека не имеющая ничего этого должна учиться за 2 минуты, ЛОЛ.
const ClickCounter = ({count = 0}) => ( <button click_e_update={() => count++}>Clicked {() => count} times</button> );
vasille Автор
07.11.2025 22:23По этому коду тут вопросов очень много:
{count = 0}почему состояние это поле объекта в параметре функций?click_e_updateчто означаетe_update? Как добавить обработчик дляtouchstartнапример?Clicked {() => count} times- для чего тут функция если это шаблон и по идею встроим значение?
Тот же пример у нас:
const ClickCounter = component(() => { let $count = 0; <button onclick={() => $count++}>Clicked {$count} times</button>; });
isumix
07.11.2025 22:23{count = 0}- это деструктурирование параметров функции, где мы получаем переменнуюcountсо значение по умолчанию0click_e- означает установка обработчика событий, для события можно добавлять модификаторы, например:touchstart_e_capture_once_update. Помимо событий есть еще модификаторы для атрибутов и пропсов. Все это сделано чтобы в полной мере отражать спецификацию W3C стандарта.Clicked {() => count} times- функция тут нужна чтобы подставлять в DOM динамически значения при вызове фукцииupdateкоторая декларируется в нашем модификаторе. Также можно явно ее вызывать как описано тут. Статическое значение можно было вставить не оборачивая в функцию.
Также существует альтернативный (не JSX) синтаксис:
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()));
vasille Автор
07.11.2025 22:23Вот стандарт: https://dom.spec.whatwg.org/#eventtarget в нём чётка указано что обработчик события устанавливается тремя параметрами: тип события, обработчик и опции. Вот часть стандарта https://html.spec.whatwg.org/multipage/webappapis.html#event-handlers-on-elements,-document-objects,-and-window-objects которая описыввает какие атрибуты у элемента под какие типы события, соответственно нам остаётся указать 1 или 2 параметра. примеры:
<button onclick={() => $count++}>Clicked {$count} times</button> <button onclick={[() => $count++, true]}>Clicked {$count} times</button> <button onclick={[() => $count++, {capture: true, once: true}]}>Clicked {$count} times</button>И вот такое как раз и соответствует стандартам, а вот эти хитроумные свойства надо выучить. Понимаешь разницу в подходе?

isumix
07.11.2025 22:23Модификатором проще это делать, в вашем способе также надо разобраться, хотя ваш способ тоже интересный, возвращение тюпла, во Фьюзоре примерно также можно сделать:
click_e={{ handle: (event, self) => 'Clicked!', capture: true, once: true, passive: true, signal: AbortSignal, update: true, // update target component after event handler completes }}Другое дело что во Фьзоре помимо эвентов поддерживаются еще и другие типы модификаторов
a-attributean-attributenamespacedp-propertyps-propertystatice-event handler
Но мы тут уходим в детали синтаксиса, который легко подправить. Главное отличие Фьюзора - то что это библиотека, она не имеет встроенного стейт-менеджера, и позволяет все делать ЯВНО.

vasille Автор
07.11.2025 22:23Я не говорил что так хуже, а то что это всё надо выучить, особенно пугает ручное обновление компонента. Ещё мне непонятно будет ли Typescript это проверять, IDE предлагать авто-дополнение этих названий и сколько всего вариантов там в списке будут.

artptr86
07.11.2025 22:23Для чего вообще кому-то может быть нужна библиотека для создания счётчиков нажатий?

vasille Автор
07.11.2025 22:23Это такой тренд, почти на всех сайтах фреймворках находится этот пример, он простой и даёт первичное впечатление о том как строится компоненты. А сам счётчик можешь найти на маркетплейсах когда нажимаешь кнопку "Добавить в корзину".

artptr86
07.11.2025 22:23Это такой же плохой тренд, как и приложение Todo MVC когда-то. Такие примеры не дают полноценного понимания data flow. В данном случае единственная «киллер-фича» библиотеки в том, что она может автоматически обновлять компонент-источник события, а больше она ничего и не умеет.

isumix
07.11.2025 22:23Прикол в том что эта библиотека всего лишь абстракция над
document.createElementбез стейт менеджмента. Но тем не менее она может делать все то же самое, что делают "большие" фреймворки, потребляя в разы меньшие ресурсы. Там есть также два полноценных приложения.
artptr86
07.11.2025 22:23У больших фреймворков есть управление состоянием. У вас этого нет, поэтому сравнивать их нельзя.

GeorgeBobrov
07.11.2025 22:23Объясните пожалуйста, зачем нужны псевдокомпоненты If, Else, ElseIf и For?
Ведь у нас внутри компонента уже есть JavaScript (или TypeScript), которые имеют if, else, for и весь арсенал для проверок/ветвлений и обхода циклов?
Я не веб-разработчик, так, для десктопа пишу, мне действительно не очевидно

vasille Автор
07.11.2025 22:23С большим удовольствием отвечаю, я сам разработчик C++ Qt (middle) по этому отлично понимаю ваше недоразумение.
Больше всего писал на QML, это всё по сути заменяет такие компоненты в нём как Loader и Repeater.
Когда используешь более классический способ у тебя есть доступ к указателя на каждый Layout и в нём можно что-то добавить и удалить без проблем, это работает на основе архитектуры MVC. Да и там всё равно есть различные ListView, TableView и подобные.
В моём фреймворке используется архитектура MVVM, по этому любые изменения на экраны идут через так называемые биндинги, т.е. меняешь данные и интерфейс сам обновляется, в таком случае программист не имеет необходимость в прямой доступ к составляющих интерфейса (а сам доступ он есть).

artptr86
07.11.2025 22:23Собственно потому, что функция компонента исполняется всего один раз для его конструирования, а все зависимости от данных внутри него перевычисляются реактивно. Поэтому, например, псевдокомпонент If декларативно задаёт, что показывать его детей нужно по изменению соответствующей переменной, а For — что дочерний компонент нужно итерировать (причём по возможности переиспользуя существующие элементы) согласно переданному массиву.
А вот в React, в котором функция компонента вызывается каждый раз при изменении данных, действительно используются встроенные в JS возможности для циклов и условий.

Prologos
07.11.2025 22:23Проект с открытым исходным кодом, но не знаю, будет ли публикация его названия считаться рекламой.
Спасибо за статью, хотелось бы ещё увидеть ссылку на гитхаб например чтобы поиграться (обычно на хабре делятся такой ссылкой)
artptr86
Кажется, вы изобрели Solid.js, только со слотами
vasille Автор
Интересное наблюдение, я лично думал что он сильно похож на Svelte 4.
artptr86
В плане использования $ для пометки реактивных переменных — да, на Svelte, но в общем как-то больше ассоциируется с Solid.
vasille Автор
Solid очень классный, по этому сочту за комплимент )