Дизайн архитектуры приложения React Native App

React.js Immutable.js React Native Redux

Представлено в предыдущей статьеСоздание среды разработки React Native, мы можем успешно запустить приложение helloword локально. В этом разделе начнется подробный анализ того, как построить архитектуру приложения React Native App и обеспечить полную предварительную версию локального запуска.

Полный код смотрите на github

Добро пожаловать в мой личный блог

предисловие

Уже есть много инструментов для строительных лесов, таких какignite, поддерживает создание структуры проекта React Native App в один клик, что очень удобно, но, пользуясь удобством, он также лишается возможности полностью изучить структуру проекта и стек технологий, и обычно архитектура технологии приложения, созданная с помощью скаффолдинга, не может полностью соответствуют нашим требованиям бизнеса, нам нужно изменять и улучшать себя, поэтому, если вы хотите иметь более глубокий контроль над структурой проекта, лучше всего понять проект от 0 до 1.

Структура проекта и стек технологий

первое использованиеreact-native-cliИнструменты для создания приложения React Native:

react-native init fuc

Сгенерированная структура проекта выглядит следующим образом:

RN项目初始结构
Начальная структура проекта РН

  1. Каталоги android и ios хранят соответствующий собственный код платформы соответственно;
  2. package.jsonУправление файлами для зависимостей проекта;
  3. index.ios.jsВходной файл для платформы ios,index.android.jsФайл входа для платформы Android, обычно используемый для регистрации корневого компонента React Native App;
  4. .babelrcфайл, файл конфигурации babel, React Native по умолчанию использует babel для компиляции кода JavaScript;
  5. __tests__Каталог тестов проекта.

Мы видим, что нет каталога для хранения нативного JavaScript-кода React Native, который нужно создавать самим, обычно создаваяsrcКаталог используется в качестве корневого каталога всех кодов и ресурсов в части Javascript приложения App.src/constantsкаталог для хранения глобально общих постоянных данных,src/configКаталог содержит глобальную конфигурацию,src/helpersХраните глобальный помощник, методы служебного класса,src/app.jsКак часть файла записи RN обычно необходимо создать каталог для сохранения redux каждого модуля, каталог для промежуточного ПО redux и так далее.

стек технологий

Построение архитектуры проекта во многом зависит от стека технологий проекта, поэтому сначала проанализируйте весь стек технологий и подведите итоги:

  1. реагировать родной + реактивная библиотека является предпосылкой проекта
  2. Навигация по приложению (отличается от концепции маршрутизации приложения React)
  3. Контейнер управления состоянием приложения
  4. Вам нужны неизменяемые данные
  5. Постоянство состояния приложения
  6. Асинхронное управление задачами
  7. Тесты и вспомогательные инструменты или функции
  8. Инструменты разработки и отладки

В соответствии с приведенным выше разделением для формирования полного стека технологий проекта выбираются следующие сторонние библиотеки и инструменты:

  1. реактивная библиотека + реакционная библиотека классов;
  2. react-navigation управляет навигацией по приложению;
  3. Redux действует как контейнер состояния JavaScript, а react-redux соединяет приложения React Native с redux;
  4. Immutable.js поддерживает неизменяемое состояние, redux-immutable делает неизменяемым все дерево состояний хранилища redux;
  5. Используйте redux-persist для поддержки постоянства дерева состояний redux и добавьте расширение redux-persist-immutable для поддержки постоянства дерева состояний Immutable;
  6. Используйте redux-saga для управления асинхронными задачами в приложении, такими как сетевые запросы, асинхронное чтение локальных данных и т. д.;
  7. Используйте Jest Integrated Application Test, используйте Lodash, RAMDA и т. д. Дополнительный вторичный класс, библиотеки классов инструментов;
  8. Используйте инструмент отладки Reactotron

С учетом приведенного выше анализа улучшенная структура проекта показана на рисунке:

RN项目结构
Структура проекта РН

Как показано выше, создайте его в корневом каталоге проекта.srcкаталог и создайте 12 каталогов и 1 js-файл частичной записи React Native по очереди в каталоге src.

Инструменты разработки и отладки

Разработка приложений React Native в настоящее время имеет множество инструментов отладки, обычно используемых, таких как атом иNuclide, инструмент отладки, поставляемый с мобильным симулятором,ReactronЖдать.

Nuclide

Nuclideпредставляет собой интегрированную среду разработки на основе атома, предоставляемую Facebook, которую можно использовать для написания,бегатьа такжеотладкаПриложение React Native.

Средство отладки симулятора

После того, как эмулятор запустится и запустит приложение, автоматически откроется браузер.http://localhost:8081/debugger-uiстраницы, вы можете выполнять вывод отладки js и удаленную отладку точки останова js в консоли; используйте сочетания клавиш в терминале симулятораcommandДобавитьDключ для открытия инструментов отладки, включая перезагрузку приложения, включение горячей перезагрузки, переключение инспекторов DOM и т. д.:

