Меня зовут Александр Чернов, я фронтенд-разработчик в KODE и я использую React Native в разработке мобильных приложений уже более семи лет. Сейчас расскажу вам, как мы у нативных разработчиков хлеб отбирали.
Однажды к нам пришел заказчик с MVP от другой команды. Это были iOS и Android-приложение. iOS-приложение было написано на Swift, сверстано в сториборде, с архитектурой MVC (Massive Model View Controller). Android-приложение было написано на Kotlin, сверстано в XML. Архитектура отсутствовала — только два слоя, data и UI. Причем Activity или Fragment из UI-слоя содержали в себе всю бизнес-логику, которая зачастую дублировалась.
При ревью кодовой базы мы поняли, что в дальнейшем поддерживать MVP и добавлять новую функциональность будет больно. Поэтому решили основательно доработать продукт, чтобы при масштабировании бизнеса и внедрении новых фичей заказчику не пришлось переписывать код.
Для реализации мы рассматривали такие варианты:
-
Поддерживать нативное приложение, отрефакторить его и в дальнейшем дорабатывать на Kotlin/Swift. Но в таком случае бюджет проекта увеличился бы, поскольку заказчику нужно было бы задействовать iOS и Android-разработчиков.
-
Остановить поддержку Kotlin/Swift и разрабатывать приложение на React Native с нуля. Для заказчика этот вариант не подходил, так как для стартапа было важно быстрее получить готовый функционирующий продукт и начать разработку новых фич.
-
Поддерживать нативное приложение на Kotlin/Swift и постепенно переписать на React Native.
Мы обсудили перспективы с заказчиком и остановились на последнем варианте — плавном переезде на RN. У него было сильное преимущество: ранее на другом проекте мы уже опробовали React Native с этим заказчиком, и он остался доволен результатом. К тому же, у нас была уже собрана команда и наработана база, поэтому заказчику не пришлось заново проходить эти процессы и увеличивать стоимость проекта.
Как мы «переезжали»
-
Потушили пожары
Сначала мы с минимальными усилиями сделали ребрендинг приложения, скрыли лишний функционал и исправили множество багов. Например, столкнулись с проблемой несогласованных моделей данных между клиентом и сервером. Также в приложении на Android отсутствовал некоторый функционал, который был на iOS — например, Google reCAPTCHA.
В общем, мы провели минимальный рефакторинг и выпустили приложение в TestFlight, чтобы бизнес мог с ним работать и параллельно проходить проверку в сторах.
-
Начали внедрять React Native
У React Native есть все необходимые инструменты и подробная документация, чтобы внедрять его в уже существующие нативные приложения.
Минимальная сущность, в которую рендерится RN-приложение для iOS — View, а для Android — Fragment. Зная это, я написал набор утилит, которые позволяют буквально в пару строчек запустить React-приложение. Получилось довольно лаконично, достаточно передать имя начального роута.
Android:
class RNDepositsFragment: RNFragment ( options = object : RNFragmentOptions () { override fun getInitialRoute(): String { return "deposits" } } )
iOS:
class RNDepositsViewController : RNViewController { override var options: RNViewControllerOptions { return RNDepositsOptions() } } struct RNDepositsOptions : RNViewControllerOptions { func getInitialRoute() -> String { return "deposits" } }
Если возникает необходимость передать дополнительные данные, с этим также не возникает проблем — в JS-слое регистрируем компонент App,
AppRegistry.registerComponent('appName', () => App);
который принимает переданные из нативного слоя свойства.
const App = ({ authToken, baseUrl, initialScreenName, …other }: AppInitialProps) => { … }
Для управления нативной навигацией по экранам, а также передачи данных между RN-представлением и нативным слоем был написан Native Module, который мы назвали AppBridge (см. схему ниже).
Пример AppBridge-модуля для Android
class AppBridgeModule( private val reactContext: ReactApplicationContext ) : ReactContextBaseJavaModule(reactContext) { private val eventEmitter = AppBridgeEventEmitter(reactContext) override fun getName(): String = MODULE_NAME @ReactMethod fun logout() { ... } // пример авторизации нативной части приложения // React-приложение отправляет токен в натив @ReactMethod fun login(token: String, promise: Promise) { ... } // пример главного экрана @ReactMethod fun openHomeScreen() { val intent = Intent(reactContext, HomeActivity::class.java) intent.addFlags( Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK ) reactContext.currentActivity?.startActivity(intent) } // Отправка ивентов с пейлоадом в RNFragment // Например в RN представлении пользователь // нажимает кнопку "назад", // но нам нужно выполнить не навигационный переход внутри RN-приложения, // а закрыть целиком Activity @ReactMethod fun send(eventName: String, payload: ReadableMap?) { UiThreadUtil.runOnUiThread { // RN - это проектное решение, singleton-объект // для доступа к React-native представлениям RN.shared.onEvent(eventName, payload); } } }
Визуально это выглядит так. Нативный слой доминирует и занимает всю площадь экрана, нижний таббар тоже нативный. Но контент внутри таба — это React-приложение со своими моделями данных, навигацией, сетевым слоем и так далее.
Напомню, что в Android основной экран с таб-навигацией — Activity, а каждый таб — Fragment. Мне достаточно было добавить новый или заменить существующий на RN фрагмент, чтобы при переходе на таб запускалось RN-приложение с нужным флоу. Очень просто. В iOS тоже самое: UITabBarController для каждого таба рендерит UIViewController, мы также легко можем добавлять новые или изменять текущие контроллеры на RNViewController (см. код выше).
Как это работает под капотом: нативный слой запускает React Native-представление, которое запускает React-приложение с начальными параметрами, такими как токен авторизации, ссылка на API, имя роута и другие.
Как видно из схемы, React-приложение может асинхронно обмениваться данными с нативным слоем и непосредственно с RN-представлением в обе стороны через AppBridge (Native Module).
Также можно заметить, что все RN-представления открывают одно и то же React-приложение, просто с разными входящими параметрами. Слой React Native намеренно сделан моноприложением, так как конечной целью было оставить одну точку входа. Но ничто не мешает организовать и микроприложения, чтобы у каждого из них была своя песочница и не было общего контекста. А RN Utils — это набор самописных утилит для работы с RN, туда входят классы RNFragment, RNViewController и другие.
-
Мигрировали на React Native
Следующим шагом была замена флоу авторизации/регистрации. Теперь React Native получает авторизационный токен, сохраняет его, рефрешит и передает в нативный слой. Также управляет биометрией, логаутом и т.д.
А еще мы добавили CodePush — киллер-фичу React Native, которая позволяет обновлять мобильное приложение без прохождения проверок в сторах.
Постепенно заменили все табы и отдельные Activity/UIViewController на RN-представления. Но основная навигация по приложению — неавторизованная зона, флоу авторизации, основной экран с табами и другие экраны, до сих пор осуществляется в нативном слое.
Наконец, остался последний нативный раздел. Прежде чем переделывать его, мы доработали приложение так, чтобы React Native начал доминировать. Получилось полноценное RN-приложение, запущенное в одном Activity/UIViewController, в котором вся навигация осуществляется через react-navigation. Оставшийся раздел я завернул в Native UI Component (документация Android, документация iOS) и использую его в React-приложении.
Финальным шагом мы переделали последний нативный раздел на RN и выпилили оставшийся мусор. Теперь это полностью React Native приложение, как будто мы написали его с нуля.
Преимущества и недостатки подхода
React Native — крутая и зрелая технология, которая хорошо себя проявляет. Забегая вперед, скажу, что при ее внедрении в существующее приложение я не встретил непреодолимых проблем, а процесс был подробно описан в документации.
Рассмотрим преимущества и недостатки нашего подхода.
Преимущества
-
Нон-стоп. Мы бесшовно переписали приложение, улучшили пользовательский опыт, кодовую базу и поддерживаемость продукта, и при всем этом не тормозили бизнес.
-
Снижение стоимости разработки. При переходе на кроссплатформу над проектом работает одна команда вместо двух, то есть вместо iOS и Android — React Native.
-
Увеличение скорости разработки. Инструменты для React Native очень развиты и позволяют быстро разрабатывать приложения. В Storybook можно сверстать приложение практически полностью и посмотреть, как оно будет выглядеть. Плюс hot-reload — мы пишем код, он сразу же обновляется и мы видим результат. Тот же самый CodePush — очень крутая вещь.
Недостатки
-
Проблемы, с которыми мы бы не столкнулись при обычной разработке. Например, работа с диплинками или кнопкой «Назад» на Android. Когда на экране два слоя — нативное приложение и React Native, они оба начинают обрабатывать эти действия, происходят некоторые конфликты и приходится тратить время на их решение.
-
Высокий порог входа для React Native-разработчика. Помимо React Native, ему нужно неплохо разбираться в нативных платформах, писать нативный код и в целом понимать, как все работает.
P.S. Возможно, вы хотите спросить, где исходники RNViewController, RNFragment и RN Utils. Это были временные, узкоспециализированные решения, которые помогли нам переехать на RN, а после переезда потеряли ценность и были удалены из проекта. Поэтому исходники мы показать не можем.
Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/865962/
Добавить комментарий