Создание нативного модуля для React Native становится намного приятнее и быстрее. В Expo Modules 2.0 модуль — это просто аннотированный класс на Swift или Kotlin: вы пишите обычные методы и свойства, помечаете те, которые хотите сделать доступными для JavaScript, и на этом всё. Больше не нужно изучать ничего нового, кроме нескольких аннотаций, не нужно поддерживать шаблонный код, а скорость работы на этапе выполнения выше, чем у API, которое оно заменяет.
Это краткая версия. API Expo Modules всегда брало на себя сложную работу по нативной интероперабельности: конвертацию данных между значениями JavaScript и нативными типами, запуск функций в правильных потоках для использования ядер устройства без необходимости писать синхронизацию вручную, а также внедрение модуля в жизненный цикл приложения. То, что меняет версия 2.0 — это та часть, которую вы пишете сами. В версии 1.0 вы описывали возможности модуля для JavaScript с помощью специально созданного DSL. В 2.0 это берется напрямую из нативного кода, и создание модуля сводится к написанию кода на Swift или Kotlin так же, как вы делали бы это для любого приложения под iOS или Android.
Expo Modules 2.0 выйдет совсем скоро, и на iOS ее уже можно попробовать: SDK 57 включает макросы Swift, которые лежат в ее основе, охватывая модули, функции, структуры данных (records), разделяемые объекты (shared objects) и события. Компоненты пользовательского интерфейса (Views) пока не поддерживаются (подробнее об этом ниже), а версия для Android, которая будет следовать той же модели создания, все еще находится в разработке. В SDK 58 Expo Modules 2.0 перейдет в статус бета-версии: она будет задокументирована, официально поддержана и поставляться со специальным навыком (agent skill) для обучения вашего ассистента программирования новому API. Поскольку iOS идет первой, в примерах ниже используется Swift. Вот как это выглядит.
Если вы читали статью «Общение с JSI на Swift: что изменилось в SDK 56», этот пост является ее продолжением и опирается на описанную там работу. На iOS в SDK 56 внимание уделялось тому, что находится «под капотом» вашего модуля: мы отказались от прослойки Objective-C++, поэтому теперь Swift общается с JSI напрямую, а вызовы стали примерно в два раза быстрее. Фундамент для Android был заложен иначе — с помощью плагина компилятора Kotlin, который переносит часть работы с этапа выполнения на этап сборки (подробнее об этом в статье «Как плагин компилятора Kotlin сократил время первого рендера на Android на 30%»). Этот пост посвящен коду, который вы пишете поверх этого фундамента.
Текущий DSL
Вот небольшой модуль, написанный с использованием API Expo Modules 1.0:
Это работает, и множество нативного кода в экосистеме Expo написано именно так. Но для его написания нужно держать в голове (или в контексте вашего AI-ассистента) довольно много деталей. DSL представляет собой собственную небольшую грамматику, построенную на функции Swift под названием result builders (тот же механизм, который лежит в основе тела в SwiftUI). Вам нужно знать его словарный запас: Function, AsyncFunction, Property, Class, Events и так далее. Требуется явно указывать типы параметров замыкания и выбирать синхронный или асинхронный вариант. Ничто из этого не относится к тому, что делает ваш модуль, и Expo Modules 2.0 избавляется от всего этого, полностью переходя на чистый синтаксис Swift.
Тот же модуль в версии 2.0
Это весь модуль целиком. Это класс с аннотированными методами и свойствами, и больше ничего: никаких definition(), никаких Name(...) (имя по умолчанию совпадает с именем класса, если вы не передадите его в качестве аргумента макроса) и никаких построителей результатов (result builders). Каждый компонент DSL сводится к обычным объявлениям в Swift.
И Function, и AsyncFunction превращаются в обычный метод Swift, помеченный как @JS. Его имя и типы считываются непосредственно из объявления. Является ли он синхронным или асинхронным, определяется ключевым словом async в Swift: асинхронный метод появляется в JavaScript как функция, возвращающая Promise.
Свойство Expo Modules 1.0 с геттером и сеттером теперь превращается в обычный var в Swift. Записываемые свойства Swift (хранимые свойства var и вычисляемые свойства с сеттером) доступны для записи в JS. Свойства Swift только для чтения (константы let и свойства var только с геттером) доступны только для чтения в JS. Эти свойства доступны как из Swift, так и из JS и не требуют дополнительной настройки; в Expo Modules 2.0 синтаксис определения свойств — это просто стандартный Swift.
Миграцию не обязательно проводить единовременно. Оба API могут сосуществовать в одном модуле: сохраните ваш существующий definition(), добавьте @ExpoModule и переносите функции и свойства в @JS по одному. Всё, у чего еще нет аналога в версии 2.0, может просто оставаться в definition. Они объединяются, поэтому вы можете внедрять новый подход постепенно, не переписывая модуль целиком.
Вам даже не обязательно выполнять конвертацию вручную. Навык expo-migrate-module позволяет вашему AI-ассистенту перенести Swift-часть модуля с версии 1.0 на 2.0, оставив JavaScript API без изменений. Всё, что он пока не может перенести, он оставляет в definition() версии 1.0 и перечисляет в своем отчете, чтобы вы точно знали, что осталось сделать. Просто запустите:
Это запускает интерактивную сессию с загруженным навыком; замените claude-code на codex, cursor или любой другой используемый вами агент. Навык находится в плагине expo-experiments и развивается вместе с API, а при каждом использовании подтягивается самая свежая версия.
Структуры данных (records), разделяемые объекты (shared objects) и события обрабатываются аналогичным образом
Функции — это не единственное, что изменилось. Остальные строительные блоки модуля следуют той же идее использования нативного синтаксиса.
Структура данных (record) — это Swift-структура, и каждое хранимое свойство, которое не является private, static или lazy, становится полем. Является ли поле обязательным, опциональным или допускающим значение null, считывается прямо из объявления, и писать ничего лишнего не нужно:
Разделяемый объект (shared object) — это класс, экземпляры которого существуют одновременно и в Swift, и в JavaScript: JS содержит объект, под которым скрывается нативный экземпляр. Вы экспортируете его так же, как модуль, с помощью методов и свойств @JS, включая инициализатор @JS init(), который становится конструктором в JS.
Сторона JS получает API в веб-стиле (например, currentTime в секундах, как у HTML-элемента мультимедиа), в то время как класс под капотом сопоставляет его с типами AVFoundation. Форму того, что видит JavaScript, определяете вы; аннотации просто переносят это через границу сред.
В версии 1.0 событие представляло собой зарегистрированную строковую переменную плюс вызовы sendEvent(...):
В Expo Modules 2.0 событие — это единое типизированное вызываемое свойство. Вы один раз объявляете тип полезной нагрузки (payload), а вызов этого свойства инициирует отправку события:
Полезная нагрузка может быть любого типа, который можно передать в JavaScript: примитивы, массивы, словари, структуры данных, разделяемые объекты, типизированные массивы и буферы массивов, а также стандартные платформенные типы, такие как Data и Date «из коробки». Поддержка любого другого нативного типа требует лишь реализации протоколов JavaScriptEncodable и JavaScriptDecodable — точно так же, как вы реализуете собственный протокол Codable в Swift. Если тип полезной нагрузки нельзя передать, компилятор Swift выдаст понятную ошибку на этапе сборки, поэтому вы узнаете об этом как можно раньше, еще до того, как пользователи получат ваше приложение.
Почему концепция «просто нативный код» стала важнее, чем раньше
Фраза «важнее, чем раньше» относится к тому, кто именно пишет код в настоящее время. Нативные модули все чаще пишутся, проверяются и мигрируют с участием AI-ассистентов, и от того, какой API вы передаете такому агенту, зависит эффективность всей работы. Всё, что делает версию 2.0 удобной для вас (меньшему нужно учиться, типы собраны в одном месте, ошибки возникают там, где была допущена оплошность) — это ровно то, что необходимо и языковой модели.
Модели обучаются на кодовой базе Swift и Kotlin возрастом более десяти лет, в то время как у DSL версии 1.0 сравнительно мало примеров для обучения модели, поэтому сегодня агенту требуется детальное описание API в контекстном окне. В версии 2.0 большую часть API представляет собой сам язык, поэтому учить приходится гораздо меньше. Кроме того, версия 2.0 сокращает цикл «генерация-ошибка-исправление», так как компилятор может находить неверные типы в объявлениях вашего модуля и выдавать понятные для агента ошибки. Агент, способный написать iOS-приложение, сможет написать и модуль Expo.
Чище на поверхности, быстрее «под капотом»
Именно здесь окупается работа, проделанная в SDK 56, и именно поэтому версия 2.0 — это не просто удобный синтаксис.
Expo Modules 2.0 использует макрос времени сборки (@JS) для чтения сигнатур ваших функций на Swift, благодаря чему заранее известны точные типы каждого аргумента и возвращаемого значения. Версия 1.0 не может определить их до этапа выполнения, поэтому она работает по принципу рефлексии: каждый нативный вызов упаковывает входящие аргументы в выделенный контейнер с подсчетом ссылок, пропускает каждый из них через конвертер динамических типов в массив [Any] и формирует кортеж перед вызовом вашей функции. Этот динамический путь представляет собой самые большие накладные расходы при вызове на сегодняшний день. Когда сигнатуры функций известны заранее, сгенерированные привязки считывают каждый аргумент прямо там, где его оставил JS-рантайм, и преобразуют его непосредственно в параметр Swift без выделения дополнительных ресурсов. Работа по преобразованию типов, которую версия 1.0 повторяет при каждом вызове, в версии 2.0 выполняется на этапе сборки, а сопутствующие накладные расходы во время выполнения также исчезают.
Прирост скорости весьма значителен благодаря оптимизациям в трех SDK. В SDK 56 был удален слой Objective-C++, что сделало вызовы модулей Expo в 1,5–2 раза быстрее, чем в SDK 55, и сопоставимым с Turbo Modules в React Native. SDK 57 снова улучшает общий рантайм, и это изменение идет на пользу абсолютно всем модулям, включая те, что остаются на версии 1.0. Как видно на графике, они стали работать быстрее без изменения ни единой строки кода. Вдобавок к этому макрос @JS устраняет оставшиеся динамические накладные расходы на каждый вызов.
Это общее время для 100 000 вызовов, меньше — значит лучше:
Все показатели были замерены на одной конфигурации: iPhone 16 Pro под управлением iOS 27, релизная сборка с предкомпилированными фреймворками.
На том же SDK путь с использованием @JS в 2,5–5,6 раза быстрее по сравнению с API версии 1.0 для синхронных вызовов. Асинхронные вызовы показывают близкие результаты, так как оба API используют под капотом один и тот же механизм промисов. Более существенное улучшение для асинхронных вызовов появится в SDK 58. Самый чистый API теперь стал еще и самым быстрым путем для вызовов модулей.
Что ждет версию 2.0 в будущем
Следующими на очереди являются представления. Они будут следовать той же модели, что и модули: представление будет представлять собой класс с аннотацией @ExpoView, а его пропсы и колбэки событий будут объявляться единожды в типизированной структуре @ViewProps вместо разделения между нативным представлением и написанным вручную типом пропсов JS.
Вторая создаваемая нами вещь — это генерация TypeScript. Поскольку каждый элемент @JS, @Record и пропс представления уже содержат свои типы в нативной сигнатуре, специальный инструмент сможет генерировать TypeScript-объявления модуля на основе этого исходного кода. Сегодня вы пишете нативные типы и соответствующие им TypeScript-объявления раздельно; наша цель — писать их один раз.
Это противоположность генерации кода в некоторых других модулях React Native, где вы пишете спецификацию на TypeScript, а генератор создает шаблон нативного кода для заполнения. В Expo Modules 2.0 вы используете нативные API платформы так же напрямую, как и любое нативное приложение, остальная часть вашего приложения остается на React, а типы TypeScript возникают из нативного кода. TypeScript-объявления модуля становятся сгенерированным артефактом сборки, и CI может обнаруживать их рассинхронизацию с нативным кодом.
Инструментарий, создающий TypeScript-объявления, сначала преобразует исходный код Swift в машиночитаемую сводку всего, что экспортирует модуль: каждой функции, свойства, записи и события с указанием их типов. Эта сводка также дает преимущества подхода, основанного на спецификациях: как только Android сможет создавать такую же сводку из своего исходного кода Kotlin, инструментарий сможет сравнивать их и выявлять непреднамеренные различия между платформами до того, как они превратятся в баги в JS.
Когда я могу начать использовать это?
🚀 Уже сегодня на iOS в SDK 57. Базовые макросы (@ExpoModule, @JS, @Record, @SharedObject, @Event) уже поставляются вместе с SDK. Они еще не документированы, поэтому считайте их экспериментальными: API все еще может измениться, а руководства и справочные материалы появятся вместе с бета-версией SDK 58. Поддержка Android находится в разработке. Если вы хотите писать нативные модули таким образом уже сегодня, вы можете это сделать.
В более отдаленной перспективе Expo Modules 2.0 станет стандартным способом написания модулей Expo, в то время как версия 1.0 продолжит работать для всего, что уже было создано на ней. Если вы пишете нативные модули, попробуйте версию 2.0 в SDK 57 и расскажите нам в канале #creating-expo-modules в Discord-сообществе разработчиков Expo о том, что сработало, что нет и что бы вы хотели увидеть дальше. Официальная бета-версия появится в SDK 58.










