Несколько советов о том, что нужно знать, чтобы писать (или не писать) приложения под React Native.

Сразу оговорюсь, что я ни разу не писал приложения под iOS, однако участвовал уже минимум в 4 проектах с React.js, немного разбираюсь в objective-c и знаком с процессом разработки под Android.

Приложение довольно простое (todo лист), но думаю, что это хороший старт.

Задача: написать таск менеджер с монетизацией. Есть наброски интерфейса на invisionapp, остальное — дело техники.

Data-flow


Самое главное в react приложениях — правильно выстроить data-flow и взаимодействие между различными компонентами с самого начала. Тогда есть вероятность, что компоненты будут только рендерить данные, а не превратятся в простыню из асинхронных вызовов и кучу логики, которую «потом можно будет вынести» куда-нибудь в другое место. На помощь нам приходит библиотека Redux. Я не буду подробно описывать как она работает (информацию можно найти здесь и здесь).

В приложении есть один store с глобальным состоянием. Он собирается из нескольких редьюсеров, обрабатывающих отдельные коллекции. Для удобства также рекомендую подключить redux-thunk и какой-нибудь devtool для отслеживания состояний стора. К сожалению, для native я ничего подобного не нашел, но это не проблема, т.к. trace состояния пишется в 10 строчек.

Код middleware
var i = 0
const devtools = store => next => action => {
  const result = next(action)
  if (console.group) {
    console.group(`#${i++}`, action.type, action)
    _(store.getState()).map((val, key) => {
      console.log(key, val)
    })
    console.groupEnd()
  }
  return result
}


Redux-thunk нужен для комбинирования нескольких асинхронных действий, а также для действий, которые требуют получение текущего состояния. Пример: Для задачи нужно создать результат, который помещается в другую коллекцию, провести оплату и пометить задачу как архивированную. Здесь участвуют 3 коллекции и 3 различных действия (все асинхронные).

Глобальное состояние позволяет нам в любой момент времени сделать snapshot стора, загрузить его на другом устройстве и увидеть то же самое, что мы видели на первом. Это упрощает процесс воспроизведения багов при краш репортах (попробуйте сделать то же самое на objective-c). Идея не новая: clojure-script обертка для react использует точно такой же подход. Есть несколько презентаций, в которых говорится о плюсах и минусах решения. Кстати, о краш репортах, я использую Crashlytics (ныне Fabric.io).

Кроме того, глобальное состояние облегчает работу с навигацией (я так думал). На самом деле пришлось городить обертку для нативного навигатора, делать индекс операций и дублировать redux действия вызовами методов в componentWillReceiveProps.

Код компонента
  componentWillReceiveProps(props) {
    const routes = props.routes

    if (this.props.routes.opIndex != routes.opIndex) {
      switch(routes.lastAction) {
      case 'push':
        this.refs.navigator.push(routes.currentRoute)
        break

      case 'pop':
        this.refs.navigator.pop()
        break

      case 'replace':
        this.refs.navigator.replace(routes.currentRoute)
        break

      case 'replacePreviousAndPop':
        this.refs.navigator.replacePrevious(routes.currentRoute)
        InteractionManager.runAfterInteractions(() => {
          this.refs.navigator.pop()
        })
        break
      }
    }
  }



Используйте promises (или await/async синтаксис ES). Если их возвращать из действий redux, то можно без проблем сделать, например, загрузчик.

Быстродействие и верстка


Верстка — один из самых приятных процессов в React Native. В Android тоже используется xml для layout'ов и он удобен, если сидеть и разбираться, что для чего служит. Однако в реакте вы уже знаете, как это работает… если вам надо сверстать hello, world :) На деле получается непредсказуемое поведение компонентов. «position: absolute» работает не так как в браузере, flux-box тоже работает по-другому, изображения не выставляют размер автоматически от ширины и высоты картинки. Получается, что вы как бы знаете что нужно писать, код выполняется, но это выглядит не так, как хотелось бы. Спасает только адекватная документация и live reload, но это все равно удобней чем верстка под андроид.