RN应用调试工具
Инструмент отладки приложений RN

Reactotron

ReactotronЭто настольное приложение для кросс-платформенной отладки приложений React и React Native. Оно может динамически отслеживать и выводить информацию, такую ​​​​как избыточность, действие и асинхронные запросы саги для приложений React, в режиме реального времени, как показано на рисунке:

Reactotron
Reactotron

Сначала инициализируйте конфигурацию, связанную с Reactotron:

import Config from './DebugConfig';
import Immutable from 'immutable';
import Reactotron from 'reactotron-react-native';
import { reactotronRedux as reduxPlugin } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga';

if (Config.useReactotron) {
  // refer to https://github.com/infinitered/reactotron for more options!
  Reactotron
    .configure({ name: 'Os App' })
    .useReactNative()
    .use(reduxPlugin({ onRestore: Immutable }))
    .use(sagaPlugin())
    .connect();

  // Let's clear Reactotron on every time we load the app
  Reactotron.clear();

  // Totally hacky, but this allows you to not both importing reactotron-react-native
  // on every file.  This is just DEV mode, so no big deal.
  console.tron = Reactotron;
}

затем включитеconsole.tron.overlayМетод расширяет входной компонент:

import './config/ReactotronConfig';
import DebugConfig from './config/DebugConfig';

class App extends Component {
  render () {
    return (
      <Provider store={store}>
        <AppContainer />
      </Provider>
    )
  }
}

// allow reactotron overlay for fast design in dev mode
export default DebugConfig.useReactotron
  ? console.tron.overlay(App)
  : App

На этом этапе вы можете использовать клиент Reactotron для захвата всех избыточных данных и действий, инициированных в приложении.

Разделение компонентов

Приложения React Native по-прежнему следуют принципу разработки компонентов React. Компоненты отвечают за визуализацию пользовательского интерфейса. Разные состояния компонентов соответствуют разным пользовательским интерфейсам. Обычно следуют следующим идеям дизайна компонентов:

  1. Компоненты макета: компоненты, которые включают только структуру интерфейса пользовательского интерфейса приложения, не включают какую-либо бизнес-логику, запросы данных и операции;
  2. Компонент-контейнер: отвечает за получение данных, обработку бизнес-логики и обычно возвращает презентационные компоненты в функции render();
  3. Компонент отображения: отвечает за отображение пользовательского интерфейса приложения;
  4. Компоненты пользовательского интерфейса: относятся к абстрактным многократно используемым независимым компонентам пользовательского интерфейса, обычно компонентам без состояния;
Компоненты дисплея компонент контейнера
Цель Представление пользовательского интерфейса (структура и стиль HTML) Бизнес-логика (получение данных, обновление статуса)
Осведомлен о Редукс без имеют
Источники данных props Подпишитесь на магазин Redux
изменить данные Вызовите функцию обратного вызова, переданную реквизитами Dispatch Redux actions
многоразовый Сильная независимость Высокая степень связанности бизнеса

Кроссплатформенная адаптация

При создании кроссплатформенного приложения, хотя в React Native была проделана большая работа по кроссплатформенной совместимости, все еще бывают ситуации, когда для разных платформ необходимо разрабатывать разный код, и в это время требуется дополнительная обработка.

Кросс-платформенный каталог

Мы можем различать разные файлы кода платформы в разных каталогах, например:

/common/components/
/android/components/
/ios/components/

commonОбщие файлы хранятся в каталоге,androidВ каталоге хранится код файла Android,iosСохраните код файла ios, но обычно выбирайте лучший метод, предоставляемый React Native, который будет представлен позже.

Платформенный модуль

В React Native есть встроенный модуль Platform, позволяющий различать текущую запущенную платформу приложения.Platform.OSзначениеios, работает на платформе Androidandroid, вы можете использовать этот модуль для загрузки соответствующего файла платформы:

var StatusBar = Platform.select({
  ios: () => require('ios/components/StatusBar'),
  android: () => require('android/components/StatusBar'),
})();

Затем используйте компонент StatusBar как обычно.

Обнаружение платформы React Native

Когда на компонент ссылаются, React Native проверит, существует ли файл.androidили.iosСуффикс, если он существует, загрузите соответствующий файловый компонент в соответствии с текущей платформой, например:

StatusBar.ios.js
StatusBar.indroid.js

Если указанные выше два файла существуют в одном и том же каталоге, на них можно ссылаться следующими способами:

import StatusBar from './components/StatusBar';

React загрузит соответствующий файл суффикса в соответствии с текущей платформой.Рекомендуется использовать этот метод для адаптации кода на уровне компонентов платформы, а для небольшой части кода, который необходимо адаптировать к платформе, вы можете использоватьPlatform.OSЗначение выглядит следующим образом, если вам нужно только добавить более высокое значение margin-top для платформы ios, и это не общедоступный стиль:

