Привет!
Сегодня я расскажу о своём опыте в создании фреймворка для фронтенд-разработки. Цель была ясна, как день: сделать так, чтобы всё можно было выучить за 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
Спасибо за внимание!
