Критика работы с React Native: наш опыт

При выборе языка React Native ожидается, что разработка одного приложения под две платформы займёт в два раза меньше времени, чем разработка двух приложений. Но по итогу оказывается, что разработка занимает столько же (если не больше) из-за сложностей, скрытых под внешним блеском и маркетингом.

В свете последних тенденций на рынке разработки ПО Android разработчик BytePace, Виктор Кинько, расскажет о некоторых подобных сложностях, с которыми ему пришлось столкнуться за последние несколько месяцев работы с React Native.

React Native адаптирует javascript под разработку для мобильных устройств. Это достигается тем, что для сборки проектов он использует несколько сборщиков - Metro Bundler, который интерпретирует JS-код и представляет ресурсы и сборщик целевой системы. В моем случае это был gradle для android. В теории приложение react-native должно запускаться довольно просто. Команда react-native run-android включает metro bundler и выполняет сборку приложения для всех подключенных android-устройств и эмуляторов.

В реальности оказалось, что даже на этом этапе есть сложности. На нашем проекте постоянно возникала ошибка "Unable to download JS bundle", которая означала, что bundler не может транслировать код в нативный. Как позже выяснилось, из-за того, что он не запустился. StackOverflow подтвердил догадки и подсказал, что стоит запускать bundler отдельным потоком с помощью команды react-native start. Это позволяет перезапускать bundler только если поменялся package.json, потому процедура не сильно замедляет разработку.

Package.json - это файл, содержащий набор внешних модулей для приложения. На npmjs.com находится большое количество различных библиотек для react-native, расширяющих функционал и упрощающих разработку. Многие библиотеки (например, firebase) используют нативные функции, а потому должны быть связаны напрямую с нативным кодом. Для этого используется команда react-native link <library-name>, которая должна настраивать эти связи с нативным кодом.

Из-за того, что все библиотеки пишутся в разное время, они используют разные версии SDK и требуют разного подхода. Иногда бывает так, что библиотеки несовместимы друг с другом, или последняя версия библиотеки оказывается экспериментальной, и сами разработчики советуют понизить версию до предпоследней. Довольно часто link не настраивает все требуемые зависимости. Так, для вышеупомянутого firebase требуется добавить множество дополнительных библиотек в нативном коде, подключить различные внешние репозитории, модифицировать mainApplication.java (и это только для android!). Для firebase есть достаточно понятная инструкция по выполнению этих действий, но для других библиотек она присутствует не всегда.

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

• rmdir node_modules /s /q && npm cache clean-force && npm i

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

• rmdir android/app/build /s /q

В ходе разработки было замечено, что часто неудачный билд - следствие того, что сборщик не может создать (или удалить) папку из директории debug. Данное действие решает проблему того, что react не может самостоятельно удалить папку. Но в то же время генерация файлов для этой папки с нуля опять же займёт дополнительное время.

• react-native start-reset-cache

Запустить Metro Bundler. Эта вкладка должна оставаться открытой на протяжении всего процесса отладки. Если случилась ошибка, то лог ошибки может появиться здесь. Скорее всего при ошибке этот процесс завершится, и его снова нужно будет перезапустить.

• react-native run-android

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

Представим процесс сборки последовательностью команд для одного проекта (уже имеющего realm, redux, react-navigation, ещё около десяти библиотек) после подключения firebase.

rmdir node_modules /s /q && npm cache clean-force && npm i react-native start react-native run-android >> не удаляется папка debug react-native run-android >> ошибка, metro bundler закрыт react-native start react-native run-android >> не удаляется папка debug react-native run-android >> установка успешна! Но metro bundler закрылся, а потому js-код не читается react-native start >> после нажатия restart в установленном приложении на телефоне оно загружается и наконец-то работает

Стоит ли говорить, что это занимает действительно много времени? И это не единоразовый процесс: к описываемому моменту эта процедура требовалась почти после каждого изменения в коде программы. С каждой новой библиотекой проект становится всё менее стабильным, и данный процесс может меняться, чаще всего в худшую сторону. Отладка приложения - это одна из важнейших функций для разработчика, а в данном случае её скорость довольно сильно уменьшается.

Кстати об отладке. Отладчик React Native имеет проблемы не только с запуском. Исправление ошибок, найденных вследствие теста, тоже довольно болезненный процесс. В react-native JS-код транслируется в Native-код, но в процессе трансляции обфусцируется. Так что если не хотите видеть ошибки типа "null pointer exception in zzz.yyy()", то нужно пользоваться встроенным отладчиком, не получится просто читать exception'ы в logcat. При ошибке отладчик показывает красный “экран смерти” с её описанием, более-менее подталкивающим в сторону пути исправления. Но и с этой частью есть проблемы.

Хорошо, когда ошибка выглядит так:

Критика работы с React Native: наш опыт

Здесь действительно понятно, что происходит - вместо ожидаемого объекта массива в переменной this.state.noteArray.map находится undefined, из-за чего возникает пресловутая TypeError. Исправить её можно переходом на app.js:14 и проверкой значения в данной переменной перед использованием.