var styles = StyleSheet.create({
  marginTop: (Platform.OS === 'ios') ? 20 : 10,
});

Навигация по приложениям и маршрутизация

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

Навигация и маршрутизация

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

Возможно, вы уже полагались на конфигурацию маршрутизации одностраничного приложения react-router и хотите создать кроссплатформенное приложение на основе URL, TOEFL активен в сообществе с открытым исходным кодом, вы можете использоватьreact-router-native, но не рекомендуется, так как для Приложения с учетом взаимодействия и опыта больше подходит использование многостраничной (экранной) формы.

react-navigation

Используя реагирующую навигацию, вы можете определить структуру навигации кроссплатформенного приложения, а также поддерживать настройку и визуализацию кроссплатформенных панелей навигации, панелей вкладок и других компонентов.

Встроенный навигационный модуль.

react-navigation предоставляет следующие методы для поддержки создания различных типов навигации:

  1. StackNavigator: Создать стек (стек) навигационных экранов, все экраны (экраны) существуют в виде стеков, отображать по одному экрану за раз, улучшать анимацию преобразования при переключении экранов и размещать экран на вершине стека при экран открыт;
  2. TabNavigator: создайте навигацию в стиле вкладок, визуализируйте строку меню вкладок, чтобы пользователи могли переключаться между различными экранами;
  3. DrawerNavigator: создание навигации по ящикам, выдвижение экрана из левой части экрана;

StackNavigator

StackNavigatorПоддерживайте кросс-платформенное переключение между различными экранами преобразующим образом и размещайте текущий экран на вершине стека.Метод вызова выглядит следующим образом:

StackNavigator(RouteConfigs, StackNavigatorConfig)
RouteConfigs

Объект конфигурации маршрутизации (маршрута) стека навигации, определите имя маршрута и объект маршрута, объект маршрута определяет компонент отображения, соответствующий текущему маршруту, например:

// routes为路由信息对象
StackNavigator({
  [routes.Main.name]: Main,
  [routes.Login.name]: {
    path: routes.Login.path,
    screen: LoginContainer,
    title: routes.Login.title
  }
}

Как и выше, указывая, что когда приложение переходит к маршрутуroutes.Login.nameпри рендерингеLoginContainerКомпонент, заданный свойством экрана объекта, при переходе к маршрутуroutes.Main.nameзначение, соответствующее рендерингу MainStack, в кодеMainОбъект:

{
  path: routes.Main.path,
  screen: MainStack,
  navigationOptions: {
    gesturesEnabled: false,
  },
}

А MainStack — это Stacknavigator:

const MainStack = StackNavigator({
  Home: HomeTabs
})

HomeTabs — это TabNavigator:

{
  name: 'Home Tabs',
  description: 'Tabs following Home Tabs',
  headerMode: 'none',
  screen: HomeTabs
};
StackNavigatorConfig

Объект конфигурации маршрутизации, вы можете дополнительно настроить дополнительные атрибуты, такие как:

  1. initialRouteName, экран по умолчанию начального стека навигации должен быть именем ключа в объекте конфигурации маршрутизации;
  2. initialRouteParams, параметры исходного маршрута по умолчанию;
  3. navigationOptions, установите конфигурацию экрана навигации по умолчанию;
    1. заголовок: заголовок в верхней части навигационного экрана;
  4. headerMode, показывать ли верхнюю панель навигации:
    1. нет: не отображать панель навигации;
    2. float: отображает независимую панель навигации вверху и сопровождается анимацией при переключении экранов, обычно это режим отображения ios;
    3. экран: привяжите панель навигации для каждого экрана, а также постепенное появление и исчезновение при переключении экрана, обычно в режиме отображения Android;
  5. mode, стиль и эффект трансформации при навигации по экрану переключения:
    1. card: режим по умолчанию, стандартное преобразование экрана;
    2. modal: действует только на платформе ios, сделайте так, чтобы нижняя часть экрана выдвигалась из нового экрана;
{
  initialRouteName: routes.Login.name,
  headerMode: 'none', // 去除顶部导航栏
  /**
   * Use modal on iOS because the card mode comes from the right,
   * which conflicts with the drawer example gesture
   */
  mode: Platform.OS === 'ios' ? 'modal' : 'card'
}

TabNavigator

использоватьTabNavigatorВы можете создать экран, с помощью TabRouter вы можете переключаться между различными вкладками, метод вызова следующий:

TabNavigator(RouteConfigs, TabNavigatorConfig)
RouteConfigs

Объект конфигурации маршрутизации Tab, формат аналогичен StackNavigator.

TabNavigatorConfig

Объекты конфигурации, связанные с навигацией по вкладкам, такие как:

  1. tabBarComponent: компонент, используемый строкой меню вкладок, который используется по умолчанию на платформе ios.TabBarBottomКомпонент, используемый по умолчанию на платформе Android.TabBarTopкомпоненты;
  2. tabBarPosition: положение строки меню вкладки,topилиbottom;
  3. tabBarOptions: конфигурация строки меню вкладки:
    1. activeTintColor: цвет шрифта и значка элемента строки меню активной вкладки.
  4. InitialRouteName: routeName маршрута tabRoute по умолчанию во время начальной загрузки, что соответствует имени ключа объекта конфигурации маршрутизации.
  5. порядок: сортировка вкладок, массив, состоящий из routeName;
const HomeTabs = TabNavigator(
  {
    Notification: {
      screen: NotificationTabContainer,
      path: 'notification',
      navigationOptions: {
        title: '消息通知'
      }
    },
    Myself: {
      screen: MyselfTabContainer,
      path: 'myself',
      navigationOptions: {
        title: '我的'
      }
    }
  },
  {
    tabBarOptions: {
      activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',
    },
    swipeEnabled: true
  }
);

DrawerNavigator

использоватьDrawerNavigatorВы можете создать экран навигации по ящикам, который называется следующим образом:

DrawerNavigator(RouteConfigs, DrawerNavigatorConfig)
const MyDrawer = DrawerNavigator({
  Home: {
    screen: MyHomeDrawerScreen,
  },
  Notifications: {
    screen: MyNotificationsDrawerScreen,
  },
});
RouteConfigs

Объект конфигурации маршрутизации навигации по выдвижным ящикам, формат аналогичен StackNavigator.

DrawerNavigatorConfig

Объекты конфигурации навигационного ящика, такие как:

  1. drawerWidth: ширина экрана ящика;
  2. drawerPosition: положение экрана ящика,leftилиright;
  3. contentComponent: компонент содержимого экрана Drawer, такой как встроенныйDrawerItems;
  4. initialRouteName: имя исходного маршрута;
import { DrawerItems } from 'react-navigation';

const CustomDrawerContentComponent = (props) => (
  <View style={styles.container}>
    <DrawerItems {...props} />
  </View>
);

const DrawerNavi = DrawerNavigator({}, {
  drawerWidth: 200,
  drawerPosition: 'right',
  contentComponent: props => <CustomDrawerContentComponent  {...props}/>,
  drawerBackgroundColor: 'transparent'
})

Navigation prop

Каждый экран приложения RN будет принимать свойство навигации, содержащее следующие методы и свойства:

  1. navigation: вспомогательный метод для перехода к другим экранам;
  2. setParams: метод изменения параметров маршрутизации;
  3. goBack: закрыть текущий экран и вернуться назад;
  4. состояние: текущее состояние экрана или информация о маршрутизации;
  5. рассылка: опубликовать действие;
navigate

Используйте метод навигации для перехода к другим экранам:

navigate(routeName, params, action)
  1. routeName: имя целевого маршрута, имя ключа маршрута, зарегистрированное в маршруте навигации приложения;
  2. params: параметры целевого маршрута;
  3. действие: если у целевого маршрута есть подмаршрут, выполните это действие на подмаршруте;
setParams

Измените текущую информацию о маршруте навигации, например, задайте и измените заголовок навигации и другую информацию:

class ProfileScreen extends React.Component {
  render() {
    const { setParams } = this.props.navigation;
    return (
      <Button
        onPress={() => setParams({name: 'Jh'})}
        title="Set title"
      />
     )
   }
}
goBack

Перейти назад от текущего экрана (параметр пуст) или указанного экрана (параметр является именем ключа маршрутизации экрана) к предыдущему экрану экрана и закрыть экран; если переданоnullпараметра исходный экран не указывается, то есть экран не будет выключен.

state

Каждый экран имеет свою собственную информацию о маршрутизации, доступ к которой можно получить черезthis.props.navigation.stateдоступ, формат возвращаемых данных следующий:

{
  // the name of the route config in the router
  routeName: 'Login',
  //a unique identifier used to sort routes
  key: 'login',
  //an optional object of string options for this screen
  params: { user: 'jh' }
}
dispatch

Этот метод используется для распределения действий навигации по маршрутам для реализации навигации.react-navigationФункция создания действий предусмотрена по умолчаниюNavigationActions, следующим образом, чтобы распределить действие экрана переключателя навигации:

import { NavigationActions } from 'react-navigation'

const navigateAction = NavigationActions.navigate({
  routeName: routeName || routes.Login.name,
  params: {},
  // navigate can have a nested navigate action that will be run inside the child router
  action: NavigationActions.navigate({ routeName: 'Notification'})
});

// dispatch the action
this.props.navigation.dispatch(navigateAction);

Навигация и Редукс

После использования Redux нужно следовать принципу redux: единый доверенный источник данных, то есть все источники данных могут быть только reudx store, и состояние маршрутизации Navigation не должно быть исключением, поэтому нужно подключить состояние Navigation с состоянием хранилища вы можете создать редуктор навигации, чтобы объединить состояние навигации в хранилище:

import AppNavigation from '../routes';

const NavigationReducer = (state = initialState, action) => {
  const newState = Object.assign({}, state, AppNavigation.router.getStateForAction(action, state));
  return newState || state;
};

export const NavigationReducers = {
  nav: NavigationReducer
};

Все, что делает этот редьюсер, — это объединяет состояние маршрутизации навигации приложения в хранилище.

Redux

Если в каком-либо современном крупномасштабном веб-приложении отсутствует контейнер управления состоянием, то приложению не хватает характеристик времени.Дополнительные библиотеки, такие как mobx, redux и т. д., на самом деле похожи, и каждая берет то, что им нужно. например, redux является наиболее часто используемой библиотекой контейнеров состояния приложения React, которая также подходит для приложений React Native.

react-redux

Как и в случае с приложением React, вам необходимо соединить Redux и приложение для унифицированного управления состоянием приложения с помощью redux и использовать официально предоставленныйreact-reduxбиблиотека.

class App extends Component {
  render () {
    return (
      <Provider store={store}>
        <AppContainer />
      </Provider>
    )
  }
}

createStore

используя предоставленный редуксcreateStoreметод для создания хранилища избыточности, но в реальных проектах нам часто нужно расширить избыточность, чтобы добавить некоторые пользовательские функции или службы, такие как добавление промежуточного программного обеспечения избыточности, добавление саги об асинхронном управлении задачами, улучшение избыточности и т. д.:

// creates the store
export default (rootReducer, rootSaga, initialState) => {
  /* ------------- Redux Configuration ------------- */
  const middleware = [];
  const enhancers = [];

  /* ------------- Analytics Middleware ------------- */
  middleware.push(ScreenTracking);

  /* ------------- Saga Middleware ------------- */
  const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
  const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
  middleware.push(sagaMiddleware);

  /* ------------- Assemble Middleware ------------- */
  enhancers.push(applyMiddleware(...middleware));

  /* ------------- AutoRehydrate Enhancer ------------- */
  // add the autoRehydrate enhancer
  if (ReduxPersist.active) {
    enhancers.push(autoRehydrate());
  }

  // if Reactotron is enabled (default for __DEV__), 
  // we'll create the store through Reactotron
  const createAppropriateStore = Config.useReactotron ? console.tron.createStore : createStore;
  const store = createAppropriateStore(rootReducer, initialState, compose(...enhancers));

  // configure persistStore and check reducer version number
  if (ReduxPersist.active) {
    RehydrationServices.updateReducers(store);
  }

  // kick off root saga
  sagaMiddleware.run(rootSaga);

  return store;
}

редукс и неизменяемый

redux предоставляет по умолчаниюcombineReducersМетод интегрирует редюсеры в избыточность, однако метод по умолчанию предполагает принимать собственные объекты JavaScript и обрабатывает состояние как собственные объекты, поэтому, когда мы используемcreateStoreметод и принимает неизменяемый объект в качестве начального состояния приложения,reducerБудет возвращена ошибка, исходный код выглядит следующим образом:

if   (!isPlainObject(inputState)) {
    return   (                              
        `The   ${argumentName} has unexpected type of "` +                                    ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +
      ".Expected argument to be an object with the following + 
      `keys:"${reducerKeys.join('", "')}"`   
    )  
}

Как показано выше, параметр состояния, принимаемый редуктором примитивного типа, должен быть нативным объектом JavaScript, нам нужноcombineReducersОн улучшен, чтобы он мог обрабатывать неизменяемые объекты,redux-immutableт. е. обеспечивает возможность созданияImmutable.jsСовместный РедуксcombineReducers.

import { combineReducers } from 'redux-immutable';
import Immutable from 'immutable';
import configureStore from './CreateStore';

// use Immutable.Map to create the store state tree
const initialState = Immutable.Map();

export default () => {
  // Assemble The Reducers
  const rootReducer = combineReducers({
    ...NavigationReducers,
    ...LoginReducers
  });

  return configureStore(rootReducer, rootSaga, initialState);
}

Как и в приведенном выше коде, вы можете видеть, что мы передалиinitialStateЯвляетсяImmutable.MapТип данных, мы неизменяем корень всего дерева состояний redux и передаем редюсеры и саги, которые могут обрабатывать неизменяемое состояние.

Кроме того, данные каждого узла дерева состояний представляют собой неизменяемую структуру, напримерNavigationReducer:

const initialState = Immutable.fromJS({
  index: 0,
  routes: [{
    routeName: routes.Login.name,
    key: routes.Login.name
  }]
});

const NavigationReducer = (state = initialState, action) => {
  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));
  return newState || state;
};