Каждый раз добавляя новый View в иерархию приходила мысль в голову: «а не долго ли это будет рендерится». Долго. Большим компонентам нужно делать экран загрузки, но facebook об этом предупреждает. Если вы собираетесь повесить какую-то логику на создание компонента (например загрузку данных из хранилища при старте приложения), обратите внимание на InteractionManager. Он позволит сначала отрендерить все, что накопилось и только потом выполнить ваши действия.

Что касается производительности приложения в целом: получается все равно немного медленнее, чем нативные приложения. Однако для большинства пользователей это будет незаметно. Если бы вы хотели нарисовать свой сайдбар (как например в airbnb), то реакт не самое подходящее решение, нужно писать нативный компонент. Хотя есть реализации, но, к сожалению, пока не в виде библиотеки.
Еще один пример: Navigator и NavigatorIOS. Первый написан на js, второй как нативный компонент. NavigatorIOS работает быстрее, если долго сидеть и смотреть на переходы между сценами :) Опять же, большинство пользователей даже не заметят разницы. Вся проблема заключается в том, что управление данными и рендеринг позиций элементов происходят в одном потоке, отсюда лаг. К счастью, мне не пришлось писать нативные компоненты, все удалось оптимизировать вызовами InteractionManager'a и постепенной загрузкой компонентов.

Обработка событий от пользователя тоже требует внимания. Если взять TextInput и сделать value/onChage в state текущего компонента, все работает быстро, но если вы начнете передавать через props родительскому компоненту и делать обработку, 100% понадобится debounce. К слову, в браузере это работает быстро. Еще одно замечание по поводу TextInput: когда iOS подставляет автозамену слова, onChageText не вызывается. Я не решил эту проблему да и, честно говоря, не пытался. Можно просто отключить autoCorrect значением false.

Сторонние библиотеки


Если нужен sidebar, menubar, какой-нибудь хитрый загрузчик изображения с прогрессом или календарь, то готовые решения есть. Есть, но их не так много, как хотелось бы. Получается выбор не очень большой. Многие компоненты выносят методы для управления, что не очень хорошо вписывается в логику рендеринга из глобального состояния. Хотелось бы больше value/onChange :)

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

Дебаг и тестирование


С этим у реакта все замечательно. Можно запустить приложение сразу на двух устройствах (я запускал на iPhone и iPad для тестов) и при изменении, например, верстки, изменения одновременно отображаются сразу на двух устройствах. Очень удобно. Есть некоторые проблемы при дебаге на устройстве при помощи google chrome. Иногда может вылететь хром, иногда приложение не успевает подключиться к компьютеру. Последний раз он просто начал сыпать в консоль warning'и из-за того, что расходится время. Это можно пережить, ведь все-таки мобильное приложение.

Для автоматического тестирования я использовал jest (можно использовать любой другой test runner). Тестами покрывал только действия и редьюсеры. Компоненты не покрыты, потому что их много, а время ограничено. Информации по тестированию react native компонентов мало, но если сильно углубиться, то можно замокать таким образом, чтобы можно было тестировать верстку.

Заключение


Для меня, как веб-разработчика, появление react native очень облегчает разработку мобильных приложений, теперь я могу перенести свои знания в другую область, однако надо хотя бы понимать синтаксис objective-c, чтобы писать под iOS. React Native, да и сам по себе React позволяет не напрягаясь реагировать на изменяющиеся требования заказчика. Когда вы получаете новую задачу, вы скорее всего уже знаете как ее реализовать.

P.S. Итого у меня ушло чуть больше месяца на написание приложения с нуля при нулевых знаниях разработки под ios.

Скриншотик