Хуже, когда ошибка выглядит так:

Критика работы с React Native: наш опыт

Так:

Критика работы с React Native: наш опыт

Или так:

Критика работы с React Native: наш опыт

Изображения взяты из интернета, но я видел их и "вживую". И несмотря на то, что они показываются в runtime, эта ошибка не связана с тем, что что-то было сделано неверно именно в вашем коде. Это может быть следствием того, что вы неверно установили библиотеку, или если в ваших импортах есть несовместимые зависимости, или что-то пошло не так в native-коде, а ошибку пытается отловить React. Каждая ошибка индивидуальна и решается крайне по-разному. Хорошо, что существует StackOverflow и хоть какой-то режим отладки.

Еще хуже, когда ошибка не воспроизводится в debug. С данной ситуацией я столкнулся при попытке собрать приложение с новой версией React с поддержкой x64-архитектур для Android. При установке приложения с дебаггером всё работает отлично. Но как только я делаю сборку тестеру на телефон, всё прекращает работать и ломается как только доходит до взаимодействия с базой данных. Чтобы отладить неотлаживаемое, на скорую руку используем консольные сообщения, в роли которых в данном случае выступал компонент react toastAndroid. Этот компонент выводит на экран короткий текст по достижению определенной строчки кода. Методично, желательно деля код пополам, локализуем функцию, в которой происходит ошибка, и выясняем, что метод Object.assign({}, item) не работает в новой версии React. Повезло, что можно было заменить эту функцию на более короткую {...item} при сохранении функционала приложения, но поиск этой ошибки обошелся в примерно десяток часов работы.

После было проведено небольшое исследование в поисках причин. Как обнаружилось, для интерпретации JS-кода в debug и production версиях React Native использует разные Javascript-движки: для отладки Chrome JS engine, а в работе JavaScriptCore. Да, React Native не переводит JavaScript в нативный код, а интерпретирует по ходу выполнения. При этом отладочный движок работает куда стабильнее, а потому баги всё чаще прокрадываются в production. К примеру, в этой статье показано, как форматирование даты работает в разных условиях. Возвращаясь к ошибке: так вышло, что после обновления версии React Native веб-движок production-версии потерял поддержку Object.assign(). А отладочный движок остался тот же.

Пожалуй, худший вариант - это случай, когда приложение ломается в случайных местах, только в production-версии и без каких-либо логов со стороны React Native. Пример: после установки релизной версии приложения на телефон оно "работает какое-то время", а потом "выключается без ошибки или предупреждения в случайный момент". Причём ошибка воспроизводится не на всех устройствах. В конце концов, методом проб и ошибок (и обнаружением того, что вышеупомянутый Firebase Crashlytics не присылает соответствующих ошибок) удалось выловить логи падения, которые выглядели так:

Ошибка "Android crashes: signal 11 (SIGSEGV)"
Ошибка "Android crashes: signal 11 (SIGSEGV)"

Этот текст даже не относится к нашему приложению, он даже не был отмечен красным. Но после того, как я его получил и отправился на форумы, я обнаружил, что новая версия React Native сломана. И предыдущая была сломана. На официальном Issue Tracker ошибка "Android crashes: signal 11 (SIGSEGV)" существовала уже два месяца, и к моей удаче за два дня до того, как я обратился туда(!), было предложено экспериментальное решение, которое исправило ошибку.

Иронично, что некоторые разработчики, которым приходилось сталкиваться с Android Studio, были в недоумении по поводу того, что в IDE есть такие опции как build/clean project или file/invalidate caches. Это требуется для того, чтобы избавиться от аномального поведения gradle, от ложных сообщений об ошибках и предупреждениях, от ошибок синхронизации. Разработчики спрашивали: "почему мы должны делать работу за нашу IDE, в таких ситуациях эти команды должны выполняться автоматически". И их можно понять, но в то же время современные IDE и так делают всю сложную работу за кадром. А эти разработчики попросту не работали с React Native.

Всё рассказанное - это единичные случаи, случившиеся за последние несколько недель. Здесь я не описываю сложности запуска приложений с Expo, с настройкой стиля кода в babel/eslint, не ругаю Javascript за излишнюю гибкость, не рассказываю как на одном из проектов почти полностью пропала возможность отладки из-за связки redux/realm. Учитывая описанные сложности поддержки и разработки и тот факт, что для двух систем это всё умножается на два, стоит задуматься, действительно ли React Native выгоден для разработки? После того как мы завершили наш третий проект на этом языке, мы решили, что нет. Как вы считаете?

99
23 комментария

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

4
Ответить