Узел состояния редуктора по умолчанию использует метод Immutable.fromJS() для преобразования его в структуру Immutable и использует метод Immutable при обновлении состояния.state.merge(), чтобы гарантировать, что состояние является однородным и предсказуемым.

избыточное постоянство

Мы знаем, что браузер имеет функцию кэширования ресурсов по умолчанию и предоставляет методы локального постоянного хранения, такие как localStorage, indexDb, webSQL и т. д., обычно некоторые данные могут храниться локально, и в определенный период, когда пользователь снова обращается, они прямое восстановление из локальных данных может значительно повысить скорость запуска приложения и повысить удобство работы пользователей.Для приложений-приложений общим требованием является сохранение некоторых данных запуска локально или даже в автономных приложениях.Мы можем использоватьAsyncStorage(Похоже на локальное хранилище в Интернете) для хранения некоторых данных, если это большой объем хранилища данных, вы можете использовать SQLite.

Кроме того, в отличие от предыдущего прямого хранения данных, при запуске приложения данные считываются локально, а затем восстанавливаются.Для redux-приложений, если хранятся только данные, то мы должны расширять для каждого редюсера, а читать и сохранять, когда приложение запускается заново.Это громоздкий и неэффективный способ.Можете ли вы попробовать сохранить ключ редьюсера, а затем восстановить соответствующие постоянные данные по ключу?Сначала зарегистрируйте редуктор Rehydrate, при срабатывании действия восстановите данные в соответствии с его ключом редуктора, а затем нужно только распространять действия при запуске приложения, что также легко абстрагируется в настраиваемую службу расширения.redux-persistВсе это сделано для нас.

