Руководство по разработке Web-приложений на React Native +22


Вы проснулись. Сияет солнце, щебечут птички. В мире никто ни с кем не воюет, никто не голодает, а один и тот же код можно использовать и в веб-проектах, и в нативных приложениях. Как бы было хорошо! К сожалению, на горизонте можно разглядеть лишь универсальный код, но путь к нему, даже сегодня, всё ещё полон неожиданностей.

image

Материал, перевод которого мы сегодня публикуем, представляет собой небольшое, но достаточно подробное руководство по разработке универсальных приложений с использованием 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 при создании универсальных приложений?




К сожалению, не доступен сервер mySQL