Чому Codegen робить підхід «спочатку специфікація» обов’язковим для React Native TurboModules
Дізнайтеся, як React Native Codegen перетворює специфікацію TurboModule TypeScript на контракт часу збірки, що погіршується, якщо його пропустити, та де правило більше не застосовується.
Джерела: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It
Багато команд, які підтримують бібліотеки React Native, включають у свої посібники з участі правило: перед тим, як торкатися будь-якого нативного коду, необхідно змінити специфікацію TurboModule. Це схоже на стилістичну вимогу — таку конвенцію сподіваєшся, що учасники будуть дотримуватися під тиском часу. У новій архітектурі це зовсім не так. Codegen генерує нативний міст відповідно до специфікації, тож сам процес збірки визначає порядок виконання завдань. У цій статті пояснюється, як дотримується цей контракт, що насправді відбувається на iOS та Android, коли хтось намагається скористатися швидким шляхом, та які типи модулів не підпадають під ці правила.
Конвенція, яка виявляється обмеженням
Уявіть собі адміністратора, який вирішує заощадити час: спочатку додає вбудований метод, а потім повертається, щоб оновити специфікацію. У модулі, підтримуваному Codegen, такий план зазнає невдачі ще до того, як може принести якусь користь. Не потрібно, щоб хтось із перевіряючих чи лінтерів заперечував – це роблять компілятор та генерований код. Важливою є наступна думка: це правило ніколи не було командною звичкою, яку випадково толерує інструментальний комплекс. Це властивість самого інструментального комплексу, яку команда випадково записала.
Різниця полягає не лише у формулюванні. Переваги можна пожертвувати заради кінцевого терміну. Контракт, який діє під час збирання, так робити не дозволяє, і усвідомлення того, з яким саме контрактом ви маєте справу, змінює спосіб планування змін у бібліотеці.
Файл специфікації в центрі TurboModule
Кожен новий TurboModule архітектури має один файл TypeScript, зазвичай src/Native<Name>.ts, у якому оголошується кожен метод, що переходить межу між JavaScript та нативним кодом. Обгортка навколо SDK для соціальних входів може мати назву src/NativeNaverLogin.ts; обгортка для відгуків у магазині додатків — src/NativeInAppReview.ts. Запис codegenConfig у файлі package.json пакета вказує Codegen, де знаходити цей файл та який тип продукту створювати на його основі.
Як правило, порядок є простим: будь-який метод, який JavaScript має можливість викликати, спочатку оголошується у специфікації, а лише потім реалізується у нативному коді. Спочатку специфікація, потім нативний код — ніколи навпаки. Те, що робить цей порядок обов’язковим, — це механізм, який обробляє специфікацію.
Як Codegen перетворює специфікацію на місцеві обов’язки
Codegen — це процес трансформації, який виконується під час збирання проекту. Він аналізує специфікацію TypeScript та створює інтерфейс для кожної платформи:
- Android: абстрактний клас, наприклад
NativeNaverLoginSpec. Ваш модуль на Kotlin або Java успадковує цей згенерований клас, а не безпосередньоReactContextBaseJavaModule, і кожен метод у специфікації стає абстрактним методом, який необхідно реалізувати. - iOS: інструмент JSI для Objective-C++, що включає одиницю перекладу
NativeNaverLoginSpecJSIта протокол, якому має відповідати ваша реалізація у форматі.mm.
Ключовою деталлю є напрямок залежності. Нативний код не описує міст; він успадковує міст, створений на основі специфікації. JavaScript, у свою чергу, може отримати доступ до нативного коду лише через цей створений інтерфейс. Саме цей факт визначає те, що відбувається у кожному з двох варіантів, коли сторони можуть розходитися.
Метод нативного коду, який не оголошений у специфікації
Якщо ви додаєте метод у нативній частині без його додавання до специфікації, Codegen нічого для нього не створює. У Android немає абстрактного методу для перевизначення, а у iOS — запису протоколу.
У Android найпоширенішим способом створення такого методу є використання ключового слова override, але це призводить до помилки компіляції, оскільки у створеному батьківському класі немає відповідного елемента. Навіть якщо прибрати цей модифікатор та файл буде скомпільований, метод все одно не є частиною створеного інтерфейсу, тому JavaScript не має до нього доступу.
У iOS ця проблема проявляється менш помітно. Метод існує у вашому файлі .mm та компілюється без проблем, але він не входить до протоколу, який використовується движком для доступу до методів, тому JavaScript ніколи не зможе до нього дістатися. Метод, який працює без звукових сигналів про помилку, можливо, є гіршим за помилку компіляції: ви навіть не дізнаєтесь про його несправність, поки хтось не спробує скористатися ним з JS та не виявить його відсутність.
Метод специфікації без нативної реалізації
Протилежний сценарій також призводить до проблем. Якщо специфікація оголошує метод, але нативна частина його не реалізує, Codegen все одно створює для нього міст, і JavaScript-проксі з радістю робить цей метод доступним.
У Android компілятор Kotlin або Java відхиляє клас, який розширює створений абстрактний специфікатор, не реалізувавши жоден із його абстрактних методів, що є найкращим варіантом, оскільки помилка виявляється на ранньому етапі. У iOS відсутній обов’язковий метод протоколу зазвичай призводить лише до попередження компілятора; справжня помилка виникає під час виконання у вигляді збою через невизнаний селектор, коли JavaScript намагається викликати цей метод.
У будь-якому разі обидві частини з’єднуються під час генерації коду. Ви не можете надати JavaScript нової функціональності, змінивши лише одну з частин. Вас не стримує коментар про перевірку, а сама система типів та створений „клей“, які не дозволяють елементи збігатися.
Чому це правило застосовується до кожної бібліотеки Codegen
Якби підхід „спочатку специфікація“ був лише стилем певної команди, він діяв би лише у репозиторіях, чиї адміністратори його обрали. Але контроль здійснюється за допомогою Codegen, а Codegen належить до React Native. Кожна бібліотека, яка дозволяє використовувати Codegen за допомогою блоку codegenConfig у режимі New Architecture, підпорядковується цим же правилам, незалежно від того, чи думали її автори коли-небудь про принцип „спочатку специфікація“ чи ні.
Офіційна документація React Native відображає це у своїх формулюваннях. Описуючи папку з результатами, яку створює Codegen, вона представляє згенеровану абстрактну класу як щось, що має бути реалізовано модулем Turbo Native. Слово „має бути“ є ключовим: це обов’язок, який перевіряється під час компіляції коду, а не рекомендація. Покрокові інструкції від спільноти описують таку саму поведінку ззовні: як тільки налаштовується codegenConfig, з’являється створений файл Native...Spec, названий відповідно до того, що вказано в codegenConfig у файлі package.json. Різні автори, різні репозиторії — ідентичні вимоги.
Існує також корисне підтвердження з протилежного боку. Якщо повністю виключити поле codegenConfig з файлу package.json, запуск процедури CocoaPods install на iOS виявить цю прогалину:
[Codegen] The "codegenConfig" field is not defined in package.json
Codegen тоді повністю ігнорує цей пакет. Конвеєр розроблений так, щоб зупинитися, а не намагатися знайти альтернативний спосіб виконання. Специфікація — це не документ, який може стати застарілим; це вхідні дані, навколо яких організований весь процес генерації.
У сукупності підхід «спочатку специфікація» є структурним наслідком того, як конвеєр обробляє свої вхідні дані. Бібліотеки дотримуються цього принципу, оскільки вони будуються за зразком Codegen. Бібліотека, яка, здається, його ігнорує, або все ще використовує стару архітектуру, або взагалі не налаштувала codegenConfig.
Де припиняє діяти контракт
Назвати це контрактом інструментального комплексу — точно, але легко перебільшити цей твердження. Корисно чітко визначити межі:
- Модулі моста старої версії. Це модулі, створені на основі моста до JSI з використанням
RCTBridgeModuleу iOS абоReactContextBaseJavaModuleу Android, без відповідних специфікацій; вони існували ще до появи Codegen. У них немає генерованого джерела істини, тому жодні механізми не забезпечують дотримання певної послідовності. Тут лише самодисципліна може допомогти. - Пакети без
codegenConfig. Без цього блоку не генеруються жодні результати, і ланцюжок від специфікації до виконання у нативному форматі так і не починається.
type: „components“, також проходять обробку через Codegen, тому цілком логічно очікувати схожої дисципліни. Однак вищенаведене обґрунтування ґрунтується на Turbo Native Modules (type: „modules“), а генеровані результати для компонентів відрізняються, тому слід розглядати ситуацію з компонентами як ймовірну, а не як уже усталену, та перевіряти її за поточною документацією вашої версії React Native.Твердження, яке залишається непохитним, є обмеженим: для New Architecture TurboModules, генерованих Codegen зі специфікації JavaScript або TypeScript — що є офіційно задокументованим способом їх створення — принцип „спочатку специфікація“ забезпечується процесом будування.
Варто також зазначити, що TurboModules на основі Codegen — це не єдиний спосіб створення нативних модулів сьогодні. Альтернативи, такі як Nitro Modules, використовують власну модель генерації коду та прив’язки; стаття на тему того, як статичні прив’язки Nitro порівнюються з TurboModules, розглядає ці переваги та недоліки.
Робочий процес, який фактично застосовується під час збирання
Як тільки механізм стає зрозумілим, порядок виконання завдань більше не є справою суб’єктивного оцінювання. Додавання методу до TurboModule відбувається за певною схемою:
- Змініть специфікацію у файлі
src/Native<Name>.ts, щоб оголосити новий метод та його типи. - Запустіть Codegen: команда
pod installгенерує код для iOS, а синхронізація чи збірка через Gradle — код для Android.
З цього випливають кілька практичних звичок:
- Переглядайте зміни специфікації разом із змінами API. Специфікація фактично є публічним нативним контрактом бібліотеки, тому зміни до неї потребують такої ж уваги, як і зміни експортованих JavaScript-типів.
- Поново генеруйте код перед дебагуванням. Коли здається, що нативного методу немає, поширеною причиною є застарілий код, створений під час пропущеного кроку
pod installабо синхронізації Gradle. - Запускайте будови для обох платформ у CI. Оскільки iOS може збиватися без жодних сигналів, тоді як Android видає чіткі помилки, наявність успішної будови лише для Android не свідчить про те, що місток між платформами створений повністю.
Підсумок
Знання того, хто „володіє“ правилом пріоритету специфікації, — це не педантизм. Як тільки ви усвідомлюєте, що це механічний процес, ви перестаєте розглядати його як пораду щодо вирішення конфлікту з термінами. Не існує варіанту „лише цього разу“ — спочатку додати нативний метод, а потім виправити специфікацію, оскільки такий швидкий шлях не призводить до функціонального коду.
- Codegen генерує нативний міст від специфікації TypeScript, тож нативний код реалізує специфікацію, а не визначає цей міст.
- Відхилення в будь-якому напрямку призводить до проблем: іноді під час компіляції, іноді безпосередньо в iOS, іноді у вигляді збою під час виконання.
- Контракт охоплює TurboModules, підтримувані Codegen; старі модулі-мости та пакети без
codegenConfigзалишаються поза ним, а компоненти Fabric потребують окремої перевірки.