Chorda 3.0. Осенний релиз

Kate

Administrator
Команда форума

Очевидные решения​

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

Расширение библиотечных компонентов​

Начну с декларативного подхода и примесей - краеугольного камня Chorda. Для наглядности нам понадобится немного кода.

Создадим простой компонент на JSX (React) с кнопкой и текстом. Задача: при клике по кнопке меняется текст

const MyComponent = () => {

const [data, changeData] = useState('')

const hahdleClick = (e) => {
changeData('Hello')
}

return <div>
<button onClick={handleClick}>Click me</button>
<p>{data}</p>
</div>

}

// вот так выглядит применение компонента
<MyComponent/>
Сделаем похожий функционал, используя чертеж Chorda

// вырожденная дизайн-функция
const MyComponent = () => {
return {
templates: {
button: {
tag: 'button',
text: 'Click me',
events: {
// обработка событий VDOM
$dom: {
click: (evt, {data}) => {
data.$value = 'Hello'
}
}
}
},
text: {
tag: 'p',
reactions: {
// реакции компонента на изменение переменной скоупа
data: v => patch({text: v})
}
}
},
initials: {
// инициализация переменной в скоупе
data: () => observable('')
}
}
}

// создаем чертеж
MyComponent()
Ну, все. Расходимся, ребята. Очевидно же, что шаблонный синтаксис намного проще и понятнее.

Но

Давайте посмотрим, что происходит с нашим JSX компонентом дальше. Итак, мы выполнили задачу, и теперь передаем наши наработки коллеге, скажем, в составе корпоративной или публичной библиотеки. Через некоторое время от коллеги приходит просьба: хочу, чтобы компонент можно было стилизовать. Не вопрос. Самый простой и быстрый способ это сделать - дать возможность управлять классом корневого компонента через пропсы.

Поехали

// Придется залезть в библиотеку (!) и сделать пару правок

const MyComponent = (props) => {

const {rootClassName} = props

/* Тут ничего не меняется. Пропускаем */

return <div className={rootClassName}>
<button onClick={handleClick}>Click me</button>
<p>{data}</p>
</div>
}

// рендерим
<MyComponent rootClassName="custom" />
Отлично!

Тем временем в Chorda

// Менять оригинальный чертеж необходимости нет

// В месте применения создадим примесь
mix(MyComponent(), {
css: 'custom',
})
Естественно, стилизацией обычно дело не заканчивается. Чем дальше, тем больше хотелок и тем больше пропсов нам понадобится добавить.

На самом деле подобные извращения следует пресекать в зародыше, и сразу предоставлять возможность потребителю "слотировать" вложенные компоненты. Однако, в нашем примере компоненты оказались жестко связаны state-параметром data. Просто так вытащить их не получится, поэтому посмотрим, во что может превратиться реализация с пропсами

const MyComponent = (props) => {

const {rootProps, buttonProps, text: MyText} = props

/* тут ничего не меняется */

return <div {...rootProps} >
<button onClick={handleClick} {...buttonProps} >Click me</button>
<MyText>{data}</MyText>
</div>
}

<MyComponent
rootProps={{className: 'custom'}}
buttonProps={{className: 'custom-button'}}
text={props => <p className="custom-text">{props.children}</p>}
/>

В ситуации с чертежом без особых именений

// Расширяем примесь
mix(MyComponent(), {
css: 'custom',
templates: {
button: {
css: 'custom-button'
},
text: {
css: 'custom-text'
}
}
})
Для React типовое решение проблемы это вынесение состояния из компонента. Получившийся "глупый" компонент уже не будет иметь внутри сильной связи и позволит легко "слотировать" вложения. Но здесь есть подводный камень. Архитектура такая штука, что если где-то убыло, то где-то прибыло. Выбрасывая сложность из наших компонентов мы ее копим в другом месте, а именно там, где компонент будет применяться

Стоит признать, с примесями тоже не все так гладко. Чтобы сделать хорошо расширяемый компонент, его необходимо сильно декомпозировать, а это напрямую влияет на производительность и восприятие кода в целом (по второму пункту полезно почитать о причинах появления setup во Vue 3)

Так к чему все это сравнение? Тот же React может предложить много вариантов и подходов для расширения функционала, один экзотичнее другого. Chorda же предлагает один путь (на самом деле нет), что на мой взгляд экономит на выборе уйму времени

Этот компонент мне не подходит​