redux-persist

Чтобы реализовать персистентность избыточности, включая два процесса локального постоянного хранилища хранилища избыточности и восстановления и запуска, если вы пишете свою собственную реализацию, объем кода усложняется, вы можете использовать библиотеки с открытым исходным кодом.redux-persist, который обеспечиваетpersistStoreа такжеautoRehydrateМетоды соответственно сохраняют локальное хранилище и восстанавливают стартовое хранилище, а также поддерживают трансформацию и расширение состояния хранилища при настройке входящего сохраняемости и восстановлении хранилища.

постоянное хранилище

Службы, связанные с persistStore, вызываются при создании хранилища следующим образом:RehydrationServices.updateReducers():

// configure persistStore and check reducer version number
if (ReduxPersist.active) {
  RehydrationServices.updateReducers(store);
}

Постоянное хранилище хранилища реализовано в этом методе:

// Check to ensure latest reducer version
AsyncStorage.getItem('reducerVersion').then((localVersion) => {
  if (localVersion !== reducerVersion) {
    if (DebugConfig.useReactotron) {
      console.tron.display({
        name: 'PURGE',
        value: {
          'Old Version:': localVersion,
          'New Version:': reducerVersion
        },
        preview: 'Reducer Version Change Detected',
        important: true
      });
    }
    // Purge store
    persistStore(store, config, startApp).purge();
    AsyncStorage.setItem('reducerVersion', reducerVersion);
  } else {
    persistStore(store, config, startApp);
  }
}).catch(() => {
  persistStore(store, config, startApp);
  AsyncStorage.setItem('reducerVersion', reducerVersion);
})

Номер версии редуктора будет храниться в AsyncStorage, который можно настроить в файле конфигурации приложения. Номер версии и хранилище будут сохранены при первом выполнении персистентности. Если номер версии редуктора изменится, исходное сохраненное хранилище будет сохранено. очищен, иначе хранилище будет передано для метода сохраненияpersistStoreВот и все.

persistStore(store, [config, callback])

Этот метод в основном реализует персистентность хранилища и распределяет действие регидратации:

  1. Подпишитесь на хранилище избыточности и запускайте операции хранения хранилища при его изменении;
  2. Получите данные из указанного StorageEngine (например, AsyncStorage), преобразуйте их, а затем инициируйте процесс REHYDRATE, отправив действие REHYDRATE;

Параметры приема в основном следующие:

  1. магазин: постоянный магазин;
  2. config: объект конфигурации
    1. хранилище: механизм сохранения, такой как LocalStorage и AsyncStorage;
    2. transforms: Преобразователи, вызываемые на этапах регидратации и хранения;
    3. черный список: массив черных списков, указывающий ключи редукторов, которые игнорируются постоянством;
  3. обратный вызов: обратный вызов после завершения операции обезвоживания;

возобновить загрузку

Как и persisStore, расширение rehydrate по-прежнему инициализируется и регистрируется при создании хранилища избыточности:

// add the autoRehydrate enhancer
if (ReduxPersist.active) {
  enhancers.push(autoRehydrate());
}

