Рендеринг, определяемый сервером, для React Native с использованием Rise
В этой статье содержится реализация разработки Server-Driven UI (SDUI) с использованием Rise Tools. Она разработана для бесшовного обновления приложений React Native, предоставляя динамические компоненты UI, которые можно обновлять в производстве без пересборки или повторного развертывания приложения.
Предварительные условия:
Перед погружением убедитесь, что у вас есть:
- Базовые знания JavaScript, TypeScript и React Native
- Node.js и npm установленые на вашей машине для разработки
- Убедитесь, что вы выполнили инструкции по настройке среды React Native.
- Базовые знания Tamagui
Введение в разработку на основе сервера
SDUI — это архитектурный шаблон, в котором сервер определяет и управляет пользовательским интерфейсом клиентского приложения. Вместо жесткого кодирования элементов UI в клиенте, сервер отправляет спецификацию UI (часто в формате JSON), которую клиент интерпретирует и отображает. Такой подход позволяет выполнять динамические обновления UI без необходимости обновления приложения.
Ключевые концепции включают в себя:
- Спецификация пользовательского интерфейса: структурированное описание макета интерфейса и его компонентов.
- Клиентский рендерер: компонент, который интерпретирует спецификацию и создает собственные элементы пользовательского интерфейса.
- Динамические обновления: возможность изменять пользовательский интерфейс путем изменения спецификации на стороне сервера.
Преимущества и варианты использования SDUI:
- Гибкость: легкое обновление пользовательского интерфейса без релизов в магазине приложений.
- Последовательность: поддержание единообразного опыта на всех платформах.
- A/B-тестирование: быстрое тестирование различных макетов или функций.
- Персонализация: адаптация пользовательского интерфейса на основе сегментов или поведения пользователей.
- Быстрая итерация: более быстрая реализация изменений пользовательского интерфейса.
- Флаги функций: удаленное включение и отключение функций.
- Уменьшен размер приложения: часть логики пользовательского интерфейса перенесена на сервер.
Почему рендеринг, определяемый сервером, превосходит обновления по беспроводной сети и запросы на обновление
- Мгновенные обновления: в отличие от обновлений OTA или запросов на обновление, SDR гарантирует, что все пользователи всегда используют последнюю версию, устраняя проблемы фрагментации и совместимости версий.
- Улучшенный пользовательский интерфейс: SDR устраняет необходимость в навязчивых запросах на обновление или времени ожидания загрузки, обеспечивая бесперебойную работу.
- Повышенная безопасность и контроль: благодаря логике на сервере SDR обеспечивает лучшую защиту от обратного проектирования и несанкционированных модификаций.
- Гибкость и оперативность: SDR позволяет проводить быстрые итерации и A/B-тестирование без необходимости одобрения со стороны магазина приложений или действий пользователя.
- Снижение сложности на стороне клиента: перенося логику на сервер, SDR упрощает клиентский код, потенциально повышая производительность приложения и сокращая количество ошибок.
- Улучшения на основе данных: поскольку все пользователи используют одну и ту же версию, проще собирать последовательную аналитику и принимать решения на основе данных.
Настройка сервера Rise
Мы будем работать с этим UI, сосредоточившись на создании UI на стороне сервера и внедрении его в приложение React Native. Следуйте разделам руководства, чтобы шаг за шагом создать свое приложение на базе SDUI.
Настройка сервера Rise
Чтобы создать новый проект сервера Rise, выполните следующие действия:
- Откройте терминал.
- Выполните следующую команду:
npm create rise@latest
Это результирующая структура папок:
my-rise-project/
├── node_modules
├── src/
│ ├── models.tsx
│ ├── server.ts
├── package.json
├── tsconfig.json
└── README.md
Настройка сервера для главного экрана
Создайте файл HomeScreen.tsx
в предпочитаемом вами каталоге и приступим.
import {
H1,
H3,
H6,
Text,
XStack,
YStack,
} from "@rise-tools/kitchen-sink/server";
export const models = {
HeaderUI,
SearchUI,
NearbyUi: NearbyUi,
BannerUi,
ProductUi,
HomeScreen,
};
function HeaderUI() {
return (
<YStack>
<Text>Header Ui</Text>
</YStack>
);
}
function SearchUI() {
return (
<YStack>
<H1>Search Ui</H1>
</YStack>
);
}
function NearbyUi() {
return (
<YStack>
<H3>Nearby resturants</H3>
<XStack gap="$12">
<H6>Nearest resturants</H6>
</XStack>
</YStack>
);
}
function BannerUi() {
return (
<YStack>
<H1>Banner</H1>
</YStack>
);
}
function ProductUi() {
return (
<YStack>
<Text>Our Top Product</Text>
<XStack gap="$8">
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
</XStack>
</YStack>
);
}
function TripUi() {
return (
<YStack gap="$8">
<Text>Your Trips</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
<Text>🌚</Text>
</YStack>
);
}
function HomeScreen() {
return (
<YStack>
<HeaderUI />
<SearchUI />
<NearbyUi />
<BannerUi />
<ProductUi />
<TripUi />
</YStack>
);
}
Объект модели:
Объект с именем models
экспортируется, содержащий ссылки на различные компоненты пользовательского интерфейса. Этот объект используется для динамического рендеринга и организации компонентов для более легкого доступа.
Этот компонент разбивает HomeScreen
на несколько отдельных компонентов: HeaderUi
, SearchUi
, NearbyUi
, BannerUi
, ProductUi
и TripUi
. Эти компоненты могут использоваться по отдельности на стороне мобильного устройства/клиента или объединяться для формирования полного HomeScreen
. Используя HomeScreen
в клиентском приложении, мы можем динамически перестраивать его структуру с сервера.
Настройка server.ts
import { setupRiseTools } from "@rise-tools/cli";
import { createWSServer } from "@rise-tools/server";
import { models as mainModal } from "~src/home/home"; // import models from `Homescreen.tsx`
const port = Number(process.env.PORT || "3015");
const models = { ...mainModal };
const server = createWSServer(models, port);
if (process.env.NODE_ENV === "development") {
setupRiseTools({ server });
}
Подготовка вашего проекта Expo
Клонирование существующего шаблона Expo
Чтобы быстро приступить к работе с приложением, вы можете клонировать этот базовый репозиторий, который настроен с помощью Expo
, expo-router
, Tamagui
, и Rise Tools
.
Выполните следующую команду для клонирования репозитория:
git clone git@github.com:rise-tools/rise-mobile-quickstart.git
Ручная настройка
Чтобы настроить свой проект с Tamagui и Expo Router, выполните следующую команду:
npx create-expo-stack@latest react-native-rise-sdui-mobile --expo-router --tamagui
Затем установите необходимые зависимости:
@rise-tools/kit-expo-router
@rise-tools/kitchen-sink
@rise-tools/react
@rise-tools/ws-client
expo-haptics
Создать modelSource.ts
Далее создайте файл modelSource.ts
для определения соединения с сервером WebSocket, предоставляемого сервером.
import { createWSModelSource } from "@rise-tools/ws-client";
export const modelSource = createWSModelSource("ws://192.168.0.213:3015"); //switch 192.168.0.213 with your expo localhost url
Создать riseActions.ts
Создайте файл riseActions.ts
для определения локальной библиотеки действий для приложения.
import { useExpoRouterActions } from "@rise-tools/kit-expo-router";
import {
useHapticsActions,
useLinkingActions,
useToastActions,
} from "@rise-tools/kitchen-sink";
export function useRiseActions() {
return {
...useHapticsActions(),
...useLinkingActions(),
...useToastActions(),
...useExpoRouterActions(),
};
}
Создать riseComponenets.ts
Создайте файл riseComponents.ts
д для определения локальной библиотеки компонентов для приложения.
import {
FormComponents,
LucideIconsComponents,
QRCodeComponents,
RiseComponents,
SVGComponents,
TamaguiComponents,
} from '@rise-tools/kitchen-sink';
export const components = {
...FormComponents,
...LucideIconsComponents,
...QRCodeComponents,
...RiseComponents,
...SVGComponents,
...TamaguiComponents,
};
Интеграция модели HomeScreen с сервера
- В терминале сервера запустите Server, чтобы запустить сервер:
npm run dev
- В терминале приложения React Native запустите приложение, используя следующие команды:
•yarn ios
открыть приложение на симуляторе iOS;
•yarn android
открыть приложение на эмуляторе Android;
•yarn start
открыть приложение с помощью Expo на физическом устройстве. - В соответствующем файле Expo настройте HomeScreen для использования инструментов Rise, вызвав модели, определенные для HomeScreen:
import { Rise } from '@rise-tools/react';
import { Stack, Link } from 'expo-router';
import { View } from 'react-native';
import { Button } from '~/components/Button';
import { Container } from '~/components/Container';
import { ScreenContent } from '~/components/ScreenContent';
import { modelSource } from '~/src/modelSource';
import { useRiseActions } from '~/src/riseActions';
import { components } from '~/src/riseComponents';
export default function Home() {
return (
<>
<Rise
modelSource={modelSource}
components={components}
path="HomeScreen" //model gotten from the server
actions={useRiseActions()}
/>
</>
);
}
Обновление приложения React Native с помощью обновления сервера
Во время работы сервера любые изменения, внесенные в модель главного экрана, будут автоматически отражены в мобильном приложении.
function HeaderUI() {
return (
<XStack
alignItems={"center"}
padding={"$2"}
justifyContent={"space-between"}>
<XStack>
<UserIcon width={70} height={60} />
<XStack alignItems={"center"}>
<YStack>
<Text color={"#838282"}>Hello,</Text>
<H3 color={"#0F0F0F"} fontWeight={"700"}>
F.A.S
</H3>
</YStack>
</XStack>
</XStack>
<Hamburger width={32} height={32} />
</XStack>
);
}
Чтобы завершить дизайн, мы обновим другие компоненты нашего компонента домашнего экрана, что автоматически обновит приложение.
import {
H3,
H6,
H5,
RiseForm,
Text,
XStack,
YStack,
InputField,
} from "@rise-tools/kitchen-sink/server";
import {
UserIcon,
Hamburger,
SearchIcon,
MicIcon,
SmallFork,
RightIcon,
ClockIcon,
StarIcon,
CarIcon,
PackageIcon,
ErrandIcon,
Ridesicon,
ResturantIcon,
} from "~/components/UserIcon";
export const models = {
HeaderUI,
SearchUI,
NearbyUi: NearbyUi,
BannerUi,
ProductUi,
HomeScreen,
};
const myProducts = [
{
name: "Package",
icon: <PackageIcon width={32} height={32} />,
},
{
name: "Rides",
icon: <Ridesicon width={35} height={32} />,
},
{
name: "Errands",
icon: <ErrandIcon width={32} height={32} />,
},
{
name: "Resturant",
icon: <ResturantIcon width={32} height={32} />,
},
];
function HeaderUI() {
return (
<XStack
alignItems={"center"}
padding={"$2"}
justifyContent={"space-between"}>
<XStack>
<UserIcon width={70} height={60} />
<XStack alignItems={"center"}>
<YStack>
<Text color={"#838282"}>Hello,</Text>
<H3 color={"#0F0F0F"} fontWeight={"700"}>
F.A.S
</H3>
</YStack>
</XStack>
</XStack>
<Hamburger width={32} height={32} />
</XStack>
);
}
function SearchUI() {
return (
<XStack alignItems={"center"}>
<SearchIcon width={20} height={20} />
<RiseForm onSubmit={() => {}} flex={1} justifyContent={"center"}>
<InputField
id="searchInput"
backgroundColor={"#fff"}
borderWidth={0}
borderRadius={0}
height={40}
style={{ marginTop: -14 }}
placeholder={"Where are you going?"}
placeholderTextColor={"#838282"}
color={"black"}
/>
</RiseForm>
<MicIcon width={20} height={20} />
</XStack>
);
}
function NearbyUi() {
return (
<YStack marginTop={"$4"} gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H3 color={"black"} fontWeight={"500"}>
Nearby Resturants
</H3>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack justifyContent={"space-between"} alignItems={"center"}>
<XStack gap={"$2"} alignItems={"center"}>
<SmallFork width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
The Place Restaurant
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>5mins drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>4.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<RightIcon width={24} height={24} />
</XStack>
</YStack>
);
}
function BannerUi() {
return (
<XStack
backgroundColor={"#24B229"}
alignItems={"center"}
justifyContent={"space-between"}
paddingHorizontal={"$4"}
marginVertical={"$4"}
borderRadius={15}>
<YStack>
<Text fontSize={20} color={"white"} fontWeight={"500"}>
Ride with us.
</Text>
<Text fontSize={20} color={"white"} fontWeight={"500"}>
Earn Points.
</Text>
<Text fontWeight={"500"} color={"white"}>
Get Started {"->"}
</Text>
</YStack>
<CarIcon width={187} height={118} />
</XStack>
);
}
function ProductUi() {
return (
<YStack gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H5 color={"black"} fontWeight={"500"}>
Our Products
</H5>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack marginBottom={"$4"}>
{myProducts.map((item) => (
<YStack
key={item.name}
justifyContent="space-between"
alignItems="center"
flex={1}>
<YStack alignItems="center">
<YStack
backgroundColor="#ECECEC"
borderRadius={"$4"}
padding={"$4"}
marginVertical={"$2"}>
{item.icon}
</YStack>
<Text fontSize={14} color={"#0F0F0F"}>
{item.name}
</Text>
</YStack>
</YStack>
))}
</XStack>
</YStack>
);
}
function TripUi() {
return (
<YStack gap={"$2"}>
<XStack alignItems={"flex-end"} justifyContent={"space-between"}>
<H5 color={"black"} fontWeight={"500"}>
Your trips
</H5>
<H3 color={"#838282"} fontSize={20}>
See all
</H3>
</XStack>
<XStack justifyContent={"space-between"} alignItems={"center"}>
<XStack gap={"$2"} alignItems={"center"}>
<SmallFork width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
So Fresh
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>1hr drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>4</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦12,000.00
</H6>
</XStack>
<XStack
justifyContent={"space-between"}
alignItems={"center"}
marginTop={"$4"}>
<XStack gap={"$2"} alignItems={"center"}>
<Ridesicon width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
Film house cinema
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>15mins drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>2.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦5,000.00
</H6>
</XStack>
<XStack
justifyContent={"space-between"}
alignItems={"center"}
marginTop={"$4"}>
<XStack gap={"$2"} alignItems={"center"}>
<Ridesicon width={34} height={34} />
<YStack gap={"$2"}>
<H6 color={"#000"} fontWeight={"500"}>
iFitness Gym
</H6>
<XStack gap={"$4"} alignItems={"center"}>
<XStack gap={"$2"}>
<ClockIcon width={16} height={16} />
<Text color={"#838282"}>10min drive</Text>
</XStack>
<XStack gap={"$2"}>
<StarIcon width={16} height={16} />
<Text color={"#838282"}>2.5</Text>
</XStack>
</XStack>
</YStack>
</XStack>
<H6 color={"#000"} fontWeight={"500"}>
₦3,000.00
</H6>
</XStack>
</YStack>
);
}
function HomeScreen() {
return (
<YStack paddingHorizontal={"$4"}>
<HeaderUI />
<SearchUI />
<NearbyUi />
<BannerUi />
<ProductUi />
<TripUi />
</YStack>
);
}
Распространенным вариантом использования Server Driven Rendering является управление рекламным контентом. Например, если наш BannerUi
представляет рекламный контент, который необходимо скрыть, вам просто нужно удалить или закомментировать его на сервере. Это изменение автоматически удалит рекламный контент из приложения React Native.
Развертывание сервера в производственную среду
Для развертывания сервера Rise мы будем использовать Render для хостинг.
Создать новый веб-сервис
Динамические обновления приложений React Native с сервера
Обновите файл modelSource.ts
в приложении React Native, чтобы использовать новую ссылку на службу рендеринга.
export const modelSource = createWSModelSource(
"https://my-render-link.com/:3015"
);
При любом обновлении основной ветки сервера сервер будет автоматически переразвернут. Это также вызовет автоматическое обновление приложения React Native.
Создание и тестирование мобильного приложения для производства
Чтобы собрать и запустить приложение iOS на физическом устройстве в конфигурации выпуска:
npx expo run:ios --configuration Release --device
Эта команда компилирует приложение с включенной оптимизацией и развертывает его на подключенном устройстве iOS.
Чтобы создать версию релиза вашего приложения Android:
npx expo run:android --variant release
Эта команда генерирует оптимизированный APK или App Bundle, подходящий для распространения и установки.
Обновите свой сервер и следите за процессом развертывания на Render. После завершения развертывания приложение обновится соответствующим образом.