Идем дальше. Мы рассмотрели как именно выполняются изменения отдельного компонента, но что если вы являетесь не автором библиотеки, а ее потребителем, и столкнулись с проблемой нехватки функционала или дефектом?

Тогда на выбор:

1. Делаем форк, вносим в него правки. Делаем PR в репо библиотеки. Пока ждем влития, пользуемся форком

2. Делаем свой компонент. Используем его вместо библиотечного. Ждем новой версии библиотеки

3. (для обладателей особого дара убеждения) Объясняем автору библиотеки в чем он не прав и почему он должен внести нужные вам правки как можно скорее. Профит!

4. Забиваем

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

Mount или не Mount?​

Наверно, правильнее этот вопрос надо задать так: в какой момент должна начинаться обработка бизнес задач?

Хорошо проиллюстрирует мою мысль пример с загрузкой данных в store приложении при открытии страницы. Как правило, загрузка выполняется по событию монтирования узла виртуального DOM, что есть странно - зачем что-то добавлять в DOM, если данных еще нет? Почему бы нам сначала не загрузить данные, а потом решать - рендерить что-то или нет. Тут ситуацию немного спасает Suspense и понятие асинхронных компонентов, когда у каждого из них есть своя отложенная задача и, соответственно, отложенная отрисовка

Мне же больше нравится вариант, когда загрузка вообще никак не связана с рендерингом. В Chorda бизнес-логика находится на уровне дерева компонентов, а результаты выполнения бизнес-задач влияют только на store/state, не касаясь отрисовки напрямую

Все мы вместе и каждый сам по себе​

В Chorda состояние компонента определяется скоупом (что-то вроде локального store). Компонент видит только свой скоуп и работает только с ним, считая, что вокруг никого нет. Это позволяет спокойно выполнять смешивание, не опасаясь сломать жесткие связи между компонентами

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

Здесь мы подходим к такой штуке, как конвейер обработки (вычисление-патч-отрисовка). Он, как и любое другое значение, попадает к компонентам через скоуп, и его, так же как и любое другое значение, можно настроить локально. К примеру, стратегия базового конвейера дает построение дерева компонентов в ширину с удержанием задач отрисовки до окончания обработки всех патчей. Если для вашего компонента или блока компонентов такое решение не подходит, вы можете подключить к ним свой кастомный конвейер

Неочевидные следствия​

А вот некоторые моменты не являлись изначальной целью, но проявились по мере развития фреймворка

Встраивание в существующие проекты​

Реализация виртуального DOM не входит в состав Chorda, т.к. разработка еще одного нового отрисовщика не решала моих проблем. Поэтому я собирался использовать какой-нибудь из уже существующих. Для того, чтобы попробовать Chorda в деле, я собрался переписать с нуля пару домашних React-проектов. Но приступив, почти сразу понял, что это совсем не обязательно. Можно постепенно заменять отдельные компоненты, подключив правильный рендерер, и так потихоньку съесть всего слона целиком.

Интересный вопрос: если фреймворк использует React, то можно ли сказать, что приложение, которое использует данный фреймворк, написано на React?

Загружаем и работаем​

Сомнительное следствие конечно, но для некоторых может оказаться чрезвычайно важным. Создание компонентов и отрисовка присходят в runtime, без этапа транспиляции. Подключив ядро Chorda и рендерер, можно сразу писать код, который тут же будет исполняться в браузере.

Правда вот, SSR превратился в нетривиальную задачу

Поведенческие компоненты​

Тут скорее интересное наблюдение. Раньше я сталкивался только с таким подходом к "глупым" компонентам: есть простой компонент без логики работы, который знает как ему рисоваться, затем поверх него создается "умный" компонент, который включает в себя "глупый", добавляя некоторое поведение.

Так вот в Chorda (из-за того, что можно легко менять порядок смешивания) у меня появился набор компонентов, в которых есть только поведение без правил отрисовки (обычно это набор стандартных реакций). И получилось, что, наоборот, уже к поведению начинает примешиваться способ отображения

Как это может выглядеть:

export default () => {
// поведенческий компонент Text
return Text({
as: Paragraph, // "глупый" компонент Paragraph
text$: $ => $.user.name
})
}

Что в итоге?​

Естественно, это не все, о чем хотелось рассказать. Но к этому времени кофе уже закончился, а статья на две чашки это уже лонгрид.

 
Сверху