Привет!

Сегодня я расскажу о своём опыте в создании фреймворка для фронтенд-разработки. Цель была ясна, как день: сделать так, чтобы всё можно было выучить за 5 минут, с расчётом на то, что человек уже знает React, Vue или Angular.

Как создать компонент

Вариантов тут много. В React это просто функция. В Vue это файл. Мне лично нравится возможность в React создавать несколько вспомогательных компонентов внутри файла, поэтому мы решили, что компонент будет функцией и объявляется он следующим образом:

export const MyComponent = component(() => {
    // код тут
});

Реактивные состояния

Для сохранения состояния были придуманы переменные, и мы будем их использовать.

Правила просты:

  1. Если название переменной начинается с $ — значит, она будет реактивной.

  2. Если название переменной не начинается с $ — значит, мы её не меняем.

Если нам нужен 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

Спасибо за внимание!

Комментарии (6)


  1. artptr86
    07.11.2025 22:23

    Кажется, вы изобрели Solid.js, только со слотами


    1. vasille Автор
      07.11.2025 22:23

      Интересное наблюдение, я лично думал что он сильно похож на Svelte 4.


  1. m6atom
    07.11.2025 22:23

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

    Что-то с падежом напутали


    1. vasille Автор
      07.11.2025 22:23

      Русский не мой родной язык, так что может быть ошибки, я пока что не понимаю какой падеж напутал.


      1. goldexer
        07.11.2025 22:23

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


  1. grammidin4eg
    07.11.2025 22:23

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