Меня на реакт подбил знакомый, работающий с React JS. До этого работал на на нативе android , iOs.
Достаточно легко было нарисовать простой список элементов, получаемых с сервера. Но когда дело пошло дальше: новостная лента, вебвью, перехват и обработка запросов, работа с БД - началась адская боль и страдания. Наверное надо быть js ником в третьем поколении, чтобы это выдержать. Война с версиями библиотек. Под айос еще как-то... pod install и все. Но с андроидом с градлом. Не в градле дело, а в вечных конфликтах версий... Да, запустилось на андроиде и не идет на айос. И наоборот. Куча кастомных приблуд. Куча ограничений. Не советую никому, кто будет писать что-то функциональное, мало-мальски сложнее презентационной странички. Тем более если вы отлично дружите с нативной разработкой на типизированных языках. После реакта пропало желание смотреть в сторону Flutter и прочих кросплатформенных приблуд. Пока что углубляюсь в котлин и свифт.

2
Ответить

Довольно.. забавное описание.
Ощущение, что процесс разработки происходил на линукс с его вечным цирком в плане прав доступа.
Бандлер обычно запускают отдельной командой, т.к это удобней( и можно почистить его кеш в случае странностей, т.е npm start — --rest-cache )
Часть ошибок( из показанного на скринах ) - ошибки связанные именно с JS. К примеру, вместо массива прилетел с бука объект или вообще null. Как ни странно, метода map у него не будет и попытка выполнения его вызовет ошибку.
Еще часть - когда отладка происходит на подключенном к ПК телефоне( обычно помогает adb reverse )
Далее, по крашам. Если приложение просто вылетает( тихо выключается ) - это сбой в нативной части. Если вместо приложения - вдруг красный(отладка) / белый(релиз) экран - сбой в JS части.

Package.json - файл, относящийся к npm, т.е пакетному менеджеру.
Штука позволяет довольно удобно настраивать версии пакетов.
npm - это не чисто RN-приблуда. Соотв., на npmjs полным-полно модулей, к RN не относящихся( там и для React.JS модули есть и для Node.JS и для многого другого )

Да, подключение модулей( особенно, файербейса и ФБ/Твиттера/Гугла - это обычно самая боль ) не всегда происходит запросто. Однако в разработке, подключение модулей происходит нечасто. Это не то действие, которое выполняется ежедневно и по 10 раз в день. Если вы подключили файербейс к андроиду, второй раз это делать не придется.
Если это сколь-нибудь большое приложение, то подавляющая часть времени - это  работа с версткой( и эта часть реально сильно сокращается в сравнении с независимой разработкой для 2 платформ ).

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

Основани часть ошибок, связанных при установке-подключении модулей, возникает из-за непонимания очевидных вещей:
Если был подключен нативный модуль( т.е был выполнен react-native link ), очевидно, что проект нужно полностью пересобрать. Желательно, почистить от старой сборки. Особенно проблемами с этим грешит android и gradle( этот "гений" создает столько мусора, что сам порой в нем разобраться не может ).
Если был подключен просто_JS модуль( т.е просто установлен через пакетный менеджер, без линковки ), полная переборка не требуется достаточно перезагрузить приложение( всм, Reload JS ).
В обоих вариантах лучше перезапустить бандлер( прибить и выполнить npm start — --reset-cache ).

п.с: сейчас для RN проектов никто npm не пользуется - пользуются yarn'ом, который несравненно быстрее.
п.с[2]: за Object.assign еще года 2 назад уже вовсю били.. ногами.. по лицу..
Это примерно аналогично тому, если бы всерьез ругались, что на винде десятке видите ли не работают программы времен DOSа, а так хотелось затолкать их в свежий релиз своего ПО.
п.с[3]: Expo надо обходить стороной

2
Ответить

Отдельно отметил бы вёрстку в RN - это только псевдо верстка под браузеры. На самом деле очень много нюансов есть и часто приходится юзать платформозависимые стили.

1
Ответить

А даёт ли это какой-то результат в скорости разработки или качестве?..

Ответить

Интересно было почитать о вашем опыте. Часто чешутся руки его изучить, особенно после комментариев под моей очередной статьей об Ionic Framework, что React Native круче.

Расскажите как сложно верстать кастомный дизайн приложения в React Native? Само собой в сравнении с нативом.

1
Ответить

Здравствуйте, спасибо за отзыв!
С Ionic совсем не знаком, потому не знаю насколько React лучше или хуже, но опыт несомненно интересный.

Если говорить о дизайне, то React Native в этом плане очень похож на верстку для веба с использованием css и Flexbox. Стоит заметить, что чаще всего лейауты связаны с функционалом, то есть у каждого компонента есть список входящих атрибутов, которые он поддерживает, что заставляет разработчика обеспечивать модульность при создании своих компонентов. Довольно оригинально и в то же время странно ощущается система обновлений элементов, когда к ним применяются новые атрибуты. С этим пришлось свыкнуться, поначалу разбирались и с тормозами, и с отсутствием перерисовки элементов по этой причине.

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

В итоге всё зависит от специфики приложения. Если это просто набор списков и кнопок, то в среднем верстка получится быстрее, чем на нативном языке. Если встретится что-то сложнее, или у вас с дизайнером взаимная ненависть, то придётся изучать хитрости и особенности React Native, подстраиваться под частные случаи и стили на конкретных платформах, что заставит потратить дополнительное время, которое, в зависимости от опыта может быть значительным.

Ответить