Мысль перевести фронт на какой-либо js фреймворк появилась одновременно с возможностью писать React на Kotlin. И я решил попробовать. Основная проблема: мало материалов и примеров (постараюсь эту ситуацию поправить). Зато у меня полноценная типизация, безбоязненный рефакторинг, все возможности Kotlin, а главное, общий код для бека на JVM и фронта на Javascript.
В этой статье будем писать страницу на Javasript + React параллельно с её аналогом на Kotlin + React. Чтобы сравнение было честным, я добавил в Javasript типизацию.
Добавить типизацию в Javascript оказалось не так просто. Если для Kotlin мне понадобились gradle, npm и webpack, то для Javascript мне понадобились npm, webpack, flow и babel с пресетами react, flow, es2015 и stage-2. При этом flow тут как-то сбоку, и запускать его надо отдельно и отдельно дружить его с IDE. Если вынести за скобки сборку и подобное, то для непосредственного написания кода с одной стороны остается Kotlin+React, а с другой Javascript+React+babel+Flow+ES5|ES6|ES7.
Для нашего примера сделаем страничку со списком машин и возможностью фильтрации по марке и цвету. Возможные для фильтрации марку и цвет подтаскиваем с бека один раз при первой загрузке. Выбранные фильтры сохраняем в query. Машины отображаем в табличке. Мой проект не про машины, но общая структура в целом похожа на то, с чем я регулярно работаю.
Результат выглядит вот так (дизайнером мне не быть):
Конфигурацию всей этой шайтан-машины я здесь описывать не буду, это тема для отдельной статьи (пока можно курить исходники от этой).
Подгрузка данных с бека
Для начала надо подгрузить бренды и доступные цвета с бека.
javascript | kotlin |
---|---|
|
|
Выглядит очень похоже. Но есть и различия:
- Дефолтные значения можно прописать там же, где объявляется тип. Так легче поддерживать целостность кода.
- lateinit позволяет не задавать дефолтное значение вообще для того, что будет подгружено позже. При компиляции такая переменная считается как NotNull, но при каждом обращении проверяется то, что она была заполнена и выдается человекочитабельная ошибка. Особенно это будет актуально при более сложном объекте, чем массив. Знаю, того же можно было бы достигнуть при помощи flow, но это настолько громоздко, что я не стал пробовать.
- kotlin-react из коробки дает функцию setState, но она не сочетается с корутинами, потому что не inline. Пришлось скопировать и поставить inline.
- Собственно, корутины. Это замена async/await и много чего ещё. Например, через них сделан yield. Интересно, что в синтаксис добавлено только слово suspend, всё остальное — просто код. Поэтому больше свободы использования. А ещё немного более жесткий контроль на уровне компиляции. Так, нельзя оверрайдить componentDidMount с
suspend
модификатом, что логично: componentDidMount синхронный метод. Зато можно в любом месте кода вставить асинхронный блокlaunch { }
. Можно в явном виде принимать асинхронную функцию в параметре или поле класса (чуть ниже пример из моего проекта). - В Javascript меньший контроль nullable. Так в получившемся state можно менять nullability полей brand, color и loaded и всё будет собираться. В Kotlin варианте будут оправданные ошибки компиляции.
suspend fun parallel(vararg tasks: suspend () -> Unit) {
tasks.map {
async { it.invoke() } //запускаем каждый task, но не ждем ответа. async {} возвращает что-то вроде promise
}.forEach { it.await() } //все запустили, теперь ждем
}
override fun componentDidMount() {
launch {
updateState {
parallel({
halls = hallExchanger.all()
}, {
instructors = instructorExchanger.active()
}, {
groups = fetchGroups()
})
}
}
}
Теперь подгрузим машины с бека используя фильтры из query
JS:
async loadCars() {
let url = `/api/cars?brand=${this.state.brand || ""}&color=${this.state.color || ""}`;
this.setState({
cars: await (await fetch(url)).json(),
loaded: true
});
}
Kotlin:
private suspend fun loadCars() {
val url = "/api/cars?brand=${state.brand.orEmpty()}&color=${state.color.orEmpty()}"
updateState {
cars = fetchJson(url, Car::class.serializer().list) //(*)
loaded = true
}
}
Хочу обратить внимание на Car::class.serializer().list
. Дело в том, что jetBrains написала библиотеку для сериализации/десериализации, которая одинаково работает на JVM и JS. Во-первых, меньше проблем и кода в случае если бек на JVM. Во-вторых валидность пришедшего json проверяется во время десериализации, а не когда-нибудь при обращении, так что при смене версии бека, и при интеграциях впринципе, проблемы будут находиться быстрее.
Рисуем шапку с фильтрами
Напишем stateless component для отображения двух выпадающих списков. В случае Kotlin это будет просто функция, в случае js — отдельный компонент, который будет генерироваться react loader при сборке.
javascript | kotlin |
---|---|
|
|
Первое, что бросается в глаза — HomeHeaderProps в JS части, мы вынуждены объявить входящие параметры отдельно. Неудобно.
Ещё немного изменился синтаксис Dropdown. Я тут использую primereact, естественно, пришлось писать kotlin обертку. С одной стороны это лишняя работа (слава богу, есть ts2kt), но с другой — это возможность местами сделать api удобнее.
Ну и немного синтаксического сахара при формировании итемов для dropdown. })))}
в js варианте выглядит интересно, но это не беда. Зато выпрямление последовательности слов намного приятнее: «преобразуем цвета в items и добавляем `all` по-умолчанию», вместо «добавляем `all` к цеветам преобразованным в items». Это кажется небольшим бонусом, но когда у тебя несколько таких переворотов подряд…
Сохраняем фильтры в query
Теперь нужно при выборе фильтров по марке и цвету изменять state, подгружать машины с бека и менять урл.
javascript | kotlin |
---|---|
|
|
И здесь опять проблема с дефолтными значениями параметров. Почему-то flow не разрешил мне одновременно иметь типизацию, деструктор и дефолтное значение взятое из state. Возможно, просто бага. Но, если бы все-таки вышло, то пришлось бы объявить тип за пределами класса, т.е. вообще на экран выше или ниже.
Рисуем таблицу
Последнее что нам осталось сделать — написать stateless component для отрисовки таблицы с машинами.
javascript | kotlin |
---|---|
|
|
Здесь видно, как я выпрямил api primefaces, и как в kotlin-react задавать стиль. Это обычный json, как и в js варианте. В своем проекте я делал обертку, которая выглядит также, но со строгой типизацией, насколько это возможно в случае html стилей.
Заключение
Ввязываться в новую технологию рискованно. Мало гайдов, на stack overflow ничего нет, не хватает некоторых базовых вещей. Но в случае с Kotlin мои затраты окупились.
Пока я готовил эту статью, я узнал кучу новых вещей о современном Javascript: flow, babel, async/await, шаблоны jsx. Интересно, насколько быстро эти знания устареют? И всё это не нужно, если использовать Kotlin. При этом знать о React нужно совсем немного, потому что большая часть проблем легко решается при помощи языка.
А что Вы думаете о замене всего этого зоопарка одним языком с большим набором плюшек впридачу?
Для заинтересовавшихся исходники.
P.S.: В планах написать статьи об конфигах, интеграции с JVM и о dsl формирующем одновременно react-dom и обычный html.
Уже написаные статьи о Kotlin:
Послевкусие от Kotlin, часть 1
Послевкусие от Kotlin, часть 2
Послевкусие от Kotlin, часть 3. Корутины — делим процессорное время
Автор: Нефедьев Георгий