Функция, реализованная этим методом, очень проста, то есть, используя постоянные данные для регидратации данных в хранилище, она фактически регистрирует редюсер autoRehydarte, который получит действие регидратации, распределенное предыдущим методом persistStore, а затем объединит состояние.

Конечно, autoRehydrate не нужен, мы можем настроить способ восстановления магазина:

import {REHYDRATE} from 'redux-persist/constants';

//...
case REHYDRATE:
  const incoming = action.payload.reducer
  if (incoming) {
    return {
      ...state,
      ...incoming
    }
  }
  return state;

обновление новой версии

Следует отметить, что библиотека redux-persist была выпущена до версии 5.x, аv4.xпреобладают, в новой версии есть некоторые обновления,Подробнее, пожалуйста, нажмите для просмотра.

Постоянство и неизменность

Интеграция Redux и Immutable упоминалась ранее.Использованный выше redux-persist по умолчанию может обрабатывать только состояние хранилища нативных объектов JavaScript, поэтому его необходимо расширить, чтобы он был совместим с Immutable.

redux-persist-immutable

использоватьredux-persist-immutableБиблиотеки могут легко добиться совместимости, все, что они делают, это используют предоставленныйpersistStoreМетод заменяет метод, предоставляемый redux-persist:

import { persistStore } from 'redux-persist-immutable';

transform

Мы знаем, что при сохранении хранилища лучше ориентироваться на нативные объекты JavaScript, потому что обычно неизменяемые структурированные данные имеют много вспомогательной информации и их непросто хранить, поэтому необходимо определить операции преобразования при сохранении и восстановлении данных:

import R from 'ramda';
import Immutable, { Iterable } from 'immutable';

// change this Immutable object into a JS object
const convertToJs = (state) => state.toJS();

// optionally convert this object into a JS object if it is Immutable
const fromImmutable = R.when(Iterable.isIterable, convertToJs);

// convert this JS object into an Immutable object
const toImmutable = (raw) => Immutable.fromJS(raw);

// the transform interface that redux-persist is expecting
export default {
  out: (state) => {
    return toImmutable(state);
  },
  in: (raw) => {
    return fromImmutable(raw);
  }
};

Как и выше, вход и выход в выходном объекте соответствуют операциям преобразования при сохранении и восстановлении данных соответственно.fromJS()а такжеtoJS()Преобразуйте Js и неизменяемые структуры данных, используйте их следующим образом:

import immutablePersistenceTransform from '../services/ImmutablePersistenceTransform'
persistStore(store, {
  transforms: [immutablePersistenceTransform]
}, startApp);

Immutable

После внедрения Immutable в проект нужно максимально обеспечить следующие моменты:

  1. Unified Immutable всего дерева состояний хранилища избыточности;
  2. Постоянство Redux совместимо с неизменяемыми данными;
  3. Навигация по приложениям совместима с Immutable;

Неизменяемость и навигация по приложениям

Первые два пункта были объяснены в предыдущих двух разделах. Третий пункт заключается в том, что Навигация совместима с неизменяемым. На самом деле, это нужно сделать состояние маршрутизации навигации совместимым с неизменяемым. В разделе Навигация и маршрутизация приложений это было представил, как подключить состояние маршрутизации навигации к хранилищу Redux.Если в приложении используется неизменяемая библиотека, требуется дополнительная обработка, преобразование состояния маршрутизатора навигации в неизменяемое и изменение вышеупомянутого NavigationReducer:

const initialState = Immutable.fromJS({
  index: 0,
  routes: [{
    routeName: routes.Login.name,
    key: routes.Login.name
  }]
});

const NavigationReducer = (state = initialState, action) => {
  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));
  return newState || state;
};

Преобразуйте начальное состояние по умолчанию в Immutable и используйте при слиянии состояний.merge()метод.

Асинхронное управление потоком задач

Последний модуль, который будет представлен, - это управление асинхронными задачами.В процессе разработки приложений наиболее важной асинхронной задачей является HTTP-запрос данных, поэтому мы говорим об асинхронном управлении задачами, в основном фокусируясь на управлении процессом HTTP-запроса данных.

axios

используется в этом проектеaxiosВ качестве библиотеки HTTP-запросов axios представляет собой HTTP-клиент в формате Promise.Причины выбора этой библиотеки следующие:

  1. XMLHttpRequest может быть инициирован в браузере, а HTTP-запрос также может быть инициирован на стороне node.js;
  2. обещания поддержки;
  3. Возможность перехвата запросов и ответов;
  4. Может отменить запрос;
  5. Автоматически преобразовывать данные JSON;

redux-saga

redux-saga — это сторонняя библиотека, предназначенная для упрощения управления асинхронными задачами, такими как сбор данных и доступ к локальному кешу в приложениях, их эффективное выполнение, облегчение тестирования и улучшенная обработка исключений.

