Создаем Telegram Web App. Часть I: разработка на React Native Web
Всем привет! На связи команда dev.family, и мы вернулись с новым экспериментом. Хотим поделиться опытом разработки приложения на React Native для нескольких платформ и мессенджера, а именно – iOS, Android, Web и Telegram.
Разработчики давно пишут приложения под Web на React Native. Например, так работают Meta, Twitter (X) и Flipkart. Но для нашего кейса важен контекст, с которым можете столкнуться и вы. К нам пришел клиент, у которого уже было приложение под Android и iOS на React Native. Он захотел еще одну версию продукта – в формате Telegram Web App. Ранее мы работали c таким видом приложений для другого проекта, но так и не зарелизили его. Готовый прототип стал основой для разработки нового кейса.
Telegram WebApp – это веб-приложение, которое рендерится в своем WebView. Его можно написать на React, а стили и навигацию зашарить через Tamagui. Но в нашем случае мобильные приложения уже были полностью реализованы на React Native. Чтобы не писать код заново, мы решили использовать react-native-web.
Установка react-native-web
В документации технология описана так: «React Native for Web – это слой совместимости между React DOM и React Native. Его можно использовать в новых и существующих приложениях, веб-приложениях и многоплатформенных приложениях».
Проще говоря, это библиотека, которая позволяет запускать код на react-native в качестве веб-приложения. Больше информации тут.
Важно! Из-за NDA мы не можем показать то самое приложение, которое вдохновило нас на этот эксперимент. Поэтому прямо по ходу статьи напишем простой кликер, где нужно продумать стили, использование Haptic, получение данных профиля и использование темы. Обращаем внимание, что наш вариант кода может расходиться с документацией. Когда мы пытались полностью сделать все по ней, возникали определенные проблемы.
Для старта понадобится react-native приложение. Запускаем в нужной вам директории команду:
Также мы решили убрать yarn и поставить pnpm (это личный выбор, вы можете использовать любой другой пакетный менеджер).
Больше о разных пакетных менеджерах читайте в нашей статье "Что такое менеджер пакетов и в чем разница YARN, NPM, PNPM?"
Но react-native не может сходу использовать pnpm. Вот, что нужно сделать:
- Выполните команду git clean -xfd
- Удалите packageManager из package.json
- Установите следующие пакеты – (@react-native-community/cli-platform-android, jsc-android, @react-native/gradle-plugin)
- После это выполните pnpm install cd ios && pod install && cd ..
- Можете запускать
Чтобы веб-приложение рендерилось, нужен index.html файл. Создадим его в самом начале - поместим в корень проекта index.html и добавим следующий код:
index.html
Нам также понадобится и index.web.js (это видно из тега script). Создаем его в корне проекта на уровне index.js и помещаем туда следующий код:
index.js
По сути, здесь происходит почти то же самое, что и в index.js. Только кроме registerComponent мы также находим наш div с id=”app-root” и рендерим в нем приложение.
Дополнение enableExperimentalWebImplementation(true)– не обязательная часть кода. Но в ходе разработки мы столкнулись с проблемами при использовании “react-native-gesture-handler”. Поэтому нам оно помогло.
Далее нужен сборщик, и просто Metro в этом случае не поможет. На странице с react-native-web есть пример конфигурации webpack. Также ее можно получить при установке react-native-reanimated (мы ставим его на всех проектах с кодом на React Native). Здесь она не сработала, поэтому в качестве сборщика использовали Vite и плагин для react-native-web–vite-plugin-react-native-web.
Далее создаем vite.config.js в корне проекта и добавляем следующую часть кода:
Забыл упомянуть, что перед этим нужно поставить следующие пакеты: vite, @vitejs/plugin-react,vite-plugin-commonjs, vite-plugin-react-native-web,babel-plugin-react-native-web.
Если вы тоже собираетесь использовать react-native-reanimated, нужно поставить еще и эти пакеты: react-native-reanimated, @babel/plugin-proposal-export-namespace-from
Если используете react-native-reanimated, ваш babel.config.js будет выглядеть вот так:
babel.config.js
Мы использовали babel.config.js чистого React Native проекта на версии 0.74.5 с дополнительными плагинами. Если вы работаете с другой версией React Native, обратите внимание только на plugins.
Далее добавьте следующие команды в scripts внутри package.json:
package.json
Теперь мы запускаем наше приложение и проверяем, все ли работает, как нужно. В нашем App.tsx пока есть только текст. Поэтому получаем следующий результат:
Также запускаем приложение на iOS и Android, чтобы убедиться, что все работает. И дальше переходим к написанию нашего приложения.
Написание приложения
У нас уже есть основа для приложения, но она довольно простая. Хочется разместить в Telegram что-то поинтереснее, потому что в мессенджере есть много возможностей и функций для взаимодействия с клиентом. Но к ним требуется доступ.
Согласно документации, это можно сделать через глобальный объект window и далее window.Telegram.WebApp. В отличие от приложений на React.js, в React Native у нас нет как такого объекта (window). И если мы попробуем получить к нему доступ, Typescript выдаст ошибку.
Но при использовании react-native-web для веб-приложения, доступ к window предусмотрен. Дальше будет не очень красивая часть, но для большего удобства нужно прописать типы и объявить window вручную. Создадим global.d.ts в корне проекта и пропишем следующее:
- Добавим скрипт в тег head, чтобы подключить наше mini app к Telegram client:
index.html
global.d.ts
В файле мы прописали необходимые типы данных, которые получим из Telegram.WebApp, и объявили глобально window.
Но помните: мы пишем еще и мобильное приложение. Поэтому не будем использовать объект window напрямую, чтобы не допустить ошибок. Вместо этого, создадим глобальный объект TelegramConfig и запишем туда все данные из Telegram. Для мобильной части создадим MockConfig, куда внесем все самостоятельно. Они будут статичными, так как данные из Telegram мы, естественно, не получим.
Создадим файл src/config.ts и пропишем:
config.ts
Создаем MockConfig для нашего мобильного или веб-приложения при отсутствии данных из Telegram-клиента. Далее – функцию конфигурации, которая, при наличии данных из Telegram, вернет нам их —} MockConfig. Ее будем использовать для получения данных.
Теперь пропишем небольшой кликер с использованием настроек темы и данных пользователя из нашего конфига/tg.
Обозначим один момент: в рамках статьи мы не стремимся создать сложное приложение. Намного важнее – показать, как можно использовать Telegram client и его функции/опции без каких-либо проблем.
Пропишем команду, чтобы поставить библиотеки для навигации внутри приложения. Для данного примера это необязательно, но мы все равно сделали это для вас ❤
Также добавим пакеты для анимаций:
Внесем настройки для подключения анимаций. Переходим в babel.config.js
babel.config.js
Ставим поды для iOS:
Далее создадим папки src, src/components, src/screens и файлы в них:
- src/RootNavigator.tsx
- src/screens/HomeScreen.tsx
- src/utils.ts
- src/components/index.ts
- src/components/Coin.tsx
- src/components/Header.tsx
- src/components/Progress.tsx
- src/components/Screen.tsx
Внутри папки src мы получаем вот такую структуру:
— components
- Screen.tsx
- Coin.tsx
- Progress.tsx
- index.ts
- Header.tsx
— screens
- HomeScreen.tsx
— utils.ts
— App.tsx
— RootNavigator.tsx
Итак, мы создали файлы для компонентов. Теперь давайте приступим к их наполнению и созданию самих компонентов. Начнем с header:
Переходим в src/components/Header.tsx
src/components/Header.tsx
Как видно из примера выше, мы используем config().themeParams, чтобы достать настройки темы для цветов в header. Отсюда же достаем информацию о пользователе – берем username и photo_url. Но, как указано в документации, photo_url может отсутствовать. Поэтому добавим проверку его наличия и вывод заглушки, если его действительно нет. Создадим в корне проекта папку assets/icons, где будем хранить наши картинки. В этом приложении понадобится только две: заглушка для фото пользователя и картинка самой монетки, по которой будем кликать.
С header мы закончили. Приступаем к следующему компоненту – самой монете. Просто вставить картинку и повесить на нее клик – скучно. Лучше добавим несколько анимаций: например, для появления и исчезновения цифры, что довольно легко, а также анимацию поворота монетки.
src/components/Coin.tsx
Все анимации воспроизводятся дважды – в момент нажатия на монету, и когда мы отпускаем палец. Разберем эту часть кода чуть подробнее.
Создаем rotateX, rotateY (SharedValue) и rotateStyle(AnimatedStyle). В rotateStyle смотрим на изменения наших SharedValue и мутируем их в соответствии с позицией нажатия на нашу монету. Сам анимированный стиль передаем в AnimatedButton, который получили после использования функции createAnimatedComponent и ее аргумента Pressable. В зависимости от rotateX и rotateY, монета будет наклоняться в одну или другую сторону.
RotateX и rotateY будем изменять при нажатии на кнопку, после которого мы получаем координаты места нажатия. Далее отнимаем от этих координат центр нашего элемента и так находим дельту. Теперь нужно умножить дельту на значение чувствительности (от 0 до 1), после чего получаем угол наклона по осям X и Y. Прописываем значения нажатия по X и Y в number, чтобы дальше использовать их для отображения улетающей цифры.
Все эти действия нужны для написания логики при нажатии на кнопку. Но в коде также должно быть описано, что происходит, когда мы ее отпускаем. Первым делом, ставим анимированное значение в 0, чтобы монета могла вернуться в начальное положение. Обратите внимание, что у нас есть условия для веба и других платформ: и при нажатии, и когда мы отпускаем кнопку. У React Native Reanimated есть проблемы с анимациями в вебе, поэтому в некоторых ситуациях им нужен повторный рендер. Для этого нужна проверка, так как мы используем withTiming. Он отвечает за то, что значение меняется не в момент, а со временем, указанным в animationConfig.
Далее вызываем метод onClick, который передаем в пропсах, чтобы выполнять действия при нажатии. В setTimeout (он нужен для своевременного появления элемента с цифрой) убираем значение number и showNumber, чтобы сработала наша анимация по выходу элемента из DOM дерева.
Раз уж мы затронули анимацию цифры, то для нее используем простой Animated.View и его пропс – exiting для анимации выхода из рендера из библиотеки Reanimated. Теперь при выходе элемента будет срабатывать анимация, которая показывает, как монета уходит вверх. Также в стили мы передаем x и y из number, чтобы разместить его в месте нажатия на кнопку.
Теперь перейдем к Progress.tsx
src/components/Progress.tsx
В этой части нет ничего сложного. Просто через пропсы передаем значение max и текущие значение (amount). При увеличении amount растет и значение прогресса. Также используем цвета из нашего конфига, который можно взять или из параметров, или из настроек темы Telegram пользователя.
Теперь создадим простой Screen.
src/components/Screen.tsx
Соберем все в нашем HomeScreen:
src/screens/HomeScreen.tsx
Тут все тоже довольно просто. Единственное, что может вызывать вопросы, – clickedAmount & amount. По сути это два одинаковых значения, зачем они нам? Ответ прост:
- amount – это количество всех момент пользователя
- clickedAmount – количество раз, которое пользователь нажал на кнопку.
Amount нужно где-то сохранять. А clickedAmount, когда пользователь получает новые клики, – со временем сбрасывать. Этот функционал мы не прописывали, поэтому можете поэкспериментировать с ним самостоятельно.Далее поместим все это в RootNavigator, а сам навигатор – в App.tsx
src/RootNavigator.tsx
src/tApp.tsx
В App.tsx мы в useEffect вызываем метод expand. Он нужен, чтобы приложение при запуске в Telegram открылось во весь экран.
Итоговый вариант кода выглядит вот так – ссылка на репозиторий.
Итак, у нас получился обычный кликер с базовым функционалом.Хочу заметить, что в этом примере мы достаем данные пользователя из initDataUnsafe. И это не лучшее решение, потому что по документации можно провалидировать нашу initData и использовать ApiKey от Telegram-бота. Но наш пример – просто демонстрация, поэтому такого варианта тоже достаточно.
Использовать мок юзера в мобильном приложении – тоже так себе идея. Лучше отдельно обработать и показать авторизацию, либо сделать вход из гостевого аккаунта. На эту тему можно долго рассуждать, но тут мы оставим вам пространство для фантазий. Просто клонируйте репозиторий и играйте с ним, как хочется. А мы продолжим.
Теперь дополнительно проверим, что дает в плане дополнительных функций Telegram client. Для этого используем Haptic Feedback из WebApp библиотеки.Для мобильного приложения он не подойдет, поэтому поступим по-другому.
Поставим библиотеку для Haptic. Мы использовали expo-haptics, потому что у них примерно схожие аргументы с HapticFeedback из Telegram. Наш проект написан на чистом React Native, поэтому сначала поставим expo и потом – expo-haptics.
- pnpx install-expo-modules@latest
- pnpx expo install expo-haptics
- cd ios && pod install
Далее пропишем hook, который будет служить нам оберткой.
src/useHaptics.ts
Теперь мы можем использовать HapticFeedback и в Telegram mini app, и в нашем обычном приложении. Остается только добавить haptic при клике на монетку.
Также, как и с haptics, вы можете попробовать сделать хранилище для сохранения результата. Но эта часть – уже на вашей стороне ✊Д
ело за малым – развернуть приложение в Telegram. Но об этом уже в следующей части.
До скорых встреч с dev.family 💜!