Чаще всего при выборе этого языка ожидается, что разработка одного приложения под две платформы займёт в два раза меньше времени, чем разработка двух приложений. Но по итогу оказывается, что разработка занимает столько же, если не больше, из-за сложностей, скрытых под внешним блеском и маркетингом. Мы расскажем о некоторых подобных сложностях, с которыми нам пришлось столкнуться за последние несколько месяцев работы с React Native.
React Native адаптирует Javascript под разработку для мобильных устройств. Это достигается тем, что для сборки проектов он использует несколько сборщиков — Metro Bundler, который интерпретирует JS-код и представляет ресурсы и сборщик целевой системы. В нашем случае это был gradle для Аndroid. В теории приложение 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.
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. При ошибке отладчик показывает красный "экран смерти" с её описанием, более-менее подталкивающим в сторону пути исправления. Но и с этой частью есть проблемы.
Хорошо, когда ошибка выглядит так:
Здесь действительно понятно, что происходит — вместо ожидаемого объекта массива в переменной this.state.noteArray.map находится undefined, из-за чего возникает пресловутая TypeError. Исправить её можно переходом на app.js:14 и проверкой значения в данной переменной перед использованием.
Хуже, когда ошибка выглядит так:
Так:
Или так:
Изображения взяты из интернета, но мы видели их и "вживую". И несмотря на то, что они показываются в 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 не присылает соответствующих ошибок) удалось выловить логи падения, которые выглядели так:
Этот текст даже не относится к нашему приложению, он даже не был отмечен красным. Но после того, как мы его получили и отправились на форумы, мы обнаружили, что новая версия 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 выгоден? После того как мы завершили наш третий проект на этом языке, мы решили, что нет. Как вы считаете?