Redux-saga - это сокращение промежуточного программного обеспечения, это похоже на приложение в отдельном процессе, оно отвечает только за управление асинхронными задачами, оно может принимать действие основного процесса приложения, чтобы принять решение о запуске, приостановке или отмене задачи процесса, он также может получить доступ к избыточности состояние хранилища приложений, а затем распространить действие.

инициализировать сагу

redux-saga — это промежуточное программное обеспечение, поэтому сначала позвонитеcreateSagaMiddlewareметод для создания промежуточного программного обеспечения, а затем использовать избыточностьapplyMiddlewareЭтот метод включает ПО промежуточного слоя, а затем использует вспомогательный метод компоновки, чтобы передать его вcreateStoreСоздайте магазин и, наконец, позвонитеrunспособ начать корневую сагу:

import { createStore, applyMiddleware, compose } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/'

const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
enhancers.push(applyMiddleware(...middleware));

const store = createStore(rootReducer, initialState, compose(...enhancers));

// kick off root saga
sagaMiddleware.run(rootSaga);

диверсия саги

В проекте обычно много параллельных модулей, и поток саги каждого модуля тоже должен быть параллельным, который должен быть параллельным в виде мультиветки, предоставляемой redux-sagaforkМетод заключается в том, чтобы запустить текущий поток саги в виде вновь открытой ветки:

import { fork, takeEvery } from 'redux-saga/effects';
import LoginSagas from './LoginSagas';

const sagas = [
  ...LoginSagas,
  ...StartAppSagas
];

export default function * root() {
  yield sagas.map(saga => fork(saga)); 
};

Как и выше, сначала соберите всю корневую сагу модуля, затем пройдитесь по массиву и запустите каждую корневую сагу потока саги.

экземпляр саги

Взяв в качестве примера LoginSagas, для операции входа в систему могут потребоваться некоторые асинхронные запросы после того, как пользователь начнет вход в систему и вход будет успешным, поэтому в списке указаны loginSaga и loginSuccessSaga.Кроме того, когда пользователь выходит из учетной записи, Также может потребоваться HTTP-запрос, поэтому logoutSaga находится здесь:

...

// process login actions
export function * loginSaga () {
  yield takeLatest(LoginTypes.LOGIN, login);
}

export function * loginSuccessSaga () {
  yield takeLatest(LoginTypes.LOGIN_SUCCESS, loginSuccess);
}

export function * logoutSaga () {
  yield takeLatest(LogoutTypes.LOGOUT, logout);
}

const sagas = [
  loginSaga,
  loginSuccessSaga,
  logoutSaga
];

export default sagas;

Использовать в loginSagatakeLatestслушатель методаLoginTypes.LOGINдействие, когда действие получено, вызовитеlogin, вход в систему — это по сути сага, в которой обрабатываются асинхронные задачи:

function * login (action) {
  const { username, password } = action.payload || {};

  if (username && password) {
    const res = yield requestLogin({
      username,
      password
    });

    const { data } = res || {};

    if (data && data.success) {
      yield put(LoginActions.loginSuccess({
        username,
        password,
        isLogin: true
      }));
    } else {
      yield put(LoginActions.loginFail({
        username,
        password,
        isLogin: false
      }));
    }
  } else {
    yield put(LoginActions.loginFail({
      username,
      password,
      isLogin: false
    }));
  }
}

requestLoginМетод представляет собой HTTP-запрос входа в систему с параметрами имени пользователя и пароля изLoginTypes.LOGINПолезная нагрузка, переданная действием, получена,yieldОператор извлекает ответ на запрос, присваивает его res, а затем оценивает успешность входа в систему по содержимому ответа:

  1. Войти успешно, раздатьLoginActions.loginSuccessдействие, которое затем выполнит редьюсер, прослушивающий это действие, иloginSuccessSagasaga;
  2. Не удалось войти, распространитьLoginActions.loginFailaction;

put — это диспетчерский метод действия, предоставляемый redux-saga.

сага и Реактотрон

Ранее было настроено, что вы можете использовать Reactotron для захвата всех редуксов и действий приложения, а редукс-сага — это тип промежуточного программного обеспечения редукса, поэтому захват саг требует дополнительной настройки.При создании хранилища добавьте сервис sagaMonitor в сагу промежуточное ПО для мониторинга саги:

const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;
const sagaMiddleware = createSagaMiddleware({ sagaMonitor });
middleware.push(sagaMiddleware);
...

Суммировать

В этой статье подробно описан процесс построения архитектуры проекта от 0 до 1, а также более глубокое понимание и размышления о приложениях React, React Native, Redux и методах разработки проектов, а также продолжение продвижения по пути большого внешнего интерфейса. рост.

Полный код смотрите на github

Ссылаться на

  1. react native
  2. реагировать на родную китайскую сеть
  3. react navigation