Главная / Статьи

Почему Codegen делает подход, основанный на спецификации в первую очередь, обязательным для React Native TurboModules

Посмотрите, как React Native Codegen преобразует спецификацию TurboModule на TypeScript в контракт времени сборки, что ломается при её пропуске и где правило перестаёт действовать.

Издатель: REACTAPP.TOP8 мин чтения1819 слов

Источники: Spec First Isn’t a House Rule — the New Architecture Toolchain Enforces It

Многие команды, которые поддерживают библиотеки React Native, включают правило в свои руководства по участию в разработке: перед изменением любого нативного кода необходимо изменить спецификацию TurboModule. Это звучит как стилистическое предпочтение, такая конвенция, от соблюдения которой разработчики ожидают даже под давлением сроков. Однако в новой архитектуре всё совершенно иначе. Codegen генерирует нативный мост на основе спецификации, поэтому сам процесс сборки определяет порядок выполнения задач. В этой статье объясняется, как соблюдается это соглашение, что на самом деле происходит в iOS и Android, когда кто-то пытается сократить процесс, и какие типы модулей не подпадают под его действие.

Конвенция, которая оказывается ограничением

Представьте себе администратора, который решает сэкономить время: сначала добавляет метод на нативном уровне, а затем возвращается, чтобы обновить спецификацию. В модуле, основанном на Codegen, такой план терпит неудачу ещё до того, как может принести какую-либо пользу. Не нужно, чтобы линтер или рецензент высказывали возражения — это делают компилятор и генерируемый код. Важной частью является осознание того, что правило никогда не было просто командной привычкой, которую случайно терпит инструментарий; это свойство самого инструментария, которое команда просто зафиксировала в письменном виде.

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

Файл спецификации в центре TurboModule

Каждый новый TurboModule с архитектурой New Architecture содержит один файл на 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 никогда не может до него добраться. Такой «невидимый» несуществующий метод, пожалуй, хуже, чем ошибка компиляции: вы даже не узнаете о его недостатке, пока кто-то не вызовет его из JavaScript и не обнаружит, что его нет.

Метод спецификации без нативной реализации

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

В Android компилятор Kotlin или Java отклоняет класс, который расширяет генерируемую абстрактную спецификацию, но не реализует один из её абстрактных методов, что является лучшим вариантом, поскольку ошибка обнаруживается на ранней стадии. В iOS отсутствие обязательного метода протокола обычно приводит лишь к предупреждению компилятора; настоящая ошибка возникает во время выполнения в виде сбоя из-за нераспознанного выборщика, когда JavaScript пытается вызвать этот метод.

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

Почему это правило применимо ко всем библиотекам Codegen

Если бы подход «спецификация прежде всего» был лишь стилем определённой команды, он применялся бы только в репозиториях, чьи разработчики его выбрали. Однако контроль за соблюдением этого принципа осуществляется через Codegen, а Codegen принадлежит React Native. Каждая библиотека, которая позволяет использовать Codegen с блоком codegenConfig в режиме New Architecture, подчиняется тем же правилам, независимо от того, думали ли её создатели когда-либо в терминах «спецификация прежде всего».

Официальная документация React Native отражает это в своей формулировке. Описывая папку с результатами, в которую записывается Codegen, она представляет сгенерированный абстрактный класс как что-то, что обязан реализовать Turbo Native Module. Слово «обязан» имеет решающее значение: это требование, проверяемое при компиляции кода, а не рекомендация. Обзоры от сообщества описывают аналогичное поведение снаружи: как только настраивается codegenConfig, появляется сгенерированный файл Native...Spec, названный в соответствии с тем, что указано в codegenConfig в файле package.json. Разные авторы, разные репозитории — идентичный формат.

Существует также полезная подтверждение с противоположной стороны. Если поле codegenConfig вовсе не указывать в package.json, запуск установки через CocoaPods на 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.
  • Утверждение, которое остается достоверным, имеет узкий диапазон применения: речь идет о TurboModules новой архитектуры, генерируемых Codegen на основе спецификации JavaScript или TypeScript, что является официально задокументированным способом их создания, причем при сборке обеспечивается соблюдение принципа «спецификация в первую очередь».

    Стоит также отметить, что TurboModules на основе Codegen сегодня не являются единственным способом создания нативных модулей. Альтернативы, такие как Nitro Modules, используют собственную модель генерации кода и связывания; статья о том, как статические связи Nitro сравниваются с TurboModules, рассматривает этот компромисс.

    Порядок действий, который фактически задаёт процесс сборки

    Как только механизм становится ясным, порядок действий перестаёт быть вопросом субъективного выбора. Для добавления метода в TurboModule существует определённый порядок действий:

    1. Измените спецификацию в файле src/Native<Name>.ts, чтобы указать новый метод и его типы.
    2. Запустите Codegen: команда pod install генерирует код для iOS, а синхронизация или сборка через Gradle — код для Android.
  • Внедрите только что сгенерированные подписи на каждой платформе. Шаги для Android и iOS независимы и могут выполняться параллельно, но оба зависят от второго шага.
  • Повторно экспортируйте метод из публичного JavaScript API библиотеки.
  • Из этого следуют несколько практических рекомендаций:

    • Анализируйте изменения спецификации так же, как и изменения API. Спецификация фактически представляет собой публичный нативный контракт библиотеки, поэтому к изменениям в ней следует относиться с такой же осторожностью, как и к изменениям экспортируемых JavaScript-типов.
    • Перегенерируйте код перед отладкой. Если кажется, что нативный метод отсутствует, частой причиной является устаревший сгенерированный код, оставшийся после пропущенной команды pod install или синхронизации Gradle.
    • Запускайте сборку для обеих платформ в CI. Поскольку iOS может выдавать незаметные ошибки, в то время как Android — явные, наличие успешной сборки только для Android не гарантирует полной работоспособности моста.

    Заключение

    Понимание того, кто «владеет» правилом приоритета спецификации, — это не педантизм. Как только вы осознаёте, что речь идёт о механическом процессе, вы перестаёте рассматривать это как совет, который нужно учитывать при соблюдении сроков. Не существует варианта «только этот раз», когда можно сначала добавить нативный метод, а уже потом исправлять спецификацию, поскольку такой ускоренный подход не приводит к рабочему коду.

    • Codegen генерирует нативный мост на основе спецификации TypeScript, так что нативный код реализует спецификацию, а не определяет мост.
    • Отклонения в любом направлении приводят к сбоям: иногда на этапе компиляции, иногда незаметно в iOS, а иногда — к сбою во время выполнения.
    • Контракт распространяется на TurboModules, поддерживаемые Codegen; устаревшие мостовые модули и пакеты без codegenConfig находятся вне его действия, а компоненты Fabric требуют собственной проверки.
  • Сохраните принцип «сначала спецификации» в вашем руководстве по участию в разработке, но сформулируйте его как описание того, как работает процесс сборки, а не как требование к правильному поведению. Сборка будет соблюдать этот принцип независимо от того, что написано в документации.