Мастрид перед написанием приложений: facebook.github.io/react-native/docs/performance.html

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


  1. DexterHD
    15.01.2016 11:16
    +1

    P.S. Итого у меня ушло чуть больше месяца на написание приложения с нуля при нулевых знаниях разработки под ios.

    За это же время при равных начальных условиях и желании можно освоить разработку под iOS на Swift или Objective-C и написать такое же приложение нативным методом.


    1. webschik
      15.01.2016 11:43

      Но фактически за это время у нас есть приложение под 2 платформы, не нужно будет потом с нуля делать приложение под Android. В этом плюс, ИМХО.


  1. k12th
    15.01.2016 11:41

    Вот опять: const devtools = store => next => action => {
    Расскажите, пожалуйста, зачем вы так делаете?


    1. kosmaks
      15.01.2016 11:48
      +1

      Вам не нравится вложенность функций? Все по докам.


      1. k12th
        15.01.2016 11:50

        Нет, мне непонятно, почему const myFunction = arg = > {}.


        1. kosmaks
          15.01.2016 11:58
          +2

          Наверное, чисто эстетически выглядит лучше чем

          function devtools(store) { return next => action { ...
          

          В данном случае мне scope функции не так важен. Это даже больше quickfix. В модулях я стараюсь использовать function.


          1. k12th
            15.01.2016 12:00

            Понял, спасибо.


    1. xGromMx
      15.01.2016 17:29
      -1

      Эта операция называется каррирование ru.wikipedia.org/wiki/%D0%9A%D0%B0%D1%80%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5


      1. k12th
        15.01.2016 17:34

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


        1. xGromMx
          15.01.2016 20:24
          -5

          Ну если ты такой умный, то ты должен знать, что все функции в функциональщине имутабельные


  1. xGromMx
    15.01.2016 17:32

    Про xml в андроид уже давно есть декларативная штука вместе с вочером в реальном времени github.com/Kotlin/anko github.com/Kotlin/anko/blob/master/doc/PREVIEW.md и да, на kotlin писать в разы приятнее чем на java. Также видел, что swift портируют под андроид


  1. ximet
    15.01.2016 17:41
    +1

    Честно со статьи мне не понятно, зачем вообще пользоваться React-Native. Что это даёт и какие недостатки этому(кроме всяких юайных штук). Как я понимаю, всё равно производительность не идеальна, но видимо лучше прототипного метода аля Cordova. Но в остальном я не оч понял смысла использования


    1. kosmaks
      15.01.2016 18:06
      +1

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

      Из преимуществ: во-первых это частично общий код для android и ios. В идеале, все что нужно сделать для перехода на другую платформу — сверстать интерфейс, привычный пользователю системы. Во-вторых — реактивная парадигма и все что с этим связано. Не знаю можно ли это считать плюсом, но javascript. На нем писать намного приятнее, чем, скажем, на objective-c, но это мое мнение. Можно даже объединить код вашего веб-приложения и мобильного приложения. Я думаю это вполне реально :) Кстати, фейсбук предлагал реактивную платформу для ios в качестве нативного фреймвока. Недостатки выделять не буду. Полагаю, для этого нужно попробовать и сравнить.

      Вообще Ваш вопрос звучит как «зачем использовать какой-либо фреймворк, если можно все сделать на jquery?».


    1. xGromMx
      15.01.2016 18:20

      www.nativescript.org более продуман и низкоуровневее


      1. Imhotep
        16.01.2016 00:03

        А как у него с быстродействием в сравнении с React Native? Хотелось бы понять, какой из них двоих более перспективный и почему.


      1. hell0w0rd
        16.01.2016 06:02

        Иммутабельный? Нет? Тогда не надо.


  1. hell0w0rd
    16.01.2016 06:04

    Спасибо за статью. Видно, что прошли через определенные грабли.
    Я так понял flexbox api не соответствует аналогичному из html? Или только некоторые части?
    А зачем вы написали свой middleware для логирования, если есть redux-logger?


    1. kosmaks
      16.01.2016 12:07

      По большей части, конечно, соответствует. Отличается то как различные элементы на него реагируют. Текстовое поле например, вот, не тянется, если прописать flex: 1.

      Не знал про redux-logger. Да и в моем коде каждая коллекция в свой console.log пишется. Меньше кликов мышкой :)


      1. ximet
        16.01.2016 14:09

        на самом деле это обычное явление, что flexbox работает с некоторыми огрехами. Ровно также работает и в Cordova\PhoneGup.