Kotlin + React vs Javasript + React

    Мысль перевести фронт на какой-либо 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
    class Home
      extends React.Component
        <ContextRouter, State>{
    
    
    
      state = {
        loaded: false, //(1)
        color: queryAsMap(
      this.props.location.search
        )["color"],
        brand: queryAsMap(
      this.props.location.search
        )["brand"],
        brands: [], //(2)
        colors: [] //(2)
      };
    
    
      async componentDidMount()
      {
    
        this.setState({ //(3)
          brands: await ( //(4)
        await fetch('/api/brands')
          ).json(),
    
          colors: await ( //(4)
        await fetch('/api/colors')
          ).json()
    
        });
    
      }
    }
    
    type State = {
      color?: string, //(5)
      brand?: string, //(5)
    
      loaded: boolean, //(1)
      brands: Array<string>, //(2)
      colors: Array<string> //(2)
    };
    
    export default Home;
    

    class Home(
      props: RouteResultProps<*>
    ) : RComponent
    <RouteResultProps<*>, State>
    (props) {
      init {
        state = State(
    
          color = queryAsMap(
            props.location.search
          )["color"],
          brand = queryAsMap(
            props.location.search
          )["brand"]
    
    
        )
      }
    
      override fun componentDidMount()
      {
        launch {
          updateState { //(3)
            brands = fetchJson( //(4)
              "/api/brands",
              StringSerializer.list
            )
            colors = fetchJson( //(4)
              "/api/colors",
              StringSerializer.list
            )
          }
        }
      }
    }
    
    class State(
      var color: String?, //(5)
      var brand: String? //(5)
    ) : RState {
    var loaded: Boolean = false //(1)
    lateinit var brands: List<String> //(2)
    lateinit var colors: List<String> //(2)
    }
    
    private val serializer: JSON = JSON()
    
    suspend fun <T> fetchJson( //(4)
      url: String,
      kSerializer: KSerializer<T>
    ): T {
      val json = window.fetch(url)
        .await().text().await()
      return serializer.parse(
        kSerializer, json
      )
    }
    


    Выглядит очень похоже. Но есть и различия:

    1. Дефолтные значения можно прописать там же, где объявляется тип. Так легче поддерживать целостность кода.
    2. lateinit позволяет не задавать дефолтное значение вообще для того, что будет подгружено позже. При компиляции такая переменная считается как NotNull, но при каждом обращении проверяется то, что она была заполнена и выдается человекочитабельная ошибка. Особенно это будет актуально при более сложном объекте, чем массив. Знаю, того же можно было бы достигнуть при помощи flow, но это настолько громоздко, что я не стал пробовать.
    3. kotlin-react из коробки дает функцию setState, но она не сочетается с корутинами, потому что не inline. Пришлось скопировать и поставить inline.
    4. Собственно, корутины. Это замена async/await и много чего ещё. Например, через них сделан yield. Интересно, что в синтаксис добавлено только слово suspend, всё остальное — просто код. Поэтому больше свободы использования. А ещё немного более жесткий контроль на уровне компиляции. Так, нельзя оверрайдить componentDidMount с suspend модификатом, что логично: componentDidMount синхронный метод. Зато можно в любом месте кода вставить асинхронный блок launch { }. Можно в явном виде принимать асинхронную функцию в параметре или поле класса (чуть ниже пример из моего проекта).
    5. В 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
    type HomeHeaderProps = {
    brands: Array<string>,
    brand?: string,
    onBrandChange: (string) => void,
    colors: Array<string>,
    color?: string,
    onColorChange: (string) => void
    }
    
    const HomeHeader = ({
    brands,
    brand,
    onBrandChange,
    colors,
    color,
    onColorChange
    }: HomeHeaderProps) => (
      <div>
        Brand:
        <Dropdown
          value={brand}
          onChange={e =>
            onBrandChange(e.value)
          }
          options={withDefault("all",
            brands.map(value => ({
          label: value, value: value
        })))}
    
        />
        Color:
        <Dropdown
          value={color}
          onChange={e =>
            onColorChange(e.value)
          }
          options={withDefault("all",
            colors.map(value => ({
          label: value, value: value
        })))}
    
        />
      </div>
    );
    
    function withDefault(
      label, options
    ) {
      options.unshift({
        label: label, value: null
      });
      return options;
    }
    

    
    private fun RBuilder.homeHeader(
    brands: List<String>,
    brand: String?,
    onBrandChange: (String?) -> Unit,
    colors: List<String>,
    color: String?,
    onColorChange: (String?) -> Unit
    ) {
    
      +"Brand:"
      dropdown(
        value = brand,
        onChange = onBrandChange,
    
    
        options = brands.map {
          SelectItem(
            label = it, value = it
          )
        } withDefault "all"
      ) {}
      +"Color:"
      dropdown(
        value = color,
        onChange = onColorChange,
    
    
        options = colors.map {
          SelectItem(
            label = it, value = it
          )
        } withDefault "all"
      ) {}
    
    }
    
    infix fun <T : Any>
      List<SelectItem<T>>.withDefault(
      label: String
    ) = listOf(
      SelectItem(
        label = label, value = null
      )
    ) + this
    


    Первое, что бросается в глаза — HomeHeaderProps в JS части, мы вынуждены объявить входящие параметры отдельно. Неудобно.

    Ещё немного изменился синтаксис Dropdown. Я тут использую primereact, естественно, пришлось писать kotlin обертку. С одной стороны это лишняя работа (слава богу, есть ts2kt), но с другой — это возможность местами сделать api удобнее.

    Ну и немного синтаксического сахара при формировании итемов для dropdown. })))} в js варианте выглядит интересно, но это не беда. Зато выпрямление последовательности слов намного приятнее: «преобразуем цвета в items и добавляем `all` по-умолчанию», вместо «добавляем `all` к цеветам преобразованным в items». Это кажется небольшим бонусом, но когда у тебя несколько таких переворотов подряд…

    Сохраняем фильтры в query


    Теперь нужно при выборе фильтров по марке и цвету изменять state, подгружать машины с бека и менять урл.

    javascript
    kotlin
    
      render() {
        if (!this.state.loaded)
          return null;
        return (
          <HomeHeader
        brands={this.state.brands}
        brand={this.state.brand}
        onBrandChange={brand =>
    this.navigateToChanged({brand})}
        colors={this.state.colors}
        color={this.state.color}
        onColorChange={color =>
    this.navigateToChanged({color})}
          />
        );
      }
    
      navigateToChanged({
        brand = this.state.brand,
        color = this.state.color
      }: Object) { //(*)
        this.props.history.push(
    `?brand=${brand || ""}`
    + `&color=${color || ""}`);
        this.setState({
          brand,
          color
        });
    
        this.loadCars()
        
      }
    

      override fun
        RBuilder.render() {
        if (!state.loaded) return
    
    
        homeHeader(
          brands = state.brands,
          brand = state.brand,
          onBrandChange = {
    navigateToChanged(brand = it) },
          colors = state.colors,
          color = state.color,
          onColorChange = {
    navigateToChanged(color = it) }
        )
    
      }
    
      private fun navigateToChanged(
        brand: String? = state.brand,
        color: String? = state.color
      ) {
        props.history.push(
    "?brand=${brand.orEmpty()}"
    + "&color=${color.orEmpty()}")
        updateState {
          this.brand = brand
          this.color = color
        }
        launch {
          loadCars()
        }
      }
    


    И здесь опять проблема с дефолтными значениями параметров. Почему-то flow не разрешил мне одновременно иметь типизацию, деструктор и дефолтное значение взятое из state. Возможно, просто бага. Но, если бы все-таки вышло, то пришлось бы объявить тип за пределами класса, т.е. вообще на экран выше или ниже.

    Рисуем таблицу


    Последнее что нам осталось сделать — написать stateless component для отрисовки таблицы с машинами.

    javascript
    kotlin
    const HomeContent = (props: {
       cars: Array<Car>
    }) => (
      <DataTable value={props.cars}>
        <Column header="Brand"
                body={rowData =>
          rowData["brand"]
                }/>
        <Column header="Color"
                body={rowData =>
          <span
        style={{
          color: rowData['color']
        }}>
            {rowData['color']}
          </span>
      }/>
        <Column header="Year"
                body={rowData =>
          rowData["year"]}
        />
      </DataTable>
    );
    

    private fun RBuilder.homeContent(
      cars: List<Car>
    ) {
      datatable(cars) {
        column(header = "Brand") {
    
          +it.brand
        }
        column(header = "Color") {
    
          span {
            attrs.style = js {
              color = it.color
            }
            +it.color
          }
        }
        column(header = "Year") {
    
          +"${it.year}"
        }
      }
    }
    


    Здесь видно, как я выпрямил 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. Корутины — делим процессорное время

    Комментарии 83

      +9
      Может логичнее было бы сравнивать с TypeScript все же?
        +3
        да, и сразу можно было бы выкинуть babel с flow и оставить только вебпак
          –6
          Проблемы были бы примерно теже. TypeScript практически ничего не вносит нового, кроме адекватного наследования по сравнению с flow (могу ошибаться, не так много на нем писал). При этом у него всё таже проблема: нет гарантий того, что скомпилиный код будет работать так, как предполагалось. Когда я узнал, что степень строгости компиляции нужно указывать в конфигах и по-умолчанию она совсем слабая, я совсем растроился
            +3
            Плюсы ts по сравнению с babel + flow
            -ts компилятор быстрее babel и генерит чуть более оптимальный код (например нет лишних оберток для классов не имеющих родителя).
            -ts отлично поддерживается в ide (vscode — из коробки, sublime — офф плагин от ms github.com/Microsoft/TypeScript-Sublime-Plugin, WebStorm — из коробки)
            -npm @types
            -c 3.0.0 расширены возможности типизации rest параметров
            -keyof ( или в flow тоже есть аналог? )
            -когда ставишь ts нет ощущения, что тебе в node_modules упало половина всего npm
            -информацию о типах при желании можно вытянуть в runtime через декораторы (например на этой основе можно генерить валидаторы, gui, мета информацию для orm github.com/typeorm/typeorm)
            В flow тоже так можно, но это несет доп издержки в рантайме codemix.github.io/flow-runtime/#
            -У ts целостная экосистема, в отличии от babel где все перемотано изолентой.
            Я так и не понял зачем нужны все эти пресеты в виде отдельных пакетов. Если уже используется транспилятор смысл использовать не все потенциальные фичи новых стандартов? В крайнем случае можно было бы сделать с конфиги одну настройку experimental включающую поддержку фич, попадание которых в стандарт находится под вопросом. Но в babel наворотили непонятно что. В итоге мануалы старше полугода уже становятся неактуальны и непонятно какой пресет нужно ставить для той или иной фичи.
            -tsconfig поддерживает множественные псевдонимы путей ( пример
            "@middlewares/*": [
                            "./src/middlewares/*",
                            "./src/core/middlewares/*"
                        ],

            ), которые не ломают автокомплит в ide.
              –1
              Да мне стоило по-другому сформулировать мысль. «практически ничего не вносит принципиально нового»
                0
                Позволю себе вас поправить: «Да мне следовало по-другому сформулировать мысль. Я не люблю тайпскрипт принципиально». Это было бы уместнее.
          –9
          На Github у Kotlin 24 тыс звезд, у React 110 тыс. Очевидно, что React ненужная никому и устаревшая технология, вы совершенно правы :)

          П.С. Сравнение теплого с мягким это дело субъективное…
            0
            Ни в коем случае не выступал против React. Я даже не выступал против JavaScript. Скорее о том, что если интересна строгая типизация на фронте, то есть хорошей язык для этого.
            Никогда не сравнивал звезды, но не стал бы пытаться сравнивать язык и фреймворк.
              –6
              Серьезно?
              Пока я готовил эту статью, я узнал кучу новых вещей о современном Javascript: flow, babel, async/await, шаблоны jsx. Интересно, насколько быстро эти знания устареют? И всё это не нужно, если использовать Kotlin. При этом знать о React нужно совсем немного, потому что большая часть проблем легко решается при помощи языка.


              Заголовок намекает, а заключение утверждает, что Javasript + React забудется и устареет, а Kotlin + React это прогрессивно. Однако сравнение востребованности говорит об обратном. Те кто пишут на Яве не спешат переходить на Котлин, а Реакт довольно популярен.
              Но опять же, каждому свои фломастеры.
                0
                Если уж так рассуждать, то flow и TypeScript «забудутся и устареют». Но всё совсем не так. Каждая технология занимает свою нишу и время. JS будет жить очень и очень долго, просто потому что он живет в браузере. flow, TypeScript, React, redux, RxJs, Kotlin — это всё толи паразиты, толи симбионты. Они решают типовые проблемы, которые сам JS не спешит решать, а если решает, то медленно (ESтакой-то). В Java мире тоже самое: Kotlin решаем набор наболевших проблем. Кто-то готов ради этого учить новое, кто-то нет. И везде встречается костность людей. «Работает и хорошо», особенно, если это большая компания, где польза от новой технологии обычно перевешивается тонной легаси, необходимостью обучения всех и выроботки стандартов работы.
                  0
                  Не вижу смысла спорить, как я уже сказал фломастеры нужна всем разные. Вам кажется что Kotlin решает ваши проблемы? Отлично! Кто то скажет что React лучше держит нагрузку? Пускай пишет на React. Все упирается в навыки команды и требования бизнеса.
                    0

                    А чем вам Redux и RxJS не угодили?

                      0
                      Во-первых, Koltin я поставил в тот же ряд. Во-вторых, я так и не понял зачем использовать redux и RxJs при работе с React. RxJs вполне перекрывается корутинами или async/await. Redux вносит глобальное состояние, что просто зло ИМХО.
                        +1
                        Примерно такое же зло как СУБД на бэке. Redux при правильной готовке реализует одну глобальную точку входа для работы с композицией множества локальных состояний.
                          0
                          А почему глобальная точка входа должна быть одна? Зачем всю систему завязывать на один элемент? Как потом разбираться какой код можно выбросить? Как оченить степень последствий при изменениях?
                            +1
                            Как раз чтобы проще было оценить степень последствий при изменениях. Если у нас единая точка входа, то мы можем контролировать все обращения к состоянию, подменять результаты, логировать и т. п. в одном месте.

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

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

                              +1
                              А почему глобальная точка входа должна быть одна? Зачем всю систему завязывать на один элемент?

                              Как выше подметил VolCh, такие же вопросы можно адресовать к СУБД. Вообще до Redux были аналогичные многосторовые решения, начиная с Flux, но они не прижились.


                              Для большинства фронтовых приложений удобно рассматривать состояние приложения как единую сущность. Любое действие в приложении вызывает детерминированное изменение его состояния. То есть приложение можно рассматривать как конечный автомат. Вот Redux и является реализацией такого конечного автомата.


                              На практике, однако, локальные изменения компонентов (такие как открытие-закрытие) часто удобнее хранить в их стейте. Можно вообще пользоваться исключительно стейтами компонентов, но рано или поздно возникнет необходимость во взаимодействии компонентов — отправке событий и получении данных. Если получится реализовать архитектуру проще и удобнее — пожалуйста, никто этого не запрещает. Redux всё-таки не панацея и даёт много лишнего кода.


                              Как потом разбираться какой код можно выбросить? Как оченить степень последствий при изменениях?

                              Если речь об экшенах и редьюсерах, то тривиально по их использованию. Разумеется, типизировать экшены нужно константами, чтобы была навигация по коду.


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

                                0
                                Спасибо за объяснения от Вас и от VolCh
                                У меня подобные проблемы встречались пару раз, но не в слишком сложной форме. Объявление пары синглетонов полностью решало задачу. Но у меня простой интерфейс, возможно, при более сложных взаимосвязях это и имеет смысл.
                                  +3

                                  А пара синглтонов — это не глобальное состояние? :)

                                    0
                                    Именно так. Но такие синглетоны формируются вокруг точки применения, а не вокруг факта того, что они хранят глобальное состояние (что подразумевает «единая точка входа»). И они не добавляют новый слой абстракции
                                      0

                                      Это может быть хорошо до того момента, пока не придётся использовать данные в разных местах интерфейса. Например, список сообщений в компоненте сообщений, счётчик непрочитанных в шапке страницы и такой же в боковом меню, а ещё всплывающие нотификации о новых сообщениях.


                                      И что такого плохого в лишнем слое абстракции?

                                        0
                                        Хороший пример, спасибо, я подумаю над этим.

                                        И что такого плохого в лишнем слое абстракции?

                                        То, что это новая библиотека, новые термины, новые ограничения, которые надо увязать со всем остальным. Чем меньше движущихся частей, тем легче поддерживать и писать новый код.
                                          +1
                                          Чем меньше движущихся частей, тем легче поддерживать и писать новый код.

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


                                          Всё-таки я бы посоветовал вам разобраться с Redux, RxJS и атомами ($mol, Knockout, Mobx). Prior art всегда полезно знать.

                                            0
                                            Ну как сказать легче. Ну вот я, прийдя на какой-то проект, вряд ли бы додумался искать какие-то синглтоны. Увидел что никакого знакомого хотя бы понаслышке стейт менеджера нет зависимостях и стал бы поднимать данные, нуждающиеся в шаринге на общий компонент, чаще всего это корневой App ну или какой-то Main. Синглтоны, имхо, куда болшее зло чем одна глобальная точка входа в хранилище.
                              0
                              Redux вносит глобальное состояние, что просто зло ИМХО.


                              Мне прямо интересно какую альтернативу вы предлагаете. А то Ваш пример из статьи с хранением состояния в ближайшем State компонента получит проблемы при необходимости разделения состояния между двумя компонентами. Особенно когда у вас иерархия компонентов перестанет совпадать с иерархией данных. Например показывать текущую страницу из основного списка на странице в заголовке, определяемым в Layout'e.

                              Сразу отвечу на некоторые Ваши вопросы.

                              А почему глобальная точка входа должна быть одна? Зачем всю систему завязывать на один элемент?

                              Да можете хоть десять сделать, просто вы при этом никакой проблемы не решите, зато добавите проблему с синхронизацией точек входа в ваши данные. Собственно из-за наличия возможного разделяемого состояния, это никак не поможет с «Как потом разбираться какой код можно выбросить? Как оценить степень последствий при изменениях?»

                              Как потом разбираться какой код можно выбросить? Как оценить степень последствий при изменениях?

                              В случае Redux+JS, проверить селекторы и использующие их компоненты. Удалить и проверить тесты.
                              В случае Redux+(TS/Flow/Kotlin) — просто удаляйте пока компилируется.
                                0
                                Например показывать текущую страницу из основного списка на странице в заголовке, определяемым в Layout'e.

                                В своем проекте я это решил через наследование, что больше соответствовало моему случаю (header отличается для разных страниц). Вот пример

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

                                Удалить и проверить тесты… просто удаляйте пока компилируется

                                Я часто на работе сталкиваюсь с вопросом «а можем ли мы сделать то-то» и если я смогу получить ответ только изменяя код и тем более запуская тесты, то это будет слишком долго.
                    +6

                    шаблонные строки в JS тоже есть, hello ${foo}. И по-моему typescript лучше чем js+flow

                      0

                      чертов парсер. Шаблонные строки окаймляются бэктиками, `

                      +1
                      Темплейты и optional chaining есть и в джсе (и даже можно подключить во флоу, в отличии от тайпскрипта)
                        0
                        Класс, буду знать
                        0
                        «Во-вторых валидность пришедшего json проверяется во время десериализации, а не когда-нибудь при обращении» — что конкретно проверяется?
                          0
                          что все ожидаемые поля есть, они могут конвертиться в нужные типы и что они не null, если так объявлено поле. И так по цепочке во всех вложенных объектах.
                            0
                            Пусть есть класс с необязательными полями: a: string, b: number, c: bool
                            { «a»: «aaa» } — валидный? Или пустые поля всегда передаются?
                            { «a»: 33 } — ?
                            { «a»: «aaa», «d»: 33} — ?
                              0
                              Попробовал: первые два валидны, третий в зависимости от флажка nonstrict сериализатора.
                              Валидность второго — вопрос, конечно, спорный
                                0
                                Ну примерно как я и думал. Основная ценность — общий котлин класс на сервере и клиенте, а проверки — весьма вторично.
                                  0
                                  Я бы так не сказал. Вот пример json ответа из моего проекта:
                                  Заголовок спойлера
                                  {
                                    "event": {
                                      "id": 52413,
                                      "name": "Событие",
                                      "owner": "Мой любимый клуб",
                                      "fromKu": "KU6",
                                      "toKu": null
                                    },
                                    "timetables": [
                                      {
                                        "id": 52414,
                                        "begin": 1524768960000,
                                        "end": 1524765600000
                                      },
                                      {
                                        "id": 52415,
                                        "begin": 1524168840000,
                                        "end": 1524169080000
                                      }
                                    ],
                                    "tariffs": [
                                      {
                                        "id": 52418,
                                        "name": "один день",
                                        "quantity": 1,
                                        "timetablesIds": [
                                          52414
                                        ],
                                        "price": "1000.00"
                                      },
                                      {
                                        "id": 52419,
                                        "name": "два дня",
                                        "quantity": 2,
                                        "timetablesIds": [
                                          52414,
                                          52415
                                        ],
                                        "price": "2000.00"
                                      }
                                    ],
                                    "demandsCount": 0,
                                    "pretendersCount": 8,
                                    "debit": {
                                      "cashless": "0",
                                      "cash": "5000.00",
                                      "full": "5000.00"
                                    },
                                    "id": 52413
                                  }
                                  


                                  Внутренние объекты как есть передаются на отрисовку и где-то очень далеко после уже используются. И если проверки не было бы, то мне пришлось проверять весь код до того места, потому что я воплне мог накосячить в js части
                                    0
                                    Ну только если голый js. Всё это прекрасно разбирается через JSON.parse() и моделируется TypeScript классом. Если язык на сервере и клиенте одинаков и сборка идёт из shared исходников, то проблемам в передаче взяться неоткуда.
                                      0
                                      За исключением того, что версии серверва и клиента будут какое-то время разными
                                        0
                                        Ну и чем тут Котлин поможет? Если изменились названия и типы полей по любому версионность на api вводить, если просто добавились новые поля, то и проблемы нет.
                                          0
                                          Представьте себе, что на вас регистрируют багу: на странице где-нибудь отрисовалось undefined. Вы смотрите на код — всё ок, вы пытаететь воспроизвести — не воспроизводится. И только через пару часов Вы выясняете, что это коллега Вася в это время выкатывал новый релиз без обратной совместимости и что надо просто Васе голову скрутить, а бага была только в процесе релиза.

                                          Если бы была проверка на валидность json, то Вы бы не потратили несколько часов в такой ситуации.

                                          Это не отрицает необходимость обратной совместимости api
                                            0
                                            Представьте себе, что на вас регистрируют багу: на странице где-нибудь отрисовалось undefined.

                                            В случае с React+Redux я сначала открою DevTools и посмотрю, что именно лежит в нужной ветке стора. Если там какая-то фигня, то затем я проверю входящие данные от API.


                                            А вообще, если бы в проекте был настроен прогон тестов, то CI дал бы по шее Васе ещё в моменте его деплоя, и просто оставил всё как есть.

                                    0
                                    Но насчет основной ценности — полностью согласен. В моем случае это вообще было основной причиной выбора этого стека
                            0

                            Как вам показался котлиновский сериализатор на вкус? Я пытался его использовать, но он плохо поддерживается IDEA'ей (говорится что нужно ставить отдельный плагин для идеи, но он не помог), кроме того почему-то для моих случаев получались какие-то костыли

                              0
                              Я без плагинов жил, нормально. Там есть проблемы, безусловно. Например, нет адекватного способа сериализовать KClass. Когда пишешь общую библиотеку, надо везде таскать KClass, чтобы потом им пользоваться (правда на такой глубине использования грех жаловаться). Ещё там какая-то странная логика при кастомных геттерах, лучше туда не лезть.
                              А так, пишешь data class простенький, помечаешь аннотаций и всё работает на ура.
                                0
                                а схему генерить, кстати, умеет? я так и неразобрался
                                  0
                                  Вот это я не пробовал.
                              –3
                              На первом скриншоте пример того, как не надо писать код. Что за ужасный стиль вместо простого и понятного

                              function Question() {

                              писать кучу скобочек и стрелочек:

                              const Question = () => {

                              Это типичная болезнь яваскриптщиков, когда появляется какая-то фича, они ищут, где бы ее использовать, даже если это не нужно, а некоторые всерьез верят, что «в 2018» слово function устарело.

                              Стрелочные функции придуманы для удобной записи маленьких анонимных функций в таких ситуациях:

                              var names = array.map(users, u => u.name);

                              Не надо их везде пытаться использовать.
                                +2
                                Как-то все забывают, что основная задача стрелочных функций — все таки использование родительского контекста (отсутствие собственного this, arguments, super и new.target).
                                  0
                                  В примере на картинке this не используется.
                                  0

                                  Мне нравится больше const Question = () => {
                                  Когда дольше используешь стрелки полностью привыкаешь к такому стилю.


                                  Минусов такого подхода я не вижу.


                                  Стрелочные функции придуманы ...

                                  Это вообще не аргумент. Какая разница для чего оно изначально делалось? Пирамиды тоже не для туристов строили.

                                    0
                                    В подобном подходе вообще минусов нет, за исключением одного и единственного: в React dev tools будет выводиться что-то типа Anonymous Component (или просто Component). Такая же штука с Vue. Иногда очень полезно видеть дерево компонентов и у кого что внутри твориться (пропсы, стейт, контекст). Естественно на продакшене такие вещи всякими минификаторами убиваются (насколько помню, можно выключить).
                                    +1
                                    Области применения обычных и стрелочных функций пересекаются. Какие использовать в пересечении — вкусовщина.
                                    0
                                    Получение машин на JS можно было короче (в котлине почему-то используете темплейт-стринг а в ЖС — нет):
                                    async loadCars() {
                                        const uri = `/api/cars?brand=${this.state.brand || ''}&color=${this.state.color || ''}`;
                                        this.setState({
                                                          cars:   await (await fetch(uri)).json(),
                                                          loaded: true,
                                                      });
                                    }
                                    
                                      0
                                      Спасибо за пример, везде заменил на этот вариант
                                      0
                                      А что по размеру бандла? Где получилось больше — у Kotlin или JS?
                                        0
                                        Сейчас померил:
                                        JS — 3.5mb
                                        Kotlin — 5.4mb
                                          0
                                          Это как? Финальный js на 5 мег? Да не может такого быть.
                                            0
                                            да, что-то тут не так. Весь мой проект о котором говорил в статье собрался в 2мб. Видимо как-то не так конфиги сварил, попробую разобраться
                                              0
                                              Разобрался. Забыл подключить UglifyJsPlugin.
                                              JS — 375kb
                                              Kotlin — 752kb
                                                0
                                                Это js, не gzip, да?
                                                  0
                                                  да, без gzip. Не стал поднимать nginx, сжал в tar.gz
                                                  JS — 83kb
                                                  Kotlin — 139kb
                                                  Теперь разыв не в ~2, а ~1.5 раза
                                                    0
                                                    Кстати, спасибо за мысль, надо будет включить сжатие на серваке
                                                    0
                                                    т.е хелло-ворлд проект весит 750кб? средний мобильник будет открывать такую страницу секунд 5
                                              0
                                              Если вынести за скобки сборку и подобное, то для непосредственного написания кода с одной стороны остается Kotlin+React, а с другой Javascript+React+babel+Flow+ES5|ES6|ES7

                                              Какая странная формулировка. Babel — это инструмент сборки, а ES5|ES6|ES7 — это часть языка Javascript. Корректнее будет написать Javascript+Flow+React, а если взять Typescript, как советуют комментаторы выше, то будет просто Typescript+React, что никак не сложнее Kotlin+React

                                                0
                                                С Babel всё-равно придется иметь дело в любом случае, потому что он отвечает за то, чтобы ( ) превратился в React компонент. Наверно, все к этому привыкли и не замечают (хотя там даже для использования обычного JS надо {} писать).
                                                  0
                                                  он отвечает за то, чтобы ( ) превратился в React компонент

                                                  У вас в скобочках контент потерялся. Подозреваю что там было что-то типа <ComponentName />.


                                                  В моем понимании это часть формулы Javascript+React. Просто такой специальный синтаксис для React. Не обязательно использовать Babel, есть и другие инструменты, Typescript его понимает из коробки, или можно взять buble, например. Это нюансы сборки, на процесс кодинга они не влияют.

                                                    0
                                                    специальный синтаксис для React

                                                    Которого нет в Kotlin варианте. А вместе с ним нет никаких ограничений что код, а что не код.

                                                    Я уже упоминал, что сторогость компиляции TypeScript зависит от конфигов, что означает, что и сам код зависит от этих конфигов (наверно, очень весело поддерживать одновренно два проекта на TypeScript с разными конфигами, у меня один, слава богу). Код зависит от того, какой ES подключен (набор разрешенных конструкций, насколько я понял). Т.е. чтобы понять что и как ты можешь писать сейчас, надо знать настройки конкретного проекта. В случае Kotlin у тебя всегда полная реализация языка без каких-либо допущений и подкручиваний.
                                                      +1
                                                      нет никаких ограничений что код, а что не код

                                                      А какие могут быть ограничения? В любом случае специальные конструкции придётся учить, даже если это сахар языка. Все-таки билдеры встречаются не в каждом Kotlin-проекте


                                                      сторогость компиляции TypeScript зависит от конфигов

                                                      Странно ставить гибкость конфигурации в недостаток. Наоборот, за счёт этого можно мигрировать существующие проекты постепенно. Переименовали файлы JS -> TS и постепенно наращиваем строгость типизации.


                                                      В случае Kotlin придётся переписывать проект целиком в один присест, а это может растянуться надолго.

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

                                                        И никто не заставляет переписывать весь проект целиком. Я не пробовал серьезных сочетаний, но, кажется, это ничем не отличается от TypeScript и JS в одном проекте.
                                                    0
                                                    С Babel всё-равно придется иметь дело в любом случае

                                                    Зачем? TypeScript умеет работать с JSX.
                                                  0

                                                  Я как-то решил попробовать пару примеров, которые есть на GitHub-е на тему Kotlin + React. Предлагаемые решения очень сильно интегрированы в инфраструктуру JS с использованием его жуткого билд-стека, поверх которого еще стоят gradle-плагины.
                                                  Из неприятных эффектов — полная рекомпиляция бандла при любом малейшем изменении, которое может длиться от 30 сек. и больше взависимости от кодовой базы. Разрабатывать с таким темпом практически невозможно, из-за чего до сих пор юзаю GWT, который умеет инкрементальную компиляцию.

                                                    0
                                                    я использую сочетание gradlew --continuous и webpack-dev-server, так что изменения подхватываются автоматически. Обновление при это, действительно, в районе 30 секунд. Возможно, это можно ускорить, не пробовал. Для меня это приемлемое время, особенно по сравнению с jsf, где на каждый чих надо перезапускать сервер. При этом в большинстве случаев мне не надо сразу смотреть результат, а когда надо, то результат уже обновлен. Кроме того, ребята из JetBrains посмотянно тюнят компилятор, когда-нибудь оно само ускорится
                                                      0

                                                      Для меня критично в 2018 году ждать 30 секунд на любое изменение. С увеличением кодовой базы и используемых сторонних модулей это время будет только расти. Тогда как на сам перезапуск сервера тратится 1.5 секунды (Embedded Jetty).


                                                      Проблема там не столько в самом компиляторе Котлина, сколько в бандлере. Чем больше библиотек — тем дольше приходится ждать. Простой хелло ворлд компилируется 10 секунд. Подключите React и время сразу увеличивается до 30.


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

                                                        0
                                                        У меня bundle отрабатывает за 6-7 секунд на подхват изменений. Наверняка можно сделать быстрее. Стартует, правда, долго изначально, но это не имеет значения вообще.
                                                        Список зависимостей
                                                          "dependencies": {
                                                            "react": "15.6.1",
                                                            "react-dom": "15.6.1",
                                                            "react-router-dom": "4.2.2",
                                                            "react-sticky": "6.0.1",
                                                            "primereact": "1.4.0",
                                                            "chart.js": "2.7.2",
                                                            "react-transition-group": "^2.2.1",
                                                            "font-awesome": "^4.7.0",
                                                            "classnames": "^2.2.5",
                                                            "bigdecimal": "^0.6",
                                                            "moment": "^2.20",
                                                            "file-saver": "1.3.3"
                                                          }
                                                        


                                                        При этом перезапуск java сервера у меня с минуту идет (база, liqubase, spring...).
                                                        У вас 1.5 секунды с учетем перекомпиляции (хоть и инкрементной) запуска jvm и подобного?
                                                          +1

                                                          Ну вот я пробовал официальные туториалы в т.ч. create react kotlin app и котлиновский tictactoe. Они все используют gradle kotlin frontend plugin, который в свою очередь пускает npm, webpack и babel. Когда я меняю строчку и делаю рефреш, все-равно компилится около 30 секунд — не важно, новый бандл или рефрешнутый. Пробовал поковыряться с настройками, но по ходу нет способа ускорить.


                                                          Компиляцию не учитываю. Запуск JVM и бутстрап приложения. Я всегда пользую Embedded Jetty, который сам по себе стартует меньше секунды. Архитектура, где нужно запускать сторонний сервер и туда что-то деплоить — это из эры динозавнов. Приложение обычно использует легкий фреймворк типа Javalin, пускает Flyway для базы, зачастую также использую Eclipselink (JPA), который пускается в 3-5 раз быстрее Hibernate. Иногда прикручиваю Google Guice для DI, но это лишних 0.5 — 0.7 секунд стартапа. Spring (Boot) принципиально не использую.

                                                            0
                                                            gradle kotlin frontend plugin не стал использовать. Шаг влево, шаг вправо — расстрел. Да и версии используемых компонент не контролируешь. Когда буду писать статью о настройке всей этой конструкции, попробую получить оптимальное время
                                                    0
                                                    del не в ту ветку
                                                      0

                                                      Как по мне, так основная проблема, которую необходимо решить любому языку, транспилирующемуся в js, — это размер коммьюнити и возможность использовать существующие библиотеки. Тот же Typescript имеет огромное коммьюнити и кучу типов для уже известных js библиотек. Как у этого с котлин?
                                                      Самому нравится шарить DTO и валидацию между фронтом и беком. Для этого все пишу на

                                                        0
                                                        С этим у Kotlin плохо, но есть одно но: сконвретировать TypeScript в Kotlin очень просто, так что «куча типов для уже известных js библиотек» есть и у Kotlin с небольшой платой.
                                                          0

                                                          На самом деле ими далеко не всегда можно воспользоваться из-за некорректной работы конвертера.

                                                            0
                                                            Да, там немножко надо подпиливать напильником, но при учете не слишком частой необходимости вообще подключения новой бибилиотеки, это вполне можно потерпеть.
                                                              +1

                                                              Есть случаи, когда проще переписать вообще всю типизацию, чем пытаться её допилить, но пример могу привести только бэкендный.


                                                              Кстати типизация бывает глючит даже с "родным" TS, приходится вставлять as any :(

                                                                0
                                                                Зачастую в «родных» TS декларациях без поллитры не разберёшься, методом тыка пытаешься понять что же ожидает или возвращает метод. А уж подпиливать где-то то, что нагенерено чем-то до конца не понимая что в оригинале происходит вообще как-то грустно…

                                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                        Самое читаемое