Вы проснулись. Сияет солнце, щебечут птички. В мире никто ни с кем не воюет, никто не голодает, а один и тот же код можно использовать и в веб-проектах, и в нативных приложениях. Как бы было хорошо! К сожалению, на горизонте можно разглядеть лишь универсальный код, но путь к нему, даже сегодня, всё ещё полон неожиданностей.
Материал, перевод которого мы сегодня публикуем, представляет собой небольшое, но достаточно подробное руководство по разработке универсальных приложений с использованием React Native.
Зачем это всё?
Аббревиатура «PWA» (Progressive Web Apps, прогрессивные веб-приложения) сегодня у всех на слуху, этот трёхбуквенный акроним кажется прямо-таки китом в море технических терминов. Но и эта популярная технология всё ещё не лишена недостатков. С такими приложениями связано немало технологических сложностей, существуют ситуации, в которых разработчик вынужден параллельно создавать нативные и веб-приложения. Вот хорошая статья, в которой сравниваются PWA и нативные приложения.
Может быть, бизнесу стоит сосредоточить внимание лишь на нативных приложениях? Нет, не стоит. Это — большая ошибка. В результате логически оправданным шагом становится разработка единого универсального приложения. Это позволяет снизить затраты времени на разработку, уменьшить стоимость создания и поддержки проектов. Именно эти характеристики универсальных приложений и подвигли меня на один небольшой эксперимент.
Речь идёт об универсальном приложении-примере из сферы электронной коммерции для заказа еды. После эксперимента я создал шаблон для будущих проектов и для дальнейших исследований.
Papu — приложение для заказа еды, предназначенное для Android, iOS и для веба
Базовые строительные блоки приложения
Здесь мы пользуемся React, поэтому нам надо отделить логику приложения от пользовательского интерфейса. Тут лучше всего использовать какую-нибудь систему для управления состоянием приложения вроде Redux или Mobx. Подобный ход сразу же делает логику функционирования приложения универсальной. Её, без изменений, можно использовать на разных платформах.
Визуальная часть приложения — это уже другой разговор. Для того чтобы сконструировать интерфейс приложения, нужно иметь универсальный набор примитивов, базовых строительных блоков. Они должны работать и в вебе, и в нативной среде. К сожалению, язык веба и язык нативных платформ — это разные вещи.
Например, стандартный веб-контейнер обратится к вам так:
<div>Здравия желаю! Стандартный веб-контейнер к вашим услугам!</div>
А нативный — так:
<View>Привет! Я - простой контейнер в React Native</View>
Некоторые умные люди нашли выход из этой ситуации. Выходом стали специализированные библиотеки элементов. Одна из моих любимых — это замечательная библиотека React Native Web. Она не только берёт на себя заботу о базовых примитивах приложения, позволяя использовать компоненты React Native в вебе (не все компоненты!), но и даёт доступ к различным API React Native. Среди них — Geolocation
, Platform
, Animated
, AsyncStorage
и многие другие. Взгляните на замечательные примеры, которые можно найти в руководстве к этой библиотеке.
Шаблон
С примитивами мы разобрались. Но нам ещё нужно связать среды для веб-разработки и для нативной разработки. В моём проекте использованы create-react-app (для веб-приложения) и скрипт инициализации React Native (для нативного приложения, без Expo). Сначала я создал один проект такой командой: create-react-app rnw_web
. Затем создал второй проект: react-native init raw_native
. Затем я, последовав примеру Виктора Франкенштейна, взял файлы package.json
из этих двух проектов и объединил их. После этого я, в папке нового проекта, скормил новый файл yarn
. Вот о каком package
-файле идёт речь:
{
"name": "rnw_boilerplate",
"version": "0.1.0",
"private": true,
"dependencies": {
"react": "^16.5.1",
"react-art": "^16.5.1",
"react-dom": "^16.5.1",
"react-native": "0.56.0",
"react-native-web": "^0.9.0",
"react-navigation": "^2.17.0",
"react-router-dom": "^4.3.1",
"react-router-modal": "^1.4.2"
},
"devDependencies": {
"babel-jest": "^23.4.0",
"babel-preset-react-native": "^5",
"jest": "^23.4.1",
"react-scripts": "1.1.5",
"react-test-renderer": "^16.3.1"
},
"scripts": {
"start": "node node_modules/react-native/local-cli/cli.js start",
"test": "jest",
"start-ios": "react-native run-ios",
"start-web": "react-scripts start",
"build": "react-scripts build",
"test-web": "react-scripts test --env=jsdom",
"eject-web": "react-scripts eject"
}
}
Обратите внимание на то, что в этой версии файла нет навигационных возможностей. Далее, надо скопировать все файлы с исходным кодом из папок веб-приложения и нативного приложения в папку нового унифицированного проекта.
Папки, которые нужно скопировать в новый проект
Теперь, в папке src
, которая лежит в директории нового проекта, создадим два файла: App.js
и App.native.js
. Благодаря webpack мы можем использовать расширения имён файлов для того, чтобы сообщить бандлеру о том, где какие файлы нужно использовать. Разделить App
-файлы жизненно важно, так как мы собираемся использовать разные подходы для навигации по приложениям.
Вот файл App.js
, предназначенный для веба. Для навигации используется react-router
.
// App.js - WEB
import React, { Component } from "react";
import { View } from "react-native";
import WebRoutesGenerator from "./NativeWebRouteWrapper/index";
import { ModalContainer } from "react-router-modal";
import HomeScreen from "./HomeScreen";
import TopNav from "./TopNav";
import SecondScreen from "./SecondScreen";
import UserScreen from "./UserScreen";
import DasModalScreen from "./DasModalScreen";
const routeMap = {
Home: {
component: HomeScreen,
path: "/",
exact: true
},
Second: {
component: SecondScreen,
path: "/second"
},
User: {
component: UserScreen,
path: "/user/:name?",
exact: true
},
DasModal: {
component: DasModalScreen,
path: "*/dasmodal",
modal: true
}
};
class App extends Component {
render() {
return (
<View>
<TopNav />
{WebRoutesGenerator({ routeMap })}
<ModalContainer />
</View>
);
}
}
export default App;
Вот App.js
для React Native-приложения. Тут для навигации используется react-navigation
.
// App.js - React Native
import React, { Component } from "react";
import {
createStackNavigator,
createBottomTabNavigator
} from "react-navigation";
import HomeScreen from "./HomeScreen";
import DasModalScreen from "./DasModalScreen";
import SecondScreen from "./SecondScreen";
import UserScreen from "./UserScreen";
const HomeStack = createStackNavigator({
Home: { screen: HomeScreen, navigationOptions: { title: "Home" } }
});
const SecondStack = createStackNavigator({
Second: { screen: SecondScreen, navigationOptions: { title: "Second" } },
User: { screen: UserScreen, navigationOptions: { title: "User" } }
});
const TabNav = createBottomTabNavigator({
Home: HomeStack,
SecondStack: SecondStack
});
const RootStack = createStackNavigator(
{
Main: TabNav,
DasModal: DasModalScreen
},
{
mode: "modal",
headerMode: "none"
}
);
class App extends Component {
render() {
return <RootStack />;
}
}
export default App;
Вот так я создал простой шаблон приложения и подготовил платформу для дальнейшей работы. Можете попробовать данный шаблон, заглянув в этот репозиторий.
Сейчас мы немного этот шаблон усложним, добавим систему маршрутизации/навигации.
Проблемы навигации и их решения
Приложению, если только оно не состоит из одного экрана, нужна некая система навигации. Сейчас (речь идёт о сентябре 2018 года) существует лишь одна такая универсальная рабочая система, подходящая и для веба, и для нативных приложений. Речь идёт о React Router. Для веба это решение подходит хорошо, а вот в случае с React Native-проектами всё уже не так однозначно.
В React Router Native нет переходов между экранами, нет поддержки кнопки Назад (для платформы Android), нет модальных экранов, навигационных панелей и других возможностей. Другие средства навигации, вроде React Navigation, этими возможностями обладают.
Я использовал именно эту библиотеку, но вы можете подобрать что-нибудь другое. Поэтому в моём проекте за навигацию в веб-приложении отвечает React Router, а за навигацию в нативном приложении — React Navigation. Это, правда, создаёт новую проблему. Дело в том, что подходы к навигации и к передаче параметров в этих системах очень сильно различаются.
Для того чтобы сохранить дух React Native Web, используя везде подход, близкий к тому, что применяется в нативных приложениях, я подошёл к решению этой проблемы, создавая веб-маршруты и оборачивая их в HOC. Это дало мне возможность создать API, напоминающее React Navigation.
Такой подход позволил перемещаться между экранами веб-приложения, избавив меня от необходимости создавать отдельные компоненты для двух видов приложения.
Первый шаг реализации этого механизма заключается в создании объекта с описанием маршрутов для веб-приложения:
import WebRoutesGenerator from "./NativeWebRouteWrapper"; // функция собственной разработки, которая генерирует маршруты React Router и оборачивает их в HOC
const routeMap = {
Home: {
screen: HomeScreen,
path: '/',
exact: true
},
Menu: {
screen: MenuScreen,
path: '/menu/sectionIndex?'
}
}
//в методе render
<View>
{WebRoutesGenerator({ routeMap })}
</View>
В сущности, тут представлена копия функции создания навигатора React Navigation с добавлениями специфических для React Router возможностей.
Далее, пользуясь моей вспомогательной функцией, я создаю маршруты react-router
и оборачиваю их в HOC. Это позволяет клонировать компонент screen
и добавить navigation
в его свойства. Этот подход имитирует поведение React Navigation и делает доступными методы вроде navigate()
, goBack()
, getParam()
.
Модальные экраны
React Navigation, благодаря createStackNavigator
, даёт возможность сделать так, чтобы некая страница приложения выезжала бы снизу в виде модального экрана. Для того чтобы добиться подобного в веб-приложении, мне пришлось использовать библиотеку React Router Modal. Для работы с модальным экраном сначала надо добавить соответствующую опцию в объект routeMap
:
const routeMap = {
Modal: {
screen: ModalScreen,
path: '*/modal',
modal: true //маршрутизатор будет использовать компонент ModalRoute для рендеринга этого маршрута
}
}
Кроме того, в макет приложения нужно добавить компонент <ModalContainer />
из библиотеки react-router-modal
. Выводиться соответствующая страница будет именно там.
Навигация между экранами
Благодаря HOC нашей разработки (временно этот компонент называется NativeWebRouteWrapper
, и это, кстати, ужасное имя), мы можем использовать практически тот же набор функций, что и в React Navigation, для организации перемещения между страницами в веб-версии приложения:
const { product, navigation } = this.props
<Button
onPress={navigation.navigate('ProductScreen', {id: product.id})}
title={`Go to ${product.name}`}
/>
<Button
onPress={navigation.goBack}
title="Go Back"
/>
Возврат к предыдущему экрану
В React Navigation можно вернуться назад на n
экранов, которые находятся в навигационном стеке. Подобное в React Router недоступно, тут нет навигационного стека. Для того чтобы решить эту проблему, нам нужно импортировать в код функцию pop
собственной разработки. Вызывая её, передадим ей несколько параметров:
import pop from '/NativeWebRouteWrapper/pop'
render() {
const { navigation } = this.props
return (
<Button
onPress={pop({screen: 'FirstScreen', n: 2, navigation})}
title="Go back two screens"
/>
)
}
Опишем эти параметры:
screen
— имя экрана (используемое React Router в веб-версии приложения).n
— число экранов для возврата с использованием стека (используется React Navigation).navigation
— объект, обеспечивающий навигацию.
Результаты работы
Если вы хотите поэкспериментировать с представленной здесь идеей разработки универсальных приложений — я создал два шаблона.
Первый представляет собой чистую универсальную среду для разработки веб-приложений и нативных приложений.
Второй — это, в сущности, первый шаблон, который расширен за счёт моей системы для организации навигации по приложению.
Вот демонстрационное приложение papu, основанное на высказанных выше соображениях. Оно полно ошибок и тупиков, но вы можете самостоятельно его собрать, запустить его в браузере и на мобильном устройстве, и на практике получить представление о том, как всё это работает.
Итоги
Сообществу React-разработчиков, безусловно, нужна универсальная библиотека для организации навигации по приложениям, так как это упростит разработку проектов, подобных тому, о котором мы говорили. Очень хорошо было бы, если бы библиотека React Navigation работала бы и в вебе (на самом деле, подобная возможность уже доступна, но работа с ней не лишена сложностей).
Уважаемые читатели! Пользуетесь ли вы возможностями React при создании универсальных приложений?
Автор: ru_vds