Using React Native as a platform [ukr]
Доповідь від партнера Avenga.
Розповім про наш досвід неочевидного використання React Native, що з цього вийшло і чому це настільки сподобалося нашим клієнтам та користувачам.
- Engineering Manager в Avenga
- Займається розробкою з 2008
- Обожнює писати під веб, але останні 5 років доводиться писати React Native
- Спікер СoreСamp Сonference, та учасник подкастів Techревені
- Вважає відеоігри невід'ємною складовою розвитку розробника, надає перевагу стратегіям та симуляторам
Транскрипція доповіді
Дякую величезне. Я нарешті зрозумів, чому була така велика пауза між моєю доповіддю і Сергієм. Це для того, щоб Сергій міг побільше розкрити свогє бачення. Незважаючи на те, що я РМ, більшу частину часу я працюю як техлід на проекті і керую технологією React Native. Тому я буду білою вороною, що розповідає про React Native на конференції з React. Розкажіть, будь ласка, підніміть руки, хто з вас знає, що таке React Native та писав на ньому. Ого, це аналіз. Тоді підніміть руки, кому сподобалося і хто ще пише на ньому досі. А, так, якщо не сподобалося, мовчіть. Я буду розповідати про наш власний досвід розробки.
І у нас є великий корпоративний проект - це CRM-система для фармацевтичних компаній. Ця CRM-система працює і має можливість функціонувати на мобільних пристроях, комп'ютерах та планшетах. Вона також підтримується на всіх платформах, включаючи веб. У CRM-системі є 6 нативних додатків, і її використовують понад 340 компаній у 60 різних країнах світу. Кількість активних користувачів перевищує 100 тисяч, а на деяких пікових ситуаціях у деяких компаній досягає 24 тисячі користувачів, тоді як у інших - всього 300.
У контексті нашої презентації я буду говорити більше про планшети та iOS, оскільки це є для нас пріоритетною платформою, і більшість наших користувачів використовують саме цю платформу. Але також важливо зазначити, що у нас є підтримка веб-версії, Android і десктопних додатків для macOS і Windows. У CRM-системі кожна компанія може налаштувати свій власний інтерфейс на головній сторінці відповідно до користувача, його ролі або профілю. Це можна кастомізувати будь-яким чином. Крім того, кастомізація доступна також на веб-версії.
Ми також маємо сторінки, які можуть бути додані до веб-каналу, такі як сторінка трійки дзвінків контакту або акаунта. Там є також дашборд, і на ньому можна налаштувати велику кількість різноманітних віджетів. Кількість віджетів насправді дуже велика, я не рахував їх точну кількість, але їх сотні. Всі компанії люблять нові функції. Кожна компанія хоче особливий підхід. І коли вони приходять із запитанням про нові функції, вони говорять: "Нам потрібно це для нашої країни, або ми хочемо додати ще одну колонку для відображення певного показника, чи можна додати можливість швидко попасти і прочитати цей документ або статистику. Ми потребуємо підпис тут, тому що в нашій країні це звичайно". Кожна нова функція вимагала багато місяців реалізації.
Кожна функція мала свою пріоритетну платформу. Для нас пріоритетніше було розробляти для iPad, тому що більшість користувачів використовують саме його. Веб-версія отримувала пріоритет через її простоту. Щодо Android і десктопів (macOS і Windows), вони отримували менший пріоритет. Деякі функції так і залишалися в очікуванні в беклогу протягом місяців або навіть років, особливо якщо користувач чи його роль не мали великого пріоритету. Є троє непріоритетних користувачів, але вони задовольняються тим, що є. Було важко конфігурувати кожен віджет. Коли ви відкриваєте конфігурацію віджета, вона може виглядати максимально просто на UI, але в ній може бути безліч прапорців та полів для заповнення, що може суттєво впливати на конфігурацію. Виникає поняття "support ticket", коли ви взаємодієте з користувачами. Іноді виникає поняття "misconfig" (помилкова конфігурація), яке можна помітити в конфігурації.
Наша невелика команда розробників React Native великої платформи мала за мету усунути обмеження для розробників, які могли створювати лише обмежену кількість віджетів для всієї платформи. Ми хотіли дати можливість клієнтам створювати свої власні віджети, якщо вони хочуть і мають можливість, оскільки у нас багато клієнтів і вони мають своїх розробників. Але не було інструменту, який дозволяв би це робити самостійно. Ми також хотіли дати можливість переписувати існуючий функціонал. Ми прагнули випустити продукт одночасно на всіх платформах, а не лише на пріоритетних. Ті, хто розробляв мобільні додатки, знають, що Android виходить пізніше, ніж iOS, через рецензію. Ми також хотіли зберегти мобільний UI та UX, оскільки мобільні додатки побудовані на принципі "offline first", тобто користувач отримує всі дані на свій пристрій під час входу.
І після цього ми вже синхронізуємо якісь маленькі частинки, які були оновлені. По суті, в пристрої існує мобільна база даних, і ви можете увімкнути light flight mode і користуватися пристроєм. Потім ви можете працювати цілий день, підключитися до Wi-Fi, синхронізувати, і всі дані потраплять в онлайн. І ось тут в нас виникає React Native. Коли ми говоримо про React Native, зазвичай всі думають про окремий мобільний додаток, написаний на React Native повністю. Існують випадки, коли малюється весь екран, випадки, коли лише частина екрану або віджет. Також Microsoft часто використовує React Native, включаючи його в останню версію Windows у меню "Пуск". Проте загалом сутність його залишається однаковою. Це логіка JavaScript, упакована в архів, яка є основою нашого додатку. І, відповідно, проходить увесь процес рев'ю разом з внутрішніми натисками.
Те, що ми хотіли побудувати, це винести JavaScript логіку на хмару. Дати можливість завантажувати тільки потрібний віджет потрібному користувачеві. Всі бінарні файли, звісно, проходять процес рев'ю та тестування. Таким чином виникла наша мобільна оболонка. Насправді це обгортка. Вона має основний функціонал, але також включає частини нашого коду, які дозволяють нам працювати з JavaScript. У нас є адмінка, через яку ми конфігуруємо все це. Також маємо безліч JavaScript бандлів, які зберігаються на хмарі. Тепер кожен може зайти на свій профайл, натискати "Редагувати лейаут", знаходити потрібний віджет, неважливо, на якій мові він написаний - на нативній частині, Mainstream чи React Native, - і додавати його за допомогою "drag-and-drop".
Як результат, у нас зараз є три фреймворки з однаковим інтерфейсом, написані на трьох різних мовах. Вони працюють на нативному рівні на iOS та Android, а також на вебі; розробляється версія для Desktop. Що включає в себе цей фреймворк? По-перше, це нативні залежності. Нативні залежності обов'язково повинні бути додані до фреймворку. Ми не можемо швидко, динамічно додавати чи видаляти пакети, як це робить Expo. Тому ми дуже уважно додаємо кожен нативний пакет, перевіряючи, яку саме проблему він вирішує і чи можна обійтися без нього.
Навігацію ми вирішили виконати за допомогою React Navigation. Це має певні власні нативні залежності, які також запаковані. Для графіків ми вибрали React Native Victory, оскільки він легко портується на веб. Він також має інший пакет, що називається просто Victory, але всі компоненти, інтерфейси і API однакові. Це був легкий вибір для нас. Також додали декілька пакетів для віджетів, таких як нативні елементи управління, іконки і так далі. Проте цього недостатньо. Тут на допомогу приходять кастомні мости. Краще React Native дозволяє нам писати кастомні мости, і в нас їх написано 19, але для основного функціоналу нам потрібно всього 4.
Перший з них - це місто для бази даних. Оскільки у нас є мобільна база даних на пристрої, ми хочемо зберегти цей підхід "offline first". Ми повинні дозволити будь-якому віджету чи будь-якій частині функціоналу взаємодіяти напряму з базою даних, отримувати дані з неї та записувати в неї. Наші розробники і команди сортпати, які створюють віджети для своїх компаній, можуть писати запити. Запит виглядає приблизно так: ми вибираємо дані з певної таблиці, де id = 1. Потім ми подаємо це на обробку лексеру або токенізатору, де всі рядки розбиваються на токени - для машини, не для людини. Потім у нас є граматичний аналізатор, який перевіряє, чи правильно розташовані токени, чи вони відповідають певним правилам. Для цього ми використовуємо Libu Antler, яке дозволяє робити це досить швидко та ефективно.
Після цього ми будуємо синтаксичне дерево і передаємо його в наш візитер, який застосовує взаємодію. Оскільки мобільна база даних містить набагато більше полів і даних, ніж дозволяється читати, записувати і так далі, кожен директор, який має візитера, також має візитера, який має візитера. Цей візитер може звертатися до мобільної бази даних і взаємодіяти з нею. Проте виникають проблеми, коли два візитера на одному екрані використовують одні й ті ж дані. Якщо один записав їх, інший не відображає ці зміни. Тут нам потрібен інший міст, який дозволяє нам відстежувати ці зміни - Data Updates Bridge. Він побудований на Native Event Emitter.
Ми використовуємо ті ж самі запити, розпарсюємо їх, валідуємо і перевіряємо, але витягуємо з них лише таблицю чи об'єкт, який ми хочемо прочитати. Це записується в загальний об'єкт, який підписується на подію зміни в мобільній базі даних. Якщо щось змінилося, спрацьовує подія, і ми повертаємо це віджету. Віджет отримує лише таблицю, яка змінилася. Далі ми переходимо до навігації. Навігація повинна бути максимально мобільною для досягнення кращого користувацького досвіду (UX). У нас є два типи навігації. Перший - коли віджет з React Native хоче щось відкрити в нативній частині. У нас є метод, який має об'єкт параметрів, entity (сторінка, яку відкрити), ID, mod для виклику певної дії та presentation mod.
Presentation mod дозволяє змінювати поведінку віджета. Коли ми додаємо новий екран, ми можемо додати його в чергу, що залишає ідентифікатор екрана, на якому був віджет. Потім новий екран з'являється з кнопкою "назад", і ми можемо повертатися на попередній екран або перейти на новий. Presentation mod може призводити до цікавих ситуацій, які ми розглянемо трошки пізніше. Далі йдуть внутрішні параметри. Оскільки ми хочемо, щоб нативна частина також могла викликати віджети з певними вхідними параметрами, ми маємо два способи це робити.
Перше - це нативний метод, що дозволяє відкривати віджет з певними даними - DeepLink. Це щось схоже на URL, але для мобільного пристрою. Ми також маємо інтерфейс для веб-браузерних лінок. Коли віджет запускається, ми можемо отримати параметри через ініційні параметри в кореневому компоненті, який реєструється як наш додаток. Розробник вирішує, як обробити ці параметри - передати їх в контекст, зберегти в стор, чи деінде.
Але я розповім декілька цікавих кейсів, які в нас виникали. По-перше, нам пощастило, що у клієнтів було яке-то бачення того, як вони уявляють цю дизайн-систему. Вони вже подали нам деякі контролі, конкретні кольори, і ми почали розробляти. Дизайн-система розробляється, і її основа – це певна тема. Ми використали провайдер ColdStack, оскільки він максимально простий, примітивний і задовольняє всі наші потреби. Ми створили дуже просту тему, де спочатку зберігали тільки три поля. Зараз вона набагато більше, але ми почали з літери DarkModa, оскільки у нас є підтримка DarkModa, певні шрифти і колірна схема. Щодо шрифтів, ми визначали платформу, на якій ми зараз, і використовували шрифти, які вже використовуються пристроєм. З колірною схемою було просто, оскільки є дизайн-система і бачення клієнта, і ми починаємо обирати потрібні нам кольори.
Тут виникають перші проблеми, оскільки це PrimaryColor на iOS, PrimaryColor на Android, а це на вебі, і це те, що хоче клієнт. Ми приводимо це до клієнта, показуємо, що це незручно, добре, що у нас є тема, і ми можемо її пере використовувати. Добре, йдемо далі. З'являється типографія. У типографії ми визначили перші п'ять компонентів, які нам потрібні на всіх платформах, і почали будувати компоненти на їхній основі. При розробці компонентів у типографії ми спочатку не хотіли мати стилі в компонентах, які насправді будують цей тип. Експортування об'єкта теж не було дуже круто, оскільки виникали проблеми, коли було невідомо, куди додавати додатковий параметр до одного з класів, якщо ми його додаємо в основний об'єкт, то яким ім'ям ми його називаємо, якщо ми називаємо його action, і через три місяці з'являється новий компонент, який виглядає так само, але робить не action, а щось інше. Це дуже важливо.
Після цього ми побудували простий компонент StyleText, який використовує Color, Font, FontFamily, і також має підтримку R2L. Після цього у нас був базовий компонент, який ми могли пере використовувати. Усі стилі, пов'язані з типографією, були додані до теми і додані по тим же п'яти ключах, які були в задачах. Це текст, підпис, заголовок, певний заповнювач. Таким чином, ми отримали п'ять компонентів, які відповідали потрібним діям. Тепер, коли комусь потрібно використати який-небудь текст, і він трошки відрізняється, людина може взяти компонент і побудувати свій власний, заснований на наших примітивах, але додавання нових стилів в тему або в існуючі компоненти не відбувається.
Маємо багато форм, і найчастіше використовуваний у нас компонент – це введення даних. При розробці компонента введення ми йшли від верху до низу відповідно до потреб, оскільки потреби поступово зростали. Спочатку у нас був текстовий ввід з заповнювачем, окрім звичайних властивостей, таких як значення, зміна значення і так далі. Потім у нас з'явилася мітка. Ми також додали мітку до нашого компонента. Однак коли ми зробили її обов'язковою, і вона отримала зірочку, ми відчули, що ми йдемо не в тому напрямку. Добре, відміняємо, додаємо мітку і даємо їй можливість передавати все, що потрібно користувачу. Йдемо далі. З'являється іконка. Окей, з'являється іконка. З'являється потреба натискати на іконку. Так. Ми знову там, де ми вже були. Відміняємо це, додаємо можливість передавати все, що потрібно користувачу, оскільки ми насправді не знаємо їхніх потреб. Те саме з правою іконкою.
Отже, ось приблизний шейп нашого компонента. Проте насправді могло б бути написано краще. Вже зараз, готуючись до доповіді, я переглядав це і подумав, ага, насправді лейба заважає, вона не повинна бути в компоненті. Її треба винести. Тобто слід зберігати її як окремий компонент, який може бути використаний або не використаний. Чому? Тому що існують випадки, коли вам потрібно обгорнути ваш ввід і передати, наприклад, введені дані, для обчислень, або отримати реакцію з вашого обгортки, або ви хочете змінити його відступи. Тому, якщо ви створюєте дизайн-систему, не лінуйтеся. Зараз у нашій дизайн-системі приблизно 75 компонентів, з яких 50 - базові компоненти, такі як введення даних. 95% з них написані на React Native, і 95% коду. І деякі з них, наприклад, Date Picker, інші платформено-специфічні компоненти, фактично мають різний код, але експортуються під одним інтерфейсом. Коли ми побудували нашу дизайн-систему, ми були приємно вражені тим, що нам слідкувати за потрібними ролями лише ми, а не всі платформи. І ми знову звернулися, як і з кольорами, до нашого клієнта, і кажемо: "От тут не співпадають радіуси кутів, а тут не співпадають відступи". І завдяки нашій темі ми можемо дуже просто побудувати потрібну тему (вибачте за тавтологію) і використовувати її на потрібній платформі, яку ми хочемо.
Недоліки всього нашого рішення полягають в тому, що немає такої легкої можливості додавання нативних пакетів. Тобто сторонні розробники або люди, які розробляють віджити, майже не впливають на той процес, які нативні пакети будуть додані до основного фреймворка. Складність також є великим пейном, але на великих проектах немає простих рішень. Тому ми все це підтримуємо, всі 19 бриджів, ми підтримуємо всі міграції React Native, ми підтримуємо нашу дизайн-систему і дивимося, щоб забезпечити максимальну зворотну сумісність. Складність для розробників, які приходять або до нас розробляти, або до клієнта. Тому що є велика система API, яку їм потрібно вивчити і зрозуміти, як використовувати. Є велика кількість компонентів, які, можливо, частково дублюють свій функціонал або перекривають його, але ми обирали гнучкість над консистентністю.
Так як наш основний момент був надати клієнтам можливість робити все, що вони вважають за потрібне для свого бізнесу. Ще один недолік, який я вирішив висвітлити окремо, це витік пам'яті. Мені, як колишньому веб-розробнику, витік пам'яті був щось далеке і страшне, доки не з'явився React Native. Витік пам'яті може бути трьох видів. По-перше, це наші витоки пам'яті. Ми щось там написали на нативній частині, і вони не так цікаві в контексті цієї доповіді. Ми зробили це, виправили це, вивчили це. Але цікавіше, коли витік пам'яті в сторонніх компонентах, бібліотеках і React Native для нас насправді є такою ж сторонньою. І, на мою думку, найцікавіше - це клієнтські витоки пам'яті. Витік пам'яті в React Native. React Native за своєю структурою не має жодного об'єкта, структури, який би мав велику кількість різних екземплярів.
Коли ми заходимо на сторінку аккаунта і відкриваємо віджет, у нас з'являється на дашборді віджет, який працює на React Native. Після цього ми виходимо з аккаунта і заходимо на інший. Там також запускається той самий віджет, але це вже інший екземпляр React Native. Каплікухи. Меморі ліки виглядають приблизно так. Ми відкриваємо віджет, потім закриваємо його. Цей цикл може повторюватися, доки мобільний пристрій не вичерпає ресурси. React Native memory ліки стають особливо болючими, особливо при використанні різних фреймворків чи бібліотек, наприклад, React Native і React Native Screens. Насправді таких ліків не так вже й багато, але вони стають проблемою для нас. Виникають цікаві ситуації, коли memory ліки не призначені для конкретних областей. При відкритті сторінки з віджетом, React Native реєструє Root View в приватному об'єкті, написаному на Objective-C. Цей об'єкт недоступний з коду, і коли віджет закривається, React Native видаляє лише те, що він створив. Однак всі компоненти, які були приєднані до скрінів, залишаються в пам'яті, створюючи значний обсяг. Ми вирішили цю проблему, написавши код для коректної очистки цих ресурсів у React Native Screens і React Native Core. Після того, як відповідні виправлення були впроваджені в 73 версії, проблема memory ліків стала менш актуальною.
Додатково, є питання з клієнтськими memory ліками, які виникають при використанні режиму Push для відкривання сторінок нативно. Це призводить до memory ліків, створених самими клієнтами. В даному випадку можемо лише надавати документацію та рекомендації щодо коректного використання. Підсумок: Незважаючи на всі труднощі, наші клієнти активно використовують віджети, і ми продовжуємо підтримувати їх. Хоча ми стикаємося з викликами, наш підхід виявився успішним, і тепер це продається як важлива функція для наших клієнтів, які можуть власноруч налаштовувати та розширювати віджети під свої потреби.