ReactNative-秘籍第二版-四-

73 阅读1小时+

ReactNative 秘籍第二版(四)

原文:zh.annas-archive.org/md5/12592741083b1cbc7e657e9f51045dce

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:实施 Redux

在本章中,我们将逐步介绍将 Redux 添加到我们的应用程序的过程。我们将涵盖以下教程:

  • 安装 Redux 并准备我们的项目

  • 定义动作

  • 定义减速器

  • 设置存储

  • 与远程 API 通信

  • 将存储连接到视图

  • 使用 Redux 存储离线内容

  • 显示网络连接状态

介绍

在大多数应用程序的开发过程中,我们都需要更好地处理整个应用程序的状态的方法。这将简化在组件之间共享数据,并为将来扩展我们的应用程序提供更健壮的架构。

为了更好地理解 Redux,本章的结构将与以前的章节不同,因为我们将通过所有这些教程创建一个应用程序。本章中的每个教程都将依赖于上一个教程。

我们将构建一个简单的应用程序来显示用户帖子,并使用ListView组件来显示从 API 返回的数据。我们将使用位于jsonplaceholder.typicode.com的优秀模拟数据 API。

安装 Redux 并准备我们的项目

在这个教程中,我们将在一个空应用程序中安装 Redux,并定义我们应用程序的基本文件夹结构。

入门

我们将需要一个新的空应用程序来完成这个教程。让我们称之为redux-app

我们还需要两个依赖项:redux用于处理状态管理和react-redux用于将 Redux 和 React Native 粘合在一起。您可以使用 yarn 从命令行安装它们:

yarn add redux react-redux

或者您可以使用npm

npm install --save redux react-redux

如何做...

  1. 作为这个教程的一部分,我们将构建应用程序将使用的文件夹结构。让我们添加一个components文件夹,里面有一个Album文件夹,用来保存相册组件。我们还需要一个redux文件夹来保存所有 Redux 代码。

  2. redux文件夹中,让我们添加一个index.js文件进行 Redux 初始化。我们还需要一个photos目录,里面有一个actions.js文件和一个reducer.js文件。

  3. 目前,App.js文件将只包含一个Album组件,我们稍后会定义:

import React, { Component } from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';

import Album from './components/Album';

const App = () => (
  <SafeAreaView style={styles.container}>
    <Album />
  </SafeAreaView>
);

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
});

export default App;

它是如何工作的...

入门中,我们安装了reduxreact-redux库。react-redux库包含了将 Redux 与 React 集成的必要绑定。Redux 并不是专门设计用于与 React 一起工作的。您可以将 Redux 与任何其他 JavaScript 库一起使用。通过使用react-redux,我们将能够无缝地将 Redux 集成到我们的 React Native 应用程序中。

步骤 2中,我们创建了我们应用程序将使用的主要文件夹:

  • components文件夹将包含我们的应用程序组件。在这种情况下,我们只添加了一个Album组件,以使本教程简单。

  • redux文件夹将包含所有与 Redux 相关的代码(初始化、操作和减速器)。

在中等到大型的应用程序中,您可能希望进一步分离您的 React Native 组件。React 社区的标准是将应用程序的组件分为三种不同的类型:

  • Components:社区称它们为展示性组件。简单来说,这些是不知道任何业务逻辑或 Redux 操作的组件。这些组件只通过 props 接收数据,并且应该可以在任何其他项目中重复使用。按钮或面板将是展示性组件的完美例子。

  • Containers:这些是直接从 Redux 接收数据并能够调用操作的组件。在这里,我们将定义诸如显示已登录用户的标题之类的组件。通常,这些组件在内部使用展示性组件。

  • Pages/Views:这些是应用程序中使用容器和展示性组件的主要模块。

有关构建 Redux 支持组件的更多信息,我建议阅读以下链接的优秀文章,为可扩展性和可维护性构建您的 React-Redux 项目

levelup.gitconnected.com/structure-your-react-redux-project-for-scalability-and-maintainability-618ad82e32b7

我们还需要创建一个redux/photos文件夹。在这个文件夹中,我们将创建以下内容:

  • actions.js文件,其中将包含应用程序可以执行的所有操作。我们将在下一个教程中更多地讨论操作。

  • reducer.js文件,其中将包含管理 Redux 存储中数据的所有代码。我们将在以后的教程中更深入地探讨这个主题。

定义操作

一个 action 是发送数据到 store 的信息载荷。使用这些 actions 是组件请求或发送数据到 Redux store 的唯一方式,Redux store 作为整个应用程序的全局状态对象。一个 action 只是一个普通的 JavaScript 对象。我们将定义返回这些 actions 的函数。返回 action 的函数称为 action creator。

在这个教程中,我们将创建加载图库初始图片的 actions。在这个教程中,我们将添加硬编码的数据,但以后,我们将从 API 请求这些数据,以创建更真实的场景。

准备工作

让我们继续在上一个教程中的代码上工作。确保按照这些步骤安装 Redux 并构建我们将在此项目中使用的文件夹结构。

如何做...

  1. 我们需要为每个 action 定义类型。打开redux/photos/actions.js文件。action 类型被定义为常量,以便稍后在 actions 和 reducers 中引用,如下所示:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
  1. 现在让我们创建我们的第一个 action creator。每个 action 都需要一个type属性来定义它,而且 actions 通常会有一个payload属性,用于传递数据。在这个教程中,我们将硬编码一个由两个照片对象组成的模拟 API 响应,如下所示:
export const fetchPhotos = () => {
  return {
    type: FETCH_PHOTOS,
    payload: {
      "photos": [
        {
          "albumId": 2,
          "title": "dolore esse a in eos sed",
          "url": "http://placehold.it/600/f783bd",
          "thumbnailUrl": "http://placehold.it/150/d83ea2",
          "id": 2
        },
        {
          "albumId": 2,
          "title": "dolore esse a in eos sed",
          "url": "http://placehold.it/600/8e6eef",
          "thumbnailUrl": "http://placehold.it/150/bf6d2a",
          "id": 3
        }
      ]
    }
  }
}
  1. 我们将需要为每个我们希望应用程序能够执行的 action 创建一个 action creator,并且我们希望这个应用程序能够添加和删除图片。首先,让我们添加addBookmark action creator,如下所示:
export const ADD_PHOTO = 'ADD_PHOTO';
export const addPhoto = (photo) => {
  return {
    type: ADD_PHOTO,
    payload: photo
  };
}
  1. 同样,我们还需要另一个用于删除照片的 action creator:
export const REMOVE_PHOTO = 'REMOVE_PHOTO';
export const removePhoto = (photo) => {
  return {
    type: REMOVE_PHOTO,
    payload: photo
  };
}

它是如何工作的...

步骤 1中,我们定义了 action 的类型来指示它的作用,这种情况下是获取图片。我们使用常量,因为它将在多个地方使用,包括 action creators、reducers 和测试。

步骤 2中,我们声明了一个 action creator。Actions 是简单的 JavaScript 对象,定义了在我们的应用程序中发生的事件,这些事件将影响应用程序的状态。我们使用 actions 与 Redux 存储中的数据进行交互。

只有一个单一的要求:每个 action 必须有一个type属性。此外,一个 action 通常会包括一个payload属性,其中包含与 action 相关的数据。在这种情况下,我们使用了一个照片对象的数组。

只要type属性被定义,一个 action 就是有效的。如果我们想发送其他内容,使用payload属性是一种常见的约定,这是 flux 模式所推广的。然而,name 属性并不是固有特殊的。我们可以将其命名为paramsdata,行为仍然相同。

还有更多...

目前,我们已经定义了动作创建者,它们是简单的返回动作的函数。为了使用它们,我们需要使用 Redux store提供的dispatch方法。我们将在后面的配方中了解更多关于 store 的内容。

定义 reducers

到目前为止,我们已经为我们的应用创建了一些动作。正如前面讨论的,动作定义了应该发生的事情,但我们还没有为执行动作创建任何内容。这就是 reducers 的作用。Reducers 是定义动作如何影响 Redux store中的数据的函数。在 reducer 中访问store中的数据。

Reducers 接收两个参数:stateactionstate参数表示应用的全局状态,action参数是 reducer 使用的动作对象。Reducers 返回一个新的state参数,反映了与给定action参数相关的更改。在这个配方中,我们将介绍一个用于通过在前一个配方中定义的动作来获取照片的 reducer。

准备工作

这个配方依赖于前一个配方定义动作。确保从本章的开头开始,以避免任何问题或混淆。

如何做...

  1. 让我们从打开photos/reducer.js文件开始,并导入我们在前一个配方中定义的所有动作类型,如下所示:
import {
  FETCH_PHOTOS,
  ADD_PHOTO,
  REMOVE_PHOTO
} from './actions';
  1. 我们将为这个 reducer 中的状态定义一个初始状态对象。它有一个photos属性,初始化为一个空数组,用于当前加载的照片,如下所示:
const initialState = () => return {
 photos: []
};
  1. 现在我们可以定义reducer函数。它将接收两个参数,当前状态和已经被分发的动作,如下所示:
export default (state = initialState, action) => {
  // Defined in next steps 
} 

React Native 组件也可以有一个state对象,但这是一个完全独立于 Redux 使用的state。在这个上下文中,state指的是存储在 Redux store中的全局状态。

  1. 状态是不可变的,所以在 reducer 函数内部,我们需要返回当前动作的新状态,而不是操纵状态,如下所示:
export default (state = initialState, action) => {
 switch (action.type) {
 case FETCH_PHOTOS:
 return {
 ...state,
 photos: [...action.payload],
 };
  // Defined in next steps
}
  1. 为了将新的书签添加到数组中,我们只需要获取操作的有效负载并将其包含在新数组中。我们可以使用展开运算符在state上展开当前的照片数组,然后将action.payload添加到新数组中,如下所示:
    case ADD_PHOTO:
      return {
        ...state,
        photos: [...state.photos, action.payload],
      };
  1. 如果我们想从数组中删除一个项目,我们可以使用 filter 方法,如下所示:
    case REMOVE_PHOTO:
      return {
        ...state,
        photos: state.photos.filter(photo => {
          return photo.id !== action.payload.id
        })
      };
  1. 最后一步是将我们拥有的所有 reducer 组合在一起。在一个更大的应用程序中,您可能有理由将您的 reducer 拆分成单独的文件。由于我们只使用一个 reducer,这一步在技术上是可选的,但它说明了如何使用 Redux 的combineReducers助手将多个 reducer 组合在一起。让我们在redux/index.js文件中使用它,我们还将在下一个示例中用它来初始化 Redux 存储,如下所示:
import { combineReducers } from 'redux';
import photos from './photos/reducers';
const reducers = combineReducers({
  photos,
});

工作原理...

步骤 1中,我们导入了在上一个示例中声明的所有操作类型。我们使用这些类型来确定应该采取什么操作以及action.payload应该如何影响 Redux 状态。

步骤 2中,我们定义了reducer函数的初始状态。目前,我们只需要一个空数组来存储我们的照片,但我们可以向状态添加其他属性,例如isLoadingdidError的布尔属性来跟踪加载和错误状态。这些可以反过来用于在async操作期间和响应async操作时更新 UI。

步骤 3中,我们定义了reducer函数,它接收两个参数:当前状态和正在分派的操作。如果没有提供初始状态,我们将初始状态设置为initialState。这样,我们可以确保照片数组始终存在于应用程序中,这将有助于避免在分派不影响 Redux 状态的操作时出现错误。

步骤 4中,我们定义了一个用于获取照片的操作。请记住,状态永远不会被直接操作。如果操作的类型与 case 匹配,那么通过将当前的state.photos数组与action.payload上的传入照片组合在一起,将创建一个新的状态对象。

reducer函数应该是纯的。这意味着任何输入值都不应该有副作用。改变状态或操作是不好的做法,应该始终避免。突变可能导致数据不一致或无法正确触发渲染。此外,为了防止副作用,我们应该避免在 reducer 内部执行任何 AJAX 请求。

步骤 5中,我们创建了向 photos 数组添加新元素的 action,但我们没有使用Array.push,而是返回一个新数组,并将传入的元素附加到最后一个位置,以避免改变状态中的原始数组。

步骤 6中,我们添加了一个从状态中删除书签的 action。这样做的最简单方法是使用filter方法,这样我们就可以忽略在 action 的 payload 中收到的 ID 对应的元素。

步骤 7中,我们使用combineReducers函数将所有的 reducers 合并成一个单一的全局状态对象,该对象将保存在 store 中。这个函数将使用与 reducer 对应的状态中的键调用每个 reducer;这个函数与下面的函数完全相同:

import photosReducer from './photos/reducer'; 

const reducers = function(state, action) { 
  return { 
    photos: photosReducer(state.photos, action), 
  }; 
} 

photos reducer 只被调用了关心 photos 的状态的部分。这将帮助你避免在单个 reducer 中管理所有状态数据。

设置 Redux store

Redux store 负责更新 reducers 内部计算的状态信息。它是一个单一的全局对象,可以通过 store 的getState方法访问。

在这个食谱中,我们将把之前创建的 actions 和 reducer 联系在一起。我们将使用现有的 actions 来影响存储在 store 中的数据。我们还将学习如何通过订阅 store 的更改来记录状态的变化。这个食谱更多地作为一个概念的证明,说明了 actions、reducers 和 store 是如何一起工作的。我们将在本章后面更深入地了解 Redux 在应用程序中更常见的用法。

如何做...

  1. 让我们打开redux/index.js文件,并从redux中导入createStore函数,如下所示:
import { combineReducers, createStore } from 'redux';
  1. 创建 store 非常简单;我们只需要调用函数

步骤 1中导入并将 reducers 作为第一个参数发送,如下所示:

const store = createStore(reducers);
export default store;
  1. 就是这样!我们已经设置好了 store,现在让我们分发一些 actions。这个食谱中的下一步将从最终项目中删除,因为它们是用来测试我们的设置。让我们首先导入我们想要分发的 action creators:
import { 
  loadPhotos, 
  addPhotos, 
  removePhotos, 
} from './photos/actions'; 
  1. 在分发任何 actions 之前,让我们订阅 store,这将允许我们监听 store 中发生的任何更改。对于我们当前的目的,我们只需要console.log store.getState()的结果,如下所示:
const unsubscribe = store.subscribe(() => {
  console.log(store.getState());
});
  1. 让我们分发一些 actions,并在开发者控制台中查看结果状态:
store.dispatch(loadPhotos());
  1. 为了添加一个新的书签,我们需要使用照片对象作为参数来分派addBookmark操作创建者:
store.dispatch(addPhoto({
  "albumId": 2,
  "title": "dolore esse a in eos sed",
  "url": `http://placehold.it/600/`,
  "thumbnailUrl": `http://placehold.it/150/`
}));
  1. 要删除一个项目,我们将要删除的照片的id传递给操作创建者,因为这是减速器用来查找应该被删除的项目的内容:
store.dispatch(removePhoto({ id: 1 }));
  1. 执行完所有这些操作后,我们可以通过运行我们在步骤 4中订阅 store 时创建的取消订阅函数来停止监听 store 上的更改,如下所示:
unsubscribe(); 
  1. 我们需要将redux/index.js文件导入到App.js文件中,这将运行本示例中的所有代码,以便我们可以在开发者控制台中看到相关的console.log消息:
import store from './redux'; 

工作原理...

步骤 3中,我们导入了我们在之前的示例定义操作中创建的操作创建者。即使我们还没有 UI,我们也可以使用 Redux 存储并观察更改的发生。只需调用一个操作创建者,然后分派生成的操作即可。

步骤 5中,我们从store实例中调用了dispatch方法。dispatch接受一个由loadBookmarks操作创建者创建的操作。然后将依次调用减速器,这将在状态上设置新的照片。

一旦我们的 UI 就位,我们将以类似的方式从我们的组件中分发操作,这将更新状态,最终触发组件的重新渲染,显示新数据。

与远程 API 通信

我们目前正在从操作中的硬编码数据中加载书签。在真实的应用程序中,我们更有可能从 API 中获取数据。在这个示例中,我们将使用 Redux 中间件来帮助从 API 中获取数据的过程。

准备工作

在这个示例中,我们将使用axios来进行所有的 AJAX 请求。使用npm安装它:

npm install --save axios

或者你可以使用yarn安装它:

yarn add axios

对于这个示例,我们将使用 Redux 中间件redux-promise-middleware。使用npm安装该软件包:

npm install --save redux-promise-middleware

或者你可以使用yarn安装它:

yarn add redux-promise-middleware

这个中间件将为我们应用程序中进行的每个 AJAX 请求创建并自动分派三个相关的动作:一个是在请求开始时,一个是在请求成功时,一个是在请求失败时。使用这个中间件,我们能够定义一个返回带有promise负载的动作创建者。在我们的情况下,我们将创建async动作FETCH_PHOTOS,其负载是一个 API 请求。中间件将创建并分派一个FETCH_PHOTOS_PENDING类型的动作。当请求解析时,中间件将创建并分派一个FETCH_PHOTOS_FULFILLED类型的动作,如果请求成功,则将解析的数据作为payload,如果请求失败,则将错误作为payloadFETCH_PHOTOS_REJECTED类型的动作。

如何做到...

  1. 让我们首先将新的中间件添加到我们的 Redux 存储中。在redux/index.js文件中,让我们添加 Redux 方法applyMiddleware。我们还将添加我们刚刚安装的新中间件,如下所示:
import { combineReducers, createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
  1. 在我们之前定义的createStore调用中,我们可以将applyMiddleware作为第二个参数传递。applyMiddleware接受一个参数,即我们要使用的中间件promiseMiddleware
const store = createStore(reducers, applyMiddleware(promiseMiddleware()));

与其他一些流行的 Redux 中间件解决方案(如redux-thunk)不同,promiseMiddleware在传递给applyMiddleware时必须被调用。它是一个返回中间件的函数。

  1. 我们现在将在我们的动作中进行真正的 API 请求,因此我们需要将axios导入到redux/photos/actions中。我们还将添加 API 的基本 URL。我们使用的是前几章中使用的相同的虚拟数据 API,托管在jsonplaceholder.typicode.com,如下所示:
import axios from 'axios';
const API_URL='http://jsonplaceholder.typicode.com'; 
  1. 接下来,我们将更新我们的动作创建者。我们将首先更新我们处理 AJAX 请求所需的类型,如下所示:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
export const FETCH_PHOTOS_PENDING = 'FETCH_PHOTOS_PENDING';
export const FETCH_PHOTOS_FULFILLED = 'FETCH_PHOTOS_FULFILLED';
export const FETCH_PHOTOS_REJECTED = 'FETCH_PHOTOS_REJECTED';
  1. 与其为这个动作返回虚拟数据作为payload,我们将返回一个GET请求。由于这是一个Promise,它将触发我们的新中间件。另外,请注意动作的类型是FETCH_PHOTOS。这将导致中间件自动创建FETCH_PHOTOS_PENDINGFETCH_PHOTOS_FULFILLED,如果成功则带有解析数据的payload,以及FETCH_PHOTOS_REJECTED,带有发生的错误的payload,如下所示:
export const fetchPhotos = () => {
  return {
    type: FETCH_PHOTOS,
    payload: axios.get(`${API_URL}/photos?_page=1&_limit=20`)
  }
}
  1. 就像FETCH_PHOTOS动作一样,我们将利用相同的中间件提供的类型来处理ADD_PHOTO动作,如下所示:
export const ADD_PHOTO = 'ADD_PHOTO';
export const ADD_PHOTO_PENDING = 'ADD_PHOTO_PENDING';
export const ADD_PHOTO_FULFILLED = 'ADD_PHOTO_FULFILLED';
export const ADD_PHOTO_REJECTED = 'ADD_PHOTO_REJECTED';
  1. action creator 本身将不再只返回传入的照片作为payload,而是将通过 API 传递一个POST请求的 promise 来添加图片,如下所示:
export const addPhoto = (photo) => {
  return {
    type: ADD_PHOTO,
    payload: axios.post(`${API_URL}/photos`, photo)
  };
}
  1. 我们可以按照相同的模式将REMOVE_PHOTO动作转换为使用 API 进行删除照片的 AJAX 请求。像ADD_PHOTOFETCH_PHOTOS这两个 action creator 一样,我们将为每个动作定义动作类型,然后将删除axios请求作为动作的payload返回。由于在我们从 Redux 存储中删除图像对象时,我们将需要photoId在 reducer 中,因此我们还将其作为动作的meta属性上的对象传递,如下所示:
export const REMOVE_PHOTO = 'REMOVE_PHOTO';
export const REMOVE_PHOTO_PENDING = 'REMOVE_PHOTO_PENDING';
export const REMOVE_PHOTO_FULFILLED = 'REMOVE_PHOTO_FULFILLED';
export const REMOVE_PHOTO_REJECTED = 'REMOVE_PHOTO_REJECTED';
export const removePhoto = (photoId) => {
  console.log(`${API_URL}/photos/${photoId}`);
  return {
    type: REMOVE_PHOTO,
    payload: axios.delete(`${API_URL}/photos/${photoId}`),
    meta: { photoId }
  };
}
  1. 我们还需要重新审视我们的 reducers 以调整预期的 payload。在redux/reducers.js中,我们将首先导入我们将使用的所有动作类型,并更新initialState。由于在下一个步骤中将会显而易见的原因,让我们将state对象上的照片数组重命名为loadedPhotos,如下所示:
import {
  FETCH_PHOTOS_FULFILLED,
  ADD_PHOTO_FULFILLED,
  REMOVE_PHOTO_FULFILLED,
} from './actions';

const initialState = {
  loadedPhotos: []
};
  1. 在 reducer 本身中,更新每个 case 以采用基本动作的FULFILLED变体:FETCH_PHOTOS变为FETCH_PHOTOS_FULFILLEDADD_PHOTOS变为ADD_PHOTOS_FULFILLEDREMOVE_PHOTOS变为REMOVE_PHOTOS_FULFILLED。我们还将更新所有对statephotos数组的引用,将其从photos更新为loadedPhotos。在使用axios时,所有响应对象都将包含一个data参数,其中包含从 API 接收到的实际数据,这意味着我们还需要将所有对action.payload的引用更新为action.payload.data。在REMOVE_PHOTO_FULFILLED reducer 中,我们无法再在action.payload.id中找到photoId,这就是为什么我们在步骤 8中在动作的meta属性上传递了photoId,因此action.payload.id变为action.meta.photoId,如下所示:
export default (state = initialState, action) => {
  switch (action.type) {
    case FETCH_PHOTOS_FULFILLED:
      return {
        ...state,
        loadedPhotos: [...action.payload.data],
      };
    case ADD_PHOTO_FULFILLED:
      return {
        ...state,
        loadedPhotos: [action.payload.data, ...state.loadedPhotos],
      };
    case REMOVE_PHOTO_FULFILLED:
      return {
        ...state,
        loadedPhotos: state.loadedPhotos.filter(photo => {
          return photo.id !== action.meta.photoId
        })
      };
    default:
      return state;
  }
}

工作原理...

步骤 2中,我们应用了在入门部分安装的中间件。如前所述,这个中间件将允许我们为自动创建PENDINGFULFILLEDREJECTED请求状态的单个动作创建 AJAX 动作的动作创建者。

步骤 5中,我们定义了fetchPhotos动作创建者。您会回忆起前面的食谱,动作是普通的 JavaScript 对象。由于我们在动作的 payload 属性上定义了一个 Promise,redux-promise-middleware将拦截此动作并自动为三种可能的请求状态创建三个关联的动作。

步骤 7步骤 8中,我们定义了addPhoto动作创建器和removePhoto动作创建器,就像fetchPhotos一样,它们的操作负载是一个 AJAX 请求。

通过使用这个中间件,我们能够避免重复使用相同的样板来进行不同的 AJAX 请求。

在这个配方中,我们只处理了应用程序中进行的 AJAX 请求的成功条件。在真实的应用程序中,明智的做法是还要处理以_REJECTED结尾的操作类型表示的错误状态。这将是一个处理错误的好地方,通过将其保存到 Redux 存储器中,以便在发生错误时视图可以显示错误信息。

将存储器连接到视图

到目前为止,我们已经设置了状态,包括了中间件,并为与远程 API 交互定义了动作、动作创建器和减速器。然而,我们无法在屏幕上显示任何这些数据。在这个配方中,我们将使我们的组件能够访问我们创建的存储器。

准备工作

这个配方依赖于之前的所有配方,所以确保按照本配方之前的每个配方进行操作。

在本章的第一个配方中,我们安装了react-redux库以及其他依赖项。在这个配方中,我们终于要开始使用它了。

我们还将使用第三方库来生成随机颜色十六进制值,我们将使用它来从占位图像服务placehold.it/请求彩色图像。在开始之前,使用npm安装randomcolor

npm install --save randomcolor

或者您也可以使用yarn安装它:

yarn add randomcolor

如何做...

  1. 让我们从将 Redux 存储器连接到 React Native 应用程序的App.js开始。我们将从导入开始,从react-redux导入Provider和我们之前创建的存储器。我们还将导入我们即将定义的Album组件,如下所示:
import React, { Component } from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';
import { Provider } from 'react-redux';
import store from './redux';

import Album from './components/Album';
  1. Provider的工作是将我们的 Redux 存储器连接到 React Native 应用程序,以便应用程序的组件可以与存储器通信。Provider应该用于包装整个应用程序,由于此应用程序位于Album组件中,我们将Album组件与Provider组件一起包装。Provider接受一个store属性,我们将传入我们的 Redux 存储器。应用程序和存储器已连接:
const App = () => (
  <Provider store={store}>
    <Album />
  </Provider>
);

export default App;
  1. 让我们转向Album组件。该组件将位于components/Album/index.js。我们将从导入开始。我们将导入randomcolor包以生成随机颜色十六进制值,如入门部分所述。我们还将从react-redux中导入connect,以及我们在之前的示例中定义的 action creators。connect将连接我们的应用程序到 Redux 存储,并且我们可以使用 action creators 来影响存储的状态,如下所示:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
  SafeAreaView,
  ScrollView,
  Image,
  TouchableOpacity
} from 'react-native';
import randomColor from 'randomcolor';
import { connect } from 'react-redux';
import {
  fetchPhotos,
  addPhoto,
  removePhoto
} from '../../redux/photos/actions';

  1. 让我们创建Album类,但是,我们不会直接将Album作为default导出,而是使用connectAlbum连接到存储。请注意,connect使用了两组括号,并且组件被传递到了第二组括号中,如下所示:
class Album extends Component {

}

export default connect()(Album);
  1. 在调用connect时,第一组括号接受两个函数参数:mapStateToPropsmapDispatchToProps。我们将首先定义mapStateToProps,它以state作为参数。这个state是我们的全局 Redux 状态对象,包含了所有的数据。该函数返回一个包含我们想在组件中使用的state片段的对象。在我们的情况下,我们只需要从photos reducer 中的loadedPhotos属性。通过将这个值设置为返回对象中的photos,我们可以期望this.props.photos是存储在state.photos.loadedPhotos中的值。当 Redux 存储更新时,它将自动更改:
class Album extends Component {

}

const mapStateToProps = (state) => {
 return {
 photos: state.photos.loadedPhotos
 }
}

export default connect(mapStateToProps)(Album);
  1. 同样,mapDispatchToProps函数也将我们的 action creators 映射到组件的 props。该函数接收 Redux 方法dispatch,用于执行 action creator。我们将每个 action creator 的执行映射到相同名称的键上,这样this.props.fetchPhotos()将执行dispatch(fetchPhotos()),依此类推,如下所示:
class Album extends Component {

}

const mapStateToProps = (state) => {
  return {
    photos: state.photos.loadedPhotos
  }
}
 const mapDispatchToProps = (dispatch) => {
 return {
 fetchPhotos: () => dispatch(fetchPhotos()),
 addPhoto: (photo) => dispatch(addPhoto(photo)),
 removePhoto: (id) => dispatch(removePhoto(id))
 }
}

export default connect(mapStateToProps, mapDispatchToProps)(Album);
  1. 现在我们已经将 Redux 存储连接到了我们的组件,让我们创建组件本身。我们可以利用componentDidMount生命周期钩子来获取我们的照片,如下所示:
class Album extends Component {
 componentDidMount() {
 this.props.fetchPhotos();
 }
  // Defined on later steps
}
  1. 我们还需要一个添加照片的方法。在这里,我们将使用randomcolor包(按照惯例导入为randomColor)来使用placehold.it服务创建一张图片。生成的颜色字符串带有一个哈希前缀的十六进制值,而图片服务的请求不需要这个前缀,所以我们可以简单地使用replace调用来移除它。要添加照片,我们只需调用映射到propsaddPhoto函数,传入新的photo对象,如下所示:
  addPhoto = () => {
    const photo = {
      "albumId": 2,
      "title": "dolore esse a in eos sed",
      "url": `http://placehold.it/600/${randomColor().replace('#',
       '')}`,
      "thumbnailUrl": 
  `http://placehold.it/150/${randomColor().replace('#', '')}`
    };
    this.props.addPhoto(photo);
  }
  1. 我们还需要一个removePhoto函数。这个函数所需要做的就是调用已经映射到propsremovePhoto函数,并传入要删除的照片的 ID,如下所示:
  removePhoto = (id) => {
    this.props.removePhoto(id);
  }
  1. 应用程序的模板将需要一个TouchableOpacity按钮用于添加照片,一个ScrollView用于容纳所有图像的可滚动列表,以及所有我们的图像。每个Image组件还将包装在一个TouchableOpacity组件中,以在按下图像时调用removePhoto方法,如下所示:
  render() {
    return (
      <SafeAreaView style={styles.container}>
        <Text style={styles.toolbar}>Album</Text>
        <ScrollView>
          <View style={styles.imageContainer}>
            <TouchableOpacity style={styles.button} onPress=
             {this.addPhoto}>
              <Text style={styles.buttonText}>Add Photo</Text>
            </TouchableOpacity>
            {this.props.photos ? this.props.photos.map((photo) => {
              return(
                <TouchableOpacity onPress={() => 
                 this.removePhoto(photo.id)} key={Math.random()}>
                  <Image style={styles.image}
                    source={{ uri: photo.url }}
                  />
                </TouchableOpacity>
              );
            }) : null}
          </View>
        </ScrollView>
      </SafeAreaView>
    );
  }
  1. 最后,我们将添加样式,以便应用程序具有布局,如下所示。这里没有我们之前没有多次涵盖过的内容:
const styles = StyleSheet.create({
  container: {
    backgroundColor: '#ecf0f1',
    flex: 1,
  },
  toolbar: {
    backgroundColor: '#3498db',
    color: '#fff',
    fontSize: 20,
    textAlign: 'center',
    padding: 20,
  },
  imageContainer: {
    flex: 1,
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center',
  },
  image: {
    height: 300,
    width: 300
  },
  button: {
    margin: 10,
    padding: 20,
    backgroundColor: '#3498db'
  },
  buttonText: {
    fontSize: 18,
    color: '#fff'
  }
});
  1. 应用程序已完成!单击“添加照片”按钮将在图像列表的开头添加一个新照片,并按下图像将删除它。请注意,由于我们使用的是虚拟数据 API,因此POSTDELETE请求将返回给定操作的适当响应。但是,实际上并没有向数据库添加或删除任何数据。这意味着如果应用程序被刷新,图像列表将重置,并且如果您尝试使用“添加照片”按钮添加的任何照片,您可以期望出现错误。随时将此应用程序连接到真实的 API 和数据库以查看预期结果:

它是如何工作的...

步骤 4中,我们使用了react-redux提供的connect方法,为Album组件赋予了与我们在整个章节中一直在使用的 Redux 存储库的连接。对connect的调用返回一个函数,该函数立即通过第二组括号执行。通过将Album组件传递到此返回函数中,connect将组件和存储库粘合在一起。

步骤 5中,我们定义了mapStateToProps函数。此函数中的第一个参数是 Redux 存储库中的state,它由connect注入到函数中。从mapStateToProps返回的对象中定义的任何键都将成为组件props上的属性。这些 props 的值将订阅 Redux 存储库中的state,因此任何影响这些state片段的更改都将在组件内自动更新。

mapStateToProps 将 Redux 存储中的 state 映射到组件的 props,而 mapDispatchToPropsaction creators 映射到组件的 props。在 步骤 6 中,我们定义了这个函数。它具有特殊的 Redux 方法 dispatch,用于调用存储中的 action creators。mapDispatchToProps 返回一个对象,将 actions 的 dispatch 调用映射到组件的 props 上指定的键。

步骤 7 中,我们创建了 componentDidMount 方法。组件在挂载时所需的所有照片只需调用映射到 this.props.fetchPhotos 的 action creator 即可。就是这样!fetchPhotos action creator 将被派发。由于 action creator 返回的 fetchPhoto action 具有一个 Promise 存储在其 payload 属性中,这个 Promise 是以 axios AJAX 请求的形式存储的,因此我们在之前的示例中应用了 redux-promise-middleware。中间件将拦截该 action,处理请求,并发送一个带有解析数据的新 action 到 reducers 的 payload 属性。如果请求成功,将派发带有解析数据的 FETCH_PHOTOS_FULFILLED 类型的 action,如果不成功,将派发带有错误作为 payloadFETCH_PHOTOS_REJECTED action。在成功时,处理 FETCH_PHOTOS_FULFILLED 的 reducer 中的情况将执行,loadedPhotos 将在存储中更新,进而 this.props.photos 也将被更新。更新组件的 props 将触发重新渲染,并且新数据将显示在屏幕上。

步骤 8步骤 9 中,我们遵循相同的模式来定义 addPhotoremovePhoto,它们调用同名的 action creators。action creators 产生的 action 由中间件处理,适当的 reducer 处理生成的 action,如果 Redux 存储中的 state 发生变化,所有订阅的 props 将自动更新!

使用 Redux 存储离线内容

Redux 是一个很好的工具,可以在应用运行时跟踪应用的状态。但是如果我们有一些数据需要在不使用 API 的情况下存储怎么办?例如,我们可以保存组件的状态,这样当用户关闭并重新打开应用时,该组件的先前状态可以被恢复,从而允许我们在会话之间持久化应用的一部分。Redux 数据持久化也可以用于缓存信息,以避免不必要地调用 API。您可以参考第八章中的在网络连接丢失时屏蔽应用教程,了解如何检测和处理网络连接状态的更多信息。

做好准备

这个教程依赖于之前的教程,所以一定要跟着之前的所有教程一起进行。在这个教程中,我们将使用redux-persist包来持久化我们应用的 Redux 存储中的数据。用npm安装它:

npm install --save redux-persist

或者你可以用yarn安装它:

yarn add redux-persist

如何做...

  1. 让我们从redux/index.js中添加我们需要的依赖项。我们在这里从redux-persist导入的storage方法将使用 React Native 的AsyncStorage方法在会话之间存储 Redux 数据,如下所示:
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage';
  1. 我们将使用一个简单的config对象来配置我们的redux-persist实例。config需要一个key属性来存储数据与AsyncStore的键,并且需要一个 storage 属性,该属性接受storage实例,如下所示:
const persistConfig = {
  key: 'root',
  storage
}
  1. 我们将使用我们在步骤 1中导入的persistReducer方法。这个方法将我们在步骤 2中创建的config对象作为第一个参数,将我们的 reducers 作为第二个参数:
const reducers = combineReducers({
  photos,
});

const persistedReducer = persistReducer(persistConfig, reducers);
  1. 现在让我们更新我们的存储以使用新的persistedReducer方法。还要注意,我们不再将store作为默认导出,因为我们需要从这个文件中导出两个内容:
export const store = createStore(persistedReducer, applyMiddleware(promiseMiddleware()));
  1. 我们从这个文件中需要的第二个导出是persistorpersistor将在会话之间持久化 Redux 存储。我们可以通过调用persistStore方法并传入store来创建persistor,如下所示:
export const persistor = persistStore(store);
  1. 现在我们从redux/index.js中得到了storepersistor作为导出,我们准备在App.js中应用它们。我们将从中导入它们,并从redux-persist中导入PersistGate组件。PersistGate将确保我们缓存的 Redux 存储在任何组件加载之前加载:
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './redux';
  1. 让我们更新App组件以使用PersistGate。该组件接受两个属性:导入的persistor属性和一个loading属性。我们将向loading属性传递null,但如果我们有一个加载指示器组件,我们可以将其传递进去,PersistGate会在数据恢复时显示这个加载指示器,如下所示:
const App = () => (
  <Provider store={store}>
    <PersistGate loading={null} persistor={persistor}>
      <Album />
    </PersistGate>
  </Provider>
);
  1. 为了测试我们的 Redux 存储的持久性,让我们调整Album组件中的componentDidMount方法。我们将延迟调用fetchPhotos两秒钟,这样我们就可以在从 API 再次获取数据之前看到保存的数据,如下所示:
  componentDidMount() {
 setTimeout(() => {
      this.props.fetchPhotos();
 }, 2000);
  }

根据您要持久化的数据类型,这种功能可以应用于许多情况,包括持久化用户数据和应用状态,甚至在应用关闭后。它还可以用于改善应用的离线体验,如果无法立即进行 API 请求,则缓存 API 请求,并为用户提供填充数据的视图。

工作原理...

步骤 2中,我们创建了用于配置redux-persist的配置对象。该对象只需要具有keystore属性,但也支持其他许多属性。您可以通过此处托管的类型定义查看此配置接受的所有选项:github.com/rt2zz/redux-persist/blob/master/src/types.js#L13-L27

步骤 7中,我们使用了PersistGate组件,这是文档建议的延迟渲染直到恢复持久化数据完成的方法。如果我们有一个加载指示器组件,我们可以将其传递给loading属性,以便在数据恢复时显示。

第十章:应用程序工作流和第三方插件

本章的工作方式有些不同,因此我们将首先了解它,然后再继续覆盖以下示例:

  • React Native 开发工具

  • 规划您的应用程序并选择您的工作流程

  • 使用 NativeBase 来实现跨平台 UI 组件

  • 使用 glamorous-native 来为 UI 组件添加样式

  • 使用 react-native-spinkit 添加动画加载指示器

  • 使用 react-native-side-menu 添加侧边导航菜单

  • 使用 react-native-modalbox 添加模态框

本章的工作方式

在本章中,我们将更仔细地了解初始化新的 React Native 应用程序的每种方法的工作原理,以及如何集成可能与 Expo 兼容或不兼容的第三方包。在之前的章节中,重点完全放在构建 React Native 应用程序的功能部分上。因此,在本章中,许多这些示例也将用于说明如何使用不同的工作流程来实现不同的包。

在本章的大多数示例中,我们将从使用 React Native CLI 命令初始化的纯 React Native 项目开始,方法如下:

 react-native init

创建新的 React Native 应用程序时,您需要选择适合初始化应用程序的正确工具。一般来说,您用于引导和开发 React Native 应用程序的工具将专注于简化开发过程,并故意为了方便和心理负担而模糊化本地代码,或者通过提供对所有本地代码的访问以及允许使用更多第三方插件来保持开发过程的灵活性。

初始化和开发应用有两种方法:Expo 和 React Native CLI。直到最近,还有第三种方法,使用 Create React Native App(CRNA)。CRNA 已经与 Expo 项目合并,只作为一个独立实体继续存在,以提供向后兼容性。

Expo 属于工具的第一类,提供了更强大和开发者友好的开发工作流程,但牺牲了一些灵活性。使用 Expo 引导的应用程序还可以访问由 Expo SDK 提供的大量有用功能,例如BarcodeScannerMapViewImagePicker等等。

使用 React Native CLI 通过以下命令初始化应用程序:

 react-native init

这提供了灵活性,但开发的难度也相应增加。

 react-native init 

据说这是一个纯 React Native 应用程序,因为没有任何原生代码对开发人员隐藏。

作为一个经验法则,如果使用第三方包的设置需要运行以下命令,则需要一个纯 React Native 应用程序:

 react-native link 

那么,当您在使用 Expo 构建应用程序时,却发现一个对应用程序要求至关重要的包不受 Expo 开发工作流支持时,该怎么办?幸运的是,Expo 有一种方法可以将 Expo 项目转换为纯 React Native 应用程序,就好像是用以下命令创建的一样:

expo eject

当项目被弹出时,所有的原生代码都被解压到iosandroid文件夹中,App.js文件被拆分为App.jsindex.js,暴露出挂载根 React Native 组件的代码。

但是,如果您的 Expo 应用依赖于 Expo SDK 提供的功能呢?毕竟,使用 Expo 开发的价值很大程度上来自于 Expo 提供的出色功能,包括AuthSessionPermissionsWebBrowser等。

这就是 ExpoKit 发挥作用的地方。当您选择从项目中弹出时,您可以选择将 ExpoKit 包含在弹出的项目中。包含 ExpoKit 将确保您应用中使用的所有 Expo 依赖项将继续工作,并且还可以让您在应用被弹出后继续使用 Expo SDK 的所有功能。

要更深入地了解弹出过程,您可以阅读 Expo 文档,链接为docs.expo.io/versions/latest/expokit/eject

React Native 开发工具

与任何开发工具一样,灵活性和易用性之间存在权衡。我鼓励您在进行 React Native 开发工作流时首先使用 Expo,除非您确定需要访问原生代码。

Expo

这是从expo.io网站上获取的:

"Expo 是围绕 React Native 构建的免费开源工具链,帮助您使用 JavaScript 和 React 构建原生 iOS 和 Android 项目。"

Expo 正在成为一个自己的生态系统,由五个相互连接的工具组成:

  • Expo CLI:Expo 的命令行界面。

我们一直在使用 Expo CLI 来创建、构建和提供应用程序。CLI 支持的所有命令列表可以在官方文档中找到,链接如下:

docs.expo.io/versions/latest/workflow/expo-cli

  • Expo 开发者工具:这是一个基于浏览器的工具,每当通过expo start命令从终端启动 Expo 应用程序时,它会自动运行。它为您的开发中应用程序提供活动日志,并快速访问本地运行应用程序并与其他开发人员共享应用程序。

  • Expo 客户端:适用于 Android 和 iOS 的应用程序。这个应用程序允许您在设备上的 Expo 应用程序中运行您的 React Native 项目,而无需安装它。这使开发人员可以在真实设备上进行热重载,或者与其他人共享开发代码,而无需安装它。

  • Expo Snack:托管在snack.expo.io,这个网络应用程序允许您在浏览器中使用 React Native 应用程序,并实时预览您正在工作的代码。如果您曾经使用过 CodePen 或 JSFiddle,Snack 是将相同的概念应用于 React Native 应用程序。

  • Expo SDK:这是一个 SDK,其中包含了一组精彩的 JavaScript API,提供了在基本 React Native 软件包中找不到的本机功能,包括使用设备的加速计、相机、通知、地理位置等。这个 SDK 已经与使用 Expo 创建的每个新项目一起提供。

这些工具共同组成了 Expo 工作流程。使用 Expo CLI,您可以创建并构建具有 Expo SDK 支持的新应用程序。CLI 还提供了一种简单的方式,通过自动将您的代码推送到 Amazon S3 并为项目生成 URL 来为您的开发中应用程序提供服务。然后,CLI 生成一个与托管代码链接的 QR 码。在您的 iPhone 或 Android 设备上打开 Expo Client 应用程序,扫描 QR 码,BOOM,您的应用程序就在那里,配备了热重载!由于应用程序托管在 Amazon S3 上,您甚至可以实时与其他开发人员共享开发中的应用程序。

React Native CLI

使用命令创建新的 React Native 应用程序的原始引导方法如下:

react-native init 

这是由 React Native CLI 提供的。如果您确定需要访问应用程序的本机层,则可能只会使用这种引导新应用程序的方法。

在 React Native 社区中,使用这种方法创建的应用程序被称为纯 React Native 应用程序,因为所有的开发和本地代码文件都暴露给开发人员。虽然这提供了最大的自由,但也迫使开发人员维护本地代码。如果你是一个 JavaScript 开发人员,因为你打算仅使用 JavaScript 编写本地应用程序而跳上 React Native 的车,那么在 React Native 项目中维护本地代码可能是这种方法最大的缺点。

另一方面,在使用已经引导的应用程序时,您将可以访问更多的第三方插件。

直接访问代码库的本地部分。您还将能够绕过 Expo 目前的一些限制,特别是无法使用后台音频或后台 GPS 服务。

CocoaPods

一旦你开始使用具有使用本地代码的组件的应用程序,你也将在开发中使用 CocoaPods。CocoaPods 是 Swift 和 Objective-C Cocoa 项目的依赖管理器。它几乎与 npm 相同,但是管理的是本地 iOS 代码的开源依赖,而不是 JavaScript 代码。

在本书中我们不会经常使用 CocoaPods,但 React Native 在其 iOS 集成中使用 CocoaPods,因此对管理器的基本了解可能会有所帮助。就像package.json文件包含了使用 npm 管理的 JavaScript 项目的所有包一样,CocoaPods 使用Podfile列出项目的 iOS 依赖关系。同样,这些依赖项可以使用以下命令安装:

 pod install

CocoaPods 需要 Ruby 才能运行。在命令行上运行以下命令来验证 Ruby 是否已安装:

 ruby -v 

如果没有,可以使用 Homebrew 命令安装:

 brew install ruby

一旦 Ruby 被安装,CocoaPods 可以通过命令安装:

sudo gem install cocoapods

如果在安装过程中遇到任何问题,可以阅读官方 CocoaPods 入门指南guides.cocoapods.org/using/getting-started.html

规划您的应用程序并选择您的工作流程

在尝试选择最适合您的应用程序需求的开发工作流程时,有一些事情您应该考虑:

  • 我是否需要访问代码库的本地部分?

  • 我是否需要任何 Expo 不支持的第三方包,需要运行 react-native link 命令?

  • 当应用程序不在前台时,是否需要播放音频?

  • 当应用程序不在前台时,是否需要位置服务?

  • 我是否愿意至少在 Xcode 和 Android Studio 中进行工作?

根据我的经验,Expo 通常是最好的起点。它为开发过程提供了许多好处,并且在应用程序超出原始要求时,可以通过退出过程来获得逃生舱。我建议只有在确定您的应用程序需要 Expo 应用程序无法提供的内容,或者确定您将需要处理本机代码时,才使用 React Native CLI 开始开发。

我还建议浏览托管在native.directory的 Native Directory。该网站拥有大量用于 React Native 开发的第三方软件包目录。该网站上列出的每个软件包都有估计的稳定性、流行度和链接到文档。然而,Native Directory 最好的功能可能是能够按照它们支持的设备/开发类型(包括 iOS、Android、Expo 和 Web)来过滤软件包。这将帮助您缩小软件包选择范围,并更好地指示应采用哪种工作流程。

如何做...

我们将从 React Native CLI 设置我们的应用程序开始,这将创建一个新的纯 React Native 应用程序,使我们可以访问所有本机代码,但也需要安装 Xcode 和 Android Studio。

您可能还记得第一章中设置您的环境,其中一些步骤已经详细介绍了。无需重新安装已在那里描述的任何列在此处的内容。

  1. 首先,我们将安装所有与纯 React Native 应用程序一起工作所需的依赖项,从 macOS 的 Homebrew(brew.sh/)软件包管理器开始。如项目主页上所述,Homebrew 可以通过以下命令轻松从终端安装:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 安装 Homebrew 后,可以使用它来安装 React Native 开发所需的依赖项:Node.js 和nodemon。如果您是 JavaScript 开发人员,您可能已经安装了 Node.js。您可以通过以下命令检查它是否已安装:
node -v

这个命令将列出已安装的 Node.js 的版本。请注意,您需要 Node.js 的 8 版本或更高版本来进行 React Native 开发。如果 Node.js 尚未安装,您可以通过以下命令使用 Homebrew 安装它:

brew install node
  1. 我们还需要nodemon包,React Native 在幕后使用它来启用开发过程中的实时重新加载等功能。通过以下命令使用 Homebrew 安装nodemon
brew install watchman
  1. 当然,我们还需要 React Native CLI 来运行引导 React Native 应用程序的命令。可以通过以下命令全局安装它:
npm install -g react-native-cli
  1. 安装了 CLI 之后,创建一个新的纯 React Native 应用程序只需要以下命令:
react-native init name-of-project

这将在一个新的name-of-project目录中创建一个新的项目。这个项目暴露了所有的原生代码,并且需要 Xcode 来运行 iOS 应用程序和 Android Studio 来运行 Android 应用程序。幸运的是,为了支持 iOS React Native 开发安装 Xcode 是一个简单的过程。第一步是从 App Store 下载 Xcode 并安装它。第二步是安装 Xcode 命令行工具。要做到这一点,打开 Xcode,从 Xcode 菜单中选择“首选项”,打开位置面板,并从命令行工具下拉菜单中安装最新版本:

  1. 很遗憾,为了支持 Android React Native 开发设置 Android Studio 并不是一件轻而易举的事情,需要一些非常具体的步骤来安装它。由于这个过程特别复杂,并且有可能在你阅读本章时已经发生了变化,我建议参考官方文档,获取安装所有 Android 开发依赖的深入和最新的说明。这些说明托管在以下 URL:

facebook.github.io/react-native/docs/getting-started.html#java-development-kit

  1. 现在所有的依赖都已经安装好了,我们可以通过命令行运行我们的纯 React Native 项目。iOS 应用程序可以通过以下方式执行:
react-native run-ios

Android 应用程序可以通过以下方式启动:

react-native run-android

在尝试打开 Android 应用程序之前,请确保您已经运行 Android 模拟器。这些命令应该在关联的模拟器上启动您的应用程序,安装新应用程序,并在模拟器中运行应用程序。如果您对这些命令的任何一个行为不符合预期遇到任何问题,您可能可以在此处找到答案:React Native 故障排除文档,托管在此处:

facebook.github.io/react-native/docs/troubleshooting.html#content

Expo CLI 设置

可以使用终端通过以下命令使用 npm 安装 Expo CLI:

npm install -g expo-cli

Expo CLI 可用于执行 Expo GUI 客户端可以执行的所有操作。有关可以使用 CLI 运行的所有命令,请查看此处的文档:

docs.expo.io/versions/latest/workflow/expo-cli

使用 NativeBase 进行跨平台 UI 组件

与 Web 上的 Bootstrap 类似,NativeBase 是一组 React Native 组件,用于提高 React Native 应用程序开发的效率。这些组件涵盖了在原生应用程序中构建 UI 的各种用例,包括操作表、徽章、卡片、抽屉和网格布局。

NativeBase 是一个支持纯 React Native 应用程序(使用 React Native CLI 通过react-native init创建的应用程序)和 Expo 应用程序的库。有关将 NativeBase 安装到一种项目或另一种项目中的说明在 NativeBase 文档的“入门”部分中概述,托管在此处:

github.com/GeekyAnts/NativeBase#4-getting-started

由于这种情况,我们将在本教程的“准备就绪”部分中概述这两种情况。

准备就绪

无论您使用哪种引导方法来完成此教程,我们都将尽可能保持教程的“如何做…”部分一致。我们需要考虑的一个区别是每种应用程序创建方法的项目命名约定。纯 React Native 应用程序以 Pascal 大小写(MyCoolApp)命名,而 Expo 应用程序以 kebab 大小写(my-cool-app)命名。如果您正在创建纯 React Native 应用程序,可以使用应用程序名称NativeBase,如果您正在使用 Expo,可以将其命名为native-base

使用纯 React Native 应用程序(React Native CLI)

假设您已经按照本章的介绍安装了 React Native CLI。如果没有,请立即使用npm安装:

npm install -g react-native-cli

要使用 CLI 创建一个新的纯 React 应用程序,我们将使用以下命令:

 react-native init NativeBase

这将在当前目录中的名为NativeBase的文件夹中创建一个新的纯 React Native 应用程序。下一步是安装所需的对等依赖项。让我们cd进入新的NativeBase目录,并使用npm安装native-base包:

npm install native-base --save

或者,您可以使用yarn

yarn add native-base

最后,我们将使用以下命令安装本机依赖项:

react-native link

如果我们在 IDE 中打开项目并查看这个纯 React Native 应用程序的文件夹结构,我们会看到与此时习惯的 Expo 应用程序有一些细微的差异。首先,存储库有一个ios和一个android文件夹,分别包含各自平台的本机代码。项目的根目录还有一个index.js文件,这个文件在使用 Expo 引导的应用程序中不包括。在使用 Expo 制作的应用程序中,这个文件会被隐藏起来,就像iosandroid文件夹一样,如下所示:

import { AppRegistry } from 'react-native';
import App from './App';

AppRegistry.registerComponent('NativeBase', () => App);

这只是在运行时为您的 React Native 应用程序提供引导过程。AppRegistryreact-native包中导入,主要的App组件从目录根目录的App.js文件中导入,并且使用两个参数调用AppRegistry方法registerComponent:我们应用的名称(NativeBase)和一个返回App组件的匿名函数。有关AppRegistry的更多信息,您可以在这里找到文档:

facebook.github.io/react-native/docs/appregistry.html

另一个小的区别是在App.js样板代码中存在两套开发说明,通过使用Platform组件显示适当的开发说明。

每当看到第三方 React Native 包的安装说明包括运行以下命令时,请记住停下来思考:

 react-native link

通常可以安全地假定它与 Expo 应用程序不兼容,除非另有明确说明。在 NativeBase 的情况下,我们有选择使用任一设置,因此让我们接下来介绍使用 Expo 进行引导的其他选项。

使用 Expo 应用

在使用 Expo 创建的应用程序中设置 Native Base 就像使用npmyarn安装所需的依赖项一样简单。首先,我们可以在命令行上使用 Expo CLI 创建应用程序:

 expo init native-base 

创建应用程序后,我们可以cd进入它,并使用npm安装 NativeBase 的依赖项:

npm install native-base @expo/vector-icons --save

或者,您可以使用yarn

yarn add native-base @expo/vector-icons

在使用 Expo 时,NativeBase 文档建议在App.js组件的componentWillMount方法中使用Expo.Font.loadAsync方法异步加载字体。我们将在本示例的如何做部分的适当步骤中介绍如何做到这一点。您可以使用以下命令从 CLI 启动应用程序:

 expo start 

如何做到这一点...

  1. 我们将首先在App.js中的App组件中添加我们将使用的导入。虽然这个应用程序不会有太多的功能,但我们将使用许多来自 NativeBase 的组件,以了解它们如何帮助改进您的工作流程,如下所示:
import React, { Component } from 'react';
import { View, Text, StyleSheet } from 'react-native'
import {
  Spinner,
  Button,
  Body,
  Title,
  Container,
  Header,
  Fab,
  Icon,
} from 'native-base';
  1. 接下来,让我们声明App类并定义一个起始的state对象。我们将添加一个 FAB 部分,以展示 NativeBase 如何让您轻松地向应用程序添加弹出菜单按钮。我们将使用fabActive布尔值来跟踪是否应该显示此菜单。稍后在render方法中,我们还将使用loading布尔值,如下所示:
export default class App extends Component {
  state = {
    loading: true
    fabActive: false
  }
  // Defined on following steps
}
  1. 您可能还记得在本示例的准备工作部分中,如果您正在使用 Expo 开发应用程序,NativeBase 建议通过Expo.Font.loadAsync函数加载 NativeBase 使用的字体。在componentWillMount方法中,我们将初始化并等待require字体的加载,然后将state上的loading属性设置为falseloading属性将在render方法中被引用,以确定应用程序是否已经完成加载,如下所示:
// Other import statements import { Font, AppLoaded } from 'expo';

export default class App extends Component {
  state = {
    fabActive: false
  }

 async componentWillMount() {
 await Font.loadAsync({
 'Roboto': require('native-base/Fonts/Roboto.ttf'),
 'Roboto_medium': require('native-base/Fonts/Roboto_medium.ttf'),
 'Ionicons': require('@expo/vector-icons/fonts/Ionicons.ttf'),
 });
    this.setState({ loading: false });
 }
  // Defined on following steps
}
  1. 由于这个应用程序主要是 UI,我们准备开始构建render函数。为了确保在使用字体之前加载它们,如果stateloading属性为 true,我们将返回 App 占位符 Expo 组件AppLoading,否则我们将渲染 App UI。AppLoading将指示应用程序继续显示应用程序的启动画面,直到组件被移除。

如果您选择使用纯 React Native 项目开始此示例,您将无法访问 Expo 组件。在这种情况下,您可以简单地返回一个空的View而不是AppLoading

  1. 我们将从Container组件开始,以及HeaderBodyTitle辅助组件。这将作为页面的容器,显示页面顶部带有标题“Header Title”的标题!
  render() {
    if (this.state.loading) {
      return <AppLoading />;
    } else {
      return (
        <Container>
          <Header>
            <Body>
              <Title>Header Title!</Title>
            </Body>
          </Header>
        </Container>
      );
    }
  }

此时,应用程序应该类似于以下屏幕截图:

  1. 在以下代码中,Header将具有来自 NativeBase 的一些其他 UI 元素。 Spinner组件允许轻松显示带有传递的所需颜色的加载旋转器。与原始的TouchableOpacity组件相比,Button组件提供了更多的内置可定制性。在这里,我们使用block属性将按钮扩展到其容器,并在每个按钮上使用infosuccess属性来应用它们各自的默认蓝色和绿色背景颜色:
      <Container>
        <Header>
          <Body>
            <Title>Header Title!</Title>
          </Body>
        </Header>
 <View style={styles.view}>
 <Spinner color='green' style={styles.spinner} />
 <Button block info
 onPress={() => { console.log('button 1 pressed') }}
 >
 <Text style={styles.buttonText}>Click Me! </Text>
 </Button>
 <Button block success
 onPress={() => { console.log('button 2 pressed') }}
 >
 <Text style={styles.buttonText}>No Click Me!</Text>
 </Button>
 {this.renderFab()}
 </View>
      </Container>
  1. 前面的渲染函数还引用了我们尚未定义的renderFab方法。这利用了IconFab组件。 NativeBase 在内部使用与 Expo 相同的vector-icons包(如果未提供type属性,则默认为 Ionicon 字体),这在第三章的“使用字体图标”配方中有介绍,请参考该配方获取更多信息:
  renderFab = () => {
    return (
      <Fab active={this.state.fabActive}
        direction="up"
        style={styles.fab}
        position="bottomRight"
        onPress={() => this.setState({ fabActive:
        !this.state.fabActive })}>
        <Icon name="share" />
        <Button style={styles.facebookButton}
          onPress={() => { console.log('facebook button pressed') }}
        >
          <Icon name="logo-facebook" />
        </Button>
        <Button style={styles.twitterButton}
          onPress={() => { console.log('twitter button pressed')}}
        >
          <Icon name="logo-twitter" />
        </Button>
      </Fab>
    );
  }
  1. 让我们用一些样式来完善这个配方,以便在View中对齐事物并将颜色应用到我们的布局中,如下所示:
const styles = StyleSheet.create({
  view: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
    paddingBottom: 40
  },
  buttonText: {
    color: '#fff'
  },
  fab: {
    backgroundColor: '#007AFF'
  },
  twitterButton: {
    backgroundColor: '#1DA1F2'
  },
  facebookButton: {
    backgroundColor: '#3B5998'
  },
  spinner: {
    marginBottom: 180
  }
});
  1. 回顾已完成的应用程序,现在有一个漂亮的跨平台 UI 分布,易于使用:

工作原理...

虽然这个配方更复杂的部分是设置应用程序本身,但我们快速回顾了 NativeBase 提供的一些组件,这些组件可能有助于您更有效地开发下一个应用程序。如果您喜欢在类似于 Bootstrap(getbootstrap.com/)或 Semantic-UI(semantic-ui.com/)在 Web 平台上提供的基于小部件的系统中工作,请务必尝试 NativeBase。有关 NativeBase 提供的所有组件及其使用方法的更多信息,您可以在docs.nativebase.io/Components.html找到官方文档。

使用 glamorous-native 来为 UI 组件设置样式

作为 JavaScript 开发人员,您可能熟悉 Web 上的 CSS 以及它如何用于样式化网页和 Web 应用程序。最近,一种称为 CSS-in-JS 的技术出现在 Web 开发中,它利用 JavaScript 的力量来调整 CSS,以实现更模块化、基于组件的样式化方法。CSS-in-JS 工具的主要好处之一是它们能够生成针对给定元素范围的样式,而不是默认的 JavaScript 级联行为。范围 CSS 允许开发人员以更可预测和模块化的方式应用样式。这反过来增加了在较大组织中的可用性,并使打包和发布样式化组件变得更容易。如果您想了解 CSS-in-JS 的工作原理或 CSS-in-JS 的概念来源,我在 gitconnected Medium 博客上写了一篇名为《CSS-in-JS 简史:我们是如何到达这里以及我们将去哪里》的文章,托管在:

levelup.gitconnected.com/a-brief-history-of-css-in-js-how-we-got-here-and-where-were-going-ea6261c19f04

React Native 捆绑的StyleSheet组件是 CSS-in-JS 的实现。在 Web 上最受欢迎的 CSS-in-JS 实现之一是glamorous,这是由备受尊敬的 Kent C. Dodds 创建的库。这个库启发了出色的 React Native 样式库glamorous-native,我们将在这个示例中使用它。

准备工作

我们需要为这个示例创建一个新的应用程序。在设置期间,此软件包不需要运行以下命令:

react-native link

因此,应该可以在 Expo 应用程序中正常工作。让我们将示例命名为glamorous-app

我们还需要安装 glamorous-app 包。这可以通过npm安装:

npm install --save glamorous-native

或者,我们可以使用yarn

yarn add glamorous-native

如何做...

  1. 让我们首先在App.js中导入我们需要的所有依赖项,如下所示:
import React from 'react';
import glamorous from 'glamorous-native';
  1. 我们的应用程序将需要一个包含View元素,以容纳应用程序中显示的所有其他组件。我们将使用glamorous来为此元素传递样式对象,而不是像我们在所有先前的示例中所做的那样,通过传递给StyleSheet组件的对象来对此元素进行样式化。我们将使用view方法,它返回一个样式化的View组件,我们将其存储在一个名为Containerconst中,以便以后使用,如下所示:
const Container = glamorous.view({
  flex: 1,
  justifyContent: 'center',
  alignItems: 'center',
  backgroundColor: '#fff',
});
  1. 同样地,我们将使用glamorous.text添加三个样式化的Text组件。通过这样做,我们有了另外三个样式化和明确定义名称的组件,可以在render中使用,如下所示:
const Headline = glamorous.text({
  fontSize: 30,
  paddingBottom: 8
});

const SubHeading = glamorous.text({
  fontSize: 26,
  paddingBottom: 8
});

const ButtonText = glamorous.text({
  fontSize: 18,
  color: 'white'
});
  1. 我们还将使用glamorous.touchableHighlight方法制作一个可重用的Button组件。这种方法展示了glamorous组件也可以用不同类型的多个样式声明来创建。在这种情况下,传递给touchableHighlight的第二个参数是一个函数,根据元素上定义的props来更新backgroundColor样式,如下所示:
const Button = glamorous.touchableHighlight(
  { padding: 10 },
  props => ({backgroundColor: props.warning ? 'red' : 'blue'})
);
  1. 我们还可以创建内联样式的组件,这要归功于glamorous提供的特殊版本的 React Native 组件。我们将使用一个Image组件,但是不是从react-native中导入,而是从导入的glamorous包中使用Image组件,如下所示:
const { Image } = glamorous;
  1. 现在,我们准备声明App组件。App只需要一个render函数来渲染我们所有新样式化的组件,如下所示:
export default class App extends React.Component {
  render() {
    // Defined in following steps.
  }
}
  1. 让我们开始构建render函数,通过添加在步骤 2中创建的Container组件。代码可读性的改进已经显而易见。Container被明确定义,并且不需要其他属性或属性来声明样式,如下所示:
  render() {
 return (
 <Container>
 // Defined on following steps
 </Container>
 );
  }
  1. 让我们添加从导入的glamorous库中提取的Image组件,这是在步骤 5中完成的。请注意,我们能够直接在组件上声明样式属性,如heightwidthborderRadius,而不是像普通的Image组件那样:
      <Container>
 <Image
 height={250}
 width={250}
 borderRadius={20}
 source={{ uri: 'http://placehold.it/250/3B5998' }}
 />
        // Defined on following steps
      </Container>
  1. 现在,我们将添加在步骤 3中创建的HeadlineSubheading组件。就像Container组件一样,这两个组件的可读性要比一个View和两个Text元素好得多:
      <Container>
        <Image
          height={250}
          width={250}
          borderRadius={20}
          source={{ uri: 'http://placehold.it/250/3B5998' }}
        />
 <Headline>I am a headline</Headline>
 <SubHeading>I am a subheading</SubHeading>
        // Defined in following steps
      <Container>
  1. 最后,我们将添加在步骤 4中创建的Button组件,以及在步骤 3中创建的ButtonText组件。两个按钮都有一个onPress方法,就像任何TouchableOpacityTouchableHighlight组件一样,但第二个Button还有一个warning属性,导致它具有红色背景而不是蓝色:
        <Button
          onPress={() => console.log('Thanks for clicking me!')}
        >
          <ButtonText>
            Click Me!
          </ButtonText>
        </Button>
        <Button
          warning
          onPress={() => console.log(`You shouldn't have clicked me!`)}
        >
          <ButtonText>
            Don't Click Me!
          </ButtonText>
        </Button>
  1. 所有我们的glamorous组件都已添加到render方法中。如果你运行这个应用程序,你应该会看到一个完全样式化的用户界面。

它是如何工作的...

步骤 2步骤 3中,我们使用相应的glamorous方法创建了带有样式的ViewText组件,并传入了一个包含应该应用于该特定组件的所有样式的对象。

步骤 4中,我们通过应用与前几步创建ViewText组件相同的方法,创建了一个可重用的Button样式组件。然而,这个组件中声明样式的方式是不同的,并展示了glamorous-native在处理样式时的多功能性。您可以将任意数量的样式集合作为参数传递给glamorous组件构造函数,它们都将被应用。这包括动态样式,通常采用在组件上定义的 props 来应用不同的样式。在步骤 10中,我们使用了我们的Button元素。如果存在warning属性,就像在render中的第一个Button上一样,backgroundColor将是red。否则,它将是blue。这为在多种类型的组件上应用简单和可重用的主题提供了一个非常好的系统。

步骤 5中,我们从glamorous库中提取了Image组件,以替代 React Native 的Image组件。这个特殊版本的组件与其 React Native 对应组件的行为相同,同时还能够直接对元素本身应用样式。在步骤 8中,我们使用了该组件,我们能够应用heightwidthborderRadius样式,而无需使用style属性。

使用 react-native-spinkit 添加动画加载指示器

无论您正在构建什么样的应用程序,您的应用程序很有可能需要等待某种数据,无论是加载资产还是等待来自 AJAX 请求的响应。当出现这种情况时,您可能还希望您的应用程序能够向用户指示某个必需的数据仍在加载中。解决这个问题的一个易于使用的解决方案是使用react-native-spinkit。这个包提供了 15 个(其中四个仅适用于 iOS)专业外观、易于使用的加载指示器,用于在您的应用程序中显示数据加载时。

这个包需要运行以下命令:

react-native link

因此,可以安全地假设它不会与 Expo 应用程序一起工作(除非随后将该应用程序弹出)。这将为我们提供另一个依赖于纯 React Native 工作流程的配方。

入门

现在我们已经确定了这个配方将在纯 React Native 中构建,我们可以通过以下方式从命令行初始化一个名为SpinKitApp的新应用程序:

react-native init SpinKitApp

这个命令将开始搭建过程。完成后,cd进入新的SpinKitApp目录,并使用npm添加react-native spinkit

npm install react-native-spinkit@latest --save

或者使用yarn

yarn add react-native-spinkit@latest

安装了库之后,我们必须使用以下命令将其链接起来才能使用:

react-native link

此时,应用程序已经启动,并且已安装了依赖项。然后可以通过以下方式在 iOS 或 Android 模拟器中运行应用程序:

react-native run-ios

或者,使用这个:

react-native run-android

在 iOS 模拟器中启动纯 React Native 项目时,如果希望指定设备,可以传递simulator参数设置为所需设备的字符串值。例如,react-native run-ios --simulator="iPhone X"将在模拟的 iPhone X 中启动应用程序。

在通过命令行启动纯 React Native 项目的 Android 模拟器时,必须在运行此命令之前打开您打算使用的 Android 模拟器。

在这个配方中,我们还将再次使用randomcolor库。使用npm安装它:

npm install randomcolor --save

或者使用yarn

yarn add randomcolor

如何做到这一点...

  1. 我们将首先在项目的根目录的App.js文件中添加依赖项,如下所示:
import React, { Component } from 'react';
import {
  StyleSheet,
  View,
  TouchableOpacity,
  Text
} from 'react-native';
import Spinner from 'react-native-spinkit';
import randomColor from 'randomcolor';
  1. 在这个配方中,我们将设置应用程序循环显示react-native-spinkit提供的所有加载旋转器类型。为此,让我们创建一个包含每种可能的旋转器类型的字符串数组。由于最后四种类型在 Android 中不完全受支持,它们在 Android 上都将显示为相同的Plane旋转器,如下所示:
const types = [
  'Bounce',
  'Wave',
  'WanderingCubes',
  'Pulse',
  'ChasingDots',
  'ThreeBounce',
  'Circle',
  '9CubeGrid',
  'FadingCircleAlt',
  'FadingCircle',
  'CircleFlip',
  'WordPress',
  'Arc',
  'ArcAlt'
];
  1. 现在,我们可以开始构建App组件。我们将需要一个具有四个属性的state对象:一个isVisible属性来跟踪是否应该显示旋转器,一个用于保存当前旋转器类型的type属性,一个用于保持在types数组中的位置的typeIndex,以及一个颜色。我们将通过简单调用randomColor()来将颜色初始化为随机十六进制代码,如下所示:
export default class App extends Component {
  state = {
    isVisible: true,
    typeIndex: 0,
    type: types[0],
    color: randomColor()
  }
}
  1. 我们将需要一个函数来改变Spinner组件的属性,我们将在render方法中稍后定义。这个函数简单地将typeIndex增加一,或者如果已经到达数组的末尾,则将其设置回0,然后相应地更新state,如下所示:
  changeSpinner = () => {
    const { typeIndex } = this.state;
    let nextType = typeIndex === types.length - 1 ? 0 : typeIndex +
    1;
    this.setState({
      color: randomColor(),
      typeIndex: nextType,
      type: types[nextType]
    });
  }
  1. render方法将由Spinner组件组成,包裹在TouchableOpacity组件中,用于改变Spinner的类型和颜色。我们还将添加一个Text组件来显示当前Spinner的类型,如下所示:
  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity onPress={this.changeSpinner}>
          <Spinner
            isVisible={this.state.isVisible}
            size={120}
            type={this.state.type}
            color={this.state.color}
          />
        </TouchableOpacity>
        <Text style={styles.text}>{this.state.type}</Text>
      </View>
    );
  }
  1. 最后,让我们为中心内容添加一些样式,并通过text类增加Text元素的字体大小,如下所示:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#fff',
  },
  text: {
    paddingTop: 40,
    fontSize: 25
  }
});
  1. 完成这个示例后,我们应该看到一个在按下时改变的加载器。感谢react-native-spinkit,这就是向我们的 React Native 应用程序添加时髦的加载指示器所需的一切!

它是如何工作的...

步骤 5中,我们定义了应用程序的render方法,其中我们使用了Spinner组件。Spinner组件有四个可选的属性:

  • isVisible:一个布尔值,用于确定是否应该显示组件。默认值:true

  • color:一个十六进制代码,用于确定旋转器的颜色。默认值:#000000

  • size:以像素为单位确定旋转器的大小。默认值:37

  • type:一个字符串,确定要使用的旋转器类型。默认值:Plane

由于Spinner组件上的isVisible属性设置为state对象上的isVisible的值,所以我们可以简单地在长时间运行的过程开始时(例如等待来自 AJAX 请求的响应),将此属性切换为true,并在操作完成时将其设置回false

还有更多...

尽管我们在这个示例中创建的应用程序相当简单,但它展示了react-native-spinkit如何实现,以及如何实际使用需要react-native link命令的第三方包。由于无数的开源贡献者的辛勤工作,有各种各样的第三方包可供在下一个 React Native 应用程序中使用。能够利用任何符合应用程序需求的第三方包,无论这些包有什么要求,都将是规划和开发 React Native 项目的重要工具。

使用 react-native-side-menu 添加侧边导航菜单

侧边菜单是一种常见的 UX 模式,用于在移动应用程序中显示选项、控件、应用程序设置、导航和其他次要信息。第三方包react-native-side-menu提供了一种在 React Native 应用程序中实现侧边菜单的出色且简单的方法。在这个示例中,我们将构建一个具有侧边菜单的应用程序,其中包含可以改变背景的按钮。

准备工作

设置react-native-side-menu包不需要命令:

 react-native link

所以请随意使用 Expo 或纯 React Native 应用程序创建此应用。我们需要为这个示例创建一个新的应用程序,并且出于项目命名的目的,我们将假设这个应用程序是使用 Expo 构建的,并将其命名为side-menu-app。如果您使用纯 React Native,可以将其命名为SideMenuApp

我们还需要使用npmreact-native-side-menu安装到我们的项目中。

npm install react-native-side-menu --save

或者,使用yarn

yarn add react-native-side-menu

如何做...

  1. 让我们从在项目根目录的App.js文件中添加我们需要的所有导入开始这个示例。其中一个导入是Menu组件,我们将在后面的步骤中创建它:
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity } from 'react-native';
import SideMenu from 'react-native-side-menu';
import Menu from './components/Menu';
  1. 接下来,让我们定义App类和初始的state。在这个应用程序中,state只需要两个属性:一个isOpen布尔值,用于跟踪侧边菜单何时应该打开,以及一个selectedBackgroundColor属性,其值是表示当前选定的背景颜色的字符串,如下所示:
export default class App extends React.Component {
  state = {
    isOpen: false,
    selectedBackgroundColor: 'green'
  }
  // Defined in following steps
}
  1. 我们的应用程序将需要一个方法来改变state上的selectedBackgroundColor属性。这个方法以一个color字符串作为参数,并将该颜色设置为selectedBackgroundColor。它还会将state.isOpen设置为false,以便在从菜单中选择颜色时关闭侧边菜单,如下所示:
  changeBackgroundColor = color => {
    this.setState({
      isOpen: false,
      selectedBackgroundColor: color,
    });
  }
  1. 我们准备好定义Apprender方法。首先,让我们设置Menu组件,以便在下一步中可以被SideMenu使用。我们还没有创建Menu组件,但我们将使用onColorSelected属性来传递changeBackgroundColor方法,如下所示:
  render() {
    const menu = <Menu onColorSelected={this.changeBackgroundColor}
   />;

    // Defined in next step
  }
  1. 渲染的 UI 由四个部分组成。第一个是一个View组件,它有一个与state.selectedBackgroundColor绑定的style属性。这个View组件包含一个单独的TouchableOpacity按钮组件,每当按下它时就会打开侧边菜单。SideMenu组件有一个必需的menu属性,它接受将充当侧边菜单本身的组件,因此我们将Menu组件传递给这个属性,如下所示:
  render() {
    const menu = <Menu onColorSelected={this.changeBackgroundColor} />;

    return (
 <SideMenu
 menu={menu}
 isOpen={this.state.isOpen}
 onChange={(isOpen) => this.setState({ isOpen })}
 >
 <View style={[
 styles.container,
 { backgroundColor: this.state.selectedBackgroundColor }
 ]}>
 <TouchableOpacity
 style={styles.button}
 onPress={() => this.setState({ isOpen: true })}
 >
 <Text style={styles.buttonText}>Open Menu</Text>
 </TouchableOpacity>
 </View>
 </SideMenu>
 );
  }
  1. 作为这个组件的最后一步,让我们添加基本样式来居中布局,并应用颜色和字体大小,如下所示:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
  },
  button: {
    backgroundColor: 'black',
    padding: 20,
    borderRadius: 10
  },
  buttonText: {
    color: 'white',
    fontSize: 25
  }
});
  1. 是时候创建Menu组件了。让我们在component文件夹中创建一个Menu.js文件。我们将从组件导入开始。就像我们在之前的示例中所做的那样,我们还将使用Dimensions将应用程序窗口的尺寸存储在一个变量中,以便应用样式,如下所示:
import React from 'react';
import {
  Dimensions,
  StyleSheet,
  View,
  Text,
  TouchableOpacity
} from 'react-native';

const window = Dimensions.get('window');
  1. Menu组件只需要是一个展示性组件,因为它没有状态或生命周期钩子的需求。该组件将接收onColorSelected作为属性,我们将在下一步中使用它,如下所示:
const Menu = ({ onColorSelected }) => {
  return (
    // Defined on next step
  );
}

export default Menu;
  1. Menu组件的主体只是一系列TouchableOpacity按钮,当按下时,会调用onColorSelected,传入相应的颜色,如下所示:
    <View style={styles.menu}>
      <Text style={styles.heading}>Select a Color</Text>
      <TouchableOpacity onPress={() => onColorSelected('green')}>
        <Text style={styles.item}>
          Green
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('blue')}>
        <Text style={styles.item}>
          Blue
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('orange')}>
        <Text style={styles.item}>
          Orange
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('pink')}>
        <Text style={styles.item}>
          Pink
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('cyan')}>
        <Text style={styles.item}>
          Cyan
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('yellow')}>
        <Text style={styles.item}>
          Yellow
        </Text>
      </TouchableOpacity>
      <TouchableOpacity onPress={() => onColorSelected('purple')}>
        <Text style={styles.item}>
          Purple
        </Text>
      </TouchableOpacity>
    </View>
  1. 让我们为Menu组件添加一些样式,应用颜色和字体大小。请注意,我们还在步骤 7中定义的window变量来设置组件的heightwidth,使其等于屏幕的大小,如下所示:
const styles = StyleSheet.create({
  menu: {
    flex: 1,
    width: window.width,
    height: window.height,
    backgroundColor: '#3C3C3C',
    justifyContent: 'center',
    padding: 20,
  },
  heading: {
    fontSize: 22,
    color: '#f6f6f6',
    fontWeight: 'bold',
    paddingBottom: 20
  },
  item: {
    fontSize: 25,
    paddingTop: 10,
    color: '#f6f6f6'
  }
});
  1. 我们的应用程序已经完成!当按下“打开菜单”按钮时,一个平滑动画的侧边菜单将从左侧滑出,显示一个供用户选择的颜色列表。当从列表中选择颜色时,应用程序的背景颜色会更改,并且菜单会滑动回关闭状态:

工作原理...

步骤 4中,我们为主App组件创建了render函数。我们将Menu组件存储在menu变量中,以便可以清晰地将其传递给SideMenumenu属性,就像我们在步骤 5中所做的那样。我们通过onColorSelected属性将changeBackgroundColor类方法传递给我们的Menu组件,以便我们可以使用它来正确更新App组件中的state

然后,我们将Menu组件作为menu属性传递给SideMenu,将这两个组件连接在一起。第二个属性是isOpen,它决定侧边菜单是否应该打开。第三个属性onChange接受一个回调函数,每次菜单打开或关闭时都会执行。onChange回调提供了一个isOpen参数,我们用它来更新stateisOpen的值,以便保持同步。

包含的View元素具有一个style属性,设置为一个数组,其中包含步骤 6中定义的container样式和一个具有backgroundColor键设置为state中的selectedBackgroundColor的对象。这将导致View组件的背景颜色在更新时更改为此值。

步骤 8步骤 9中,我们构建了Menu组件的render方法。每个TouchableOpacity按钮都连接到onColorSelected,传入与按下按钮相关联的颜色。这反过来在父App类中运行changeBackgroundColor,在设置state.isOpenfalse时更新state.selectedBackgroundColor,导致背景颜色改变并关闭侧边菜单。

使用 react-native-modalbox 添加模态框

许多移动 UI 的常见部分是模态框。模态框是隔离数据的理想解决方案,以有意义的方式提醒用户更新的信息,显示阻止其他用户交互的必需操作(如登录屏幕)等等。

我们将使用第三方包react-native-modalbox。该软件包提供了一个易于理解和多功能的 API,用于创建模态框,选项包括以下内容:

  • position:顶部、底部、中心

  • entry:模态框进入的方向-顶部或底部?

  • backdropColor

  • backdropOpacity

有关所有可用选项,请参阅文档:

github.com/maxs15/react-native-modalbox

准备就绪

我们将需要一个新的应用程序来完成这个示例。react-native-modalbox软件包对 Expo 友好,因此我们可以使用 Expo 创建此应用程序。我们将为这个应用程序命名为modal-app。如果使用纯 React Native 项目,可以使用ModalApp这样的名称,以匹配命名约定。

我们还需要第三方软件包。可以使用npm进行安装:

npm install react-native-modalbox --save

或者,使用yarn

yarn add react-native-modalbox

如何做...

  1. 让我们从在项目的根目录中打开App.js文件并添加导入开始,如下所示:
import React from 'react';
import Modal from 'react-native-modalbox';
import {
  Text,
  StyleSheet,
  View,
  TouchableOpacity
} from 'react-native';
  1. 接下来,我们将定义和导出App组件,以及初始的state对象,如下所示。对于这个应用程序,我们只需要一个isOpen布尔值来跟踪我们的模态框是否应该打开或关闭:
export default class App extends Component {
  state = {
    isOpen: false
  };
  // Defined on following steps
}
  1. 让我们跳到下一个构建render方法。该模板由两个TouchableOpacity按钮组件组成,当按下时,打开它们各自的模态框。我们将在接下来的步骤中定义这两个模态框。这些按钮将调用两种方法来渲染每个模态框的两个模态框组件,如下所示:
  render = () => {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          onPress={this.openModal1}
          style={styles.button}
        >
          <Text style={styles.buttonText}>
            Open Modal 1
          </Text>
        </TouchableOpacity>
        <TouchableOpacity
          onPress={this.openModal2}
          style={styles.button}
        >
          <Text style={styles.buttonText}>
            Open Modal 2
          </Text>
        </TouchableOpacity>
        {this.renderModal1()}
        {this.renderModal2()}
      </View>
    );
  }
  1. 现在,我们准备定义renderModal1方法。Modal组件需要一个ref属性来分配一个字符串,这将用于在我们想要打开或关闭它时引用Modal,如下所示:
  renderModal1 = () => {
    return(
      <Modal
        style={[styles.modal, styles.modal1]}
        ref={'modal1'}
        onClosed={this.onClose}
        onOpened={this.onOpen}
      >
        <Text style={styles.modalText}>
          Hello from Modal 1
        </Text>
      </Modal>
    )
  }
  1. 让我们接下来添加openModal1方法。这个方法是在步骤 3中我们在render方法中添加的第一个TouchableOpacity组件上通过onPress调用的。通过将modal1字符串传递给我们在步骤 4中定义的Modal组件上的ref属性,我们能够将模态框访问为this.refs.modal1。在这个引用上调用open方法将打开模态框。关于这一点,我们将在本教程末尾的*它是如何工作的...*部分详细介绍。添加openModal1方法如下:
  openModal1 = () => {
    this.refs.modal1.open();
  }
  1. 我们在步骤 4中定义的Modal还有onClosedonOpened属性,它们分别接受一个在模态框关闭或打开时执行的回调函数。让我们接下来为这些属性定义回调函数。在本教程中,我们将只是简单地使用console.log作为概念验证,如下所示:
  onClose = () => {
    console.log('modal is closed');
  }

  onOpen = () => {
    console.log('modal is open');
  }
  1. 我们准备好定义第二个模态框了。这个Modal组件的ref属性将设置为字符串modal2,我们将添加两个其他可选的属性,这些属性在另一个模态框上没有使用。第一个是position,可以设置为topbottomcenter(默认)。isOpen属性提供了通过布尔值打开和关闭模态框的第二种方法。模态框的内容有一个带有 OK 按钮的TouchableOpacity,当按下时,将会将state对象上的isOpen布尔值设置为false,关闭模态框,如下所示:
renderModal2 = () => {
    return(
      <Modal
        style={[styles.modal, styles.modal2]}
        ref={'modal2'}
        position={'bottom'}
        onClosed={this.onCloseModal2}
        isOpen={this.state.isOpen}
      >
        <Text style={styles.modalText}>
          Hello from Modal 2
        </Text>
        <TouchableOpacity
          onPress={() => this.setState({isOpen: false})}
          style={styles.button}
        >
          <Text style={styles.buttonText}>
            OK
          </Text>
        </TouchableOpacity>
      </Modal>
    )
  }
  1. 由于我们使用state布尔值isOpen来操纵模态框的状态,openModal2方法将演示另一种打开和关闭模态框的方法。通过将state上的isOpen设置为true,第二个模态框将打开,如下所示:
  openModal2 = () => {
    this.setState({ isOpen: true });
  }
  1. 您可能还注意到,在步骤 7中定义的第二个模态框有一个不同的onClosed回调。如果用户按下 OK 按钮,state上的isOpen值将成功更新为false,但如果他们通过触摸背景来关闭模态框,它将不会。添加onCloseModal2方法可以确保stateisOpen值无论用户如何关闭模态框都能正确保持同步,如下所示:
  onCloseModal2 = () => {
    this.setState({ isOpen: false });
  }
  1. 这个教程的最后一步是应用样式。我们将有一个用于共享模态框样式的modal类,用于每个模态框独特样式的modal1modal2类,以及用于将颜色、填充和边距应用于按钮和文本的类,如下所示:
const styles = StyleSheet.create({
  container: {
    backgroundColor: '#f6f6f6',
    justifyContent: 'center',
    alignItems: 'center',
    flex: 1
  },
  modal: {
    width: 300,
    justifyContent: 'center',
    alignItems: 'center'
  },
  modal1: {
    height: 200,
    backgroundColor: "#4AC9B0"
  },
  modal2: {
    height: 300,
    backgroundColor: "#6CCEFF"
  },
  modalText: {
    fontSize: 25,
    padding: 10,
    color: '#474747'
  },
  button: {
    backgroundColor: '#000',
    padding: 16,
    borderRadius: 10,
    marginTop: 20
  },
  buttonText: {
    fontSize: 30,
    color: '#fff'
  }
});
  1. 这个教程已经完成,我们现在有一个应用程序,其中有两个基本的模态框,通过按钮按下显示,并在同一个组件中和谐共存:

工作原理...

步骤 4中,我们定义了第一个Modal组件。我们定义了onClosedonOpened属性,将onCloseonOpen类方法传递给这些属性。每当打开这个Modal组件时,this.onOpen都会触发,当Modal关闭时,this.onClose会执行。虽然在这个示例中我们没有对这些方法做任何激动人心的事情,但这些钩子可以作为记录与模态框相关的用户操作的绝佳机会。或者,如果模态框包含一个表单,onOpen可以用来预先填充一些表单输入数据,而onClose可以将表单数据保存到state对象中,以便在关闭模态框时使用。

步骤 5中,我们定义了第一个TouchableOpacity按钮组件在按下时执行的方法:openModal1。在这个方法中,我们利用了Modal组件的引用。引用是 React 本身的一个核心特性,它为组件实例提供了一个存储在组件渲染方法中创建的 DOM 节点和/或 React 元素的位置。就像 React(和 React Native)组件有状态和属性(在类组件中为this.statethis.props)一样,它们也可以有引用(存储在this.ref上)。有关 React 中引用的工作原理,请查看文档:

reactjs.org/docs/refs-and-the-dom.html

由于我们将第一个Modal上的ref属性设置为字符串modal1,因此我们可以在openModal1方法中使用引用this.ref.modal1访问同一个组件。由于Modal有一个open和一个close方法,调用this.ref.modal1.open()会打开具有modal1引用的Modal

这并不是打开和关闭Modal组件的唯一方法,就像我们在步骤 7中定义的第二个模态框所示。由于这个组件有一个isOpen属性,可以通过改变传递给该属性的布尔值来打开或关闭模态框。通过将isOpen设置为状态的isOpen值,我们可以使用此模态框中的确定按钮来从内部关闭模态框,通过在state上将isOpen设置为 false。在步骤 8中,我们定义了openModal2方法,也说明了通过改变state上的isOpen值来打开第二个模态框。

步骤 9中,我们为保持stateisOpen值同步定义了一个单独的isClosed回调,以防用户通过按下背景而不是模态框的确定按钮来关闭模态框。另一种策略是通过向Modal组件添加backdropPressToClose属性并将其设置为false来禁用用户通过按下背景来关闭模态框。

react-native-modalbox包提供了许多其他可选的属性,可以使模态框的创建更加容易。在这个示例中,我们使用了position来声明第二个模态框应该放在屏幕底部,您可以在文档中查看Modal的所有其他可用属性:

github.com/maxs15/react-native-modalbox

react-native-modalbox库支持在单个组件中使用多个模态框;但是,尝试在这些模态框中的多个上使用isOpen属性将导致所有这些模态框同时打开,这不太可能是期望的行为。

第十一章:添加原生功能-第一部分

在这一章中,我们将涵盖以下内容:

  • 暴露自定义 iOS 模块

  • 渲染自定义 iOS 视图组件

  • 暴露自定义 Android 模块

  • 渲染自定义 Android 视图组件

介绍

React Native 开发的核心原则之一是使用 JavaScript 构建真正的原生移动应用程序。为了实现这一点,许多原生 API 和 UI 组件通过抽象层暴露,并通过 React Native 桥访问。虽然 React Native 和 Expo 团队继续改进和扩展已经存在的令人印象深刻的 API,但通过原生 API,我们可以访问其他方式无法获得的功能,比如振动、联系人以及原生警报和提示。

通过暴露原生视图组件,我们能够利用设备提供的所有渲染性能,因为我们不像混合应用程序那样通过 WebView 进行渲染。这给用户提供了原生的外观和感觉,可以适应用户运行应用程序的平台。使用 React Native,我们已经能够渲染许多原生视图组件,包括地图、列表、输入字段、工具栏和选择器。

虽然 React Native 带有许多内置的原生模块和视图组件,但有时我们需要一些自定义功能,利用原生应用程序层,这些功能并不是开箱即用的。幸运的是,有一个非常丰富的开源社区支持 React Native,不仅为库本身做出贡献,还发布了一些导出常见原生模块和视图组件的库。如果找不到满足需求的第一方或第三方库,您总是可以自己构建。

在这一章中,我们将涵盖一些关于在两个平台上暴露自定义原生功能的方法,无论是 API 还是视图组件。

在这些配方中,我们将使用原生部分中的大量生成的代码。本章中提供的代码块将像以前的章节一样,继续显示特定步骤中使用的所有代码,无论是我们添加的还是生成的,除非另有说明。这旨在减轻理解代码片段的上下文的负担,并在需要进一步解释这些生成的代码片段时促进讨论。

暴露自定义 iOS 模块

当您开始开发更有趣和复杂的 React Native 应用程序时,可能会达到一个只能在本地层执行某些代码(或显着改进)的点。这允许在本地层执行比 JavaScript 更快的数据处理,并访问某些本地功能,否则这些功能不会暴露,例如文件 I/O,或者利用 React Native 应用程序中其他应用程序或库中的现有本地代码。

这个示例将引导您执行一些本地 Objective-C 或 Swift 代码并与 JavaScript 层进行通信的过程。我们将构建一个本地的HelloManager模块来向用户问候。我们还将展示如何执行本地的 Objective-C 和 Swift 代码,传入参数,并展示与 UI(或 JavaScript)层进行多种通信的方式。

准备工作

对于这个示例,我们需要一个新的空的纯 React Native 应用程序。让我们称之为NativeModuleApp

在这个示例中,我们还将使用react-native-button库。这个库将允许我们使用比 React Native 对应组件更复杂的Button组件。它可以使用npm进行安装:

npm install react-native-button --save

或者可以使用yarn进行安装:

yarn add react-native-button

如何做到...

  1. 我们将从在 Xcode 中打开 iOS 项目开始。项目文件的文件扩展名为.xcodeproj,位于项目根目录的ios/目录中。在我们的情况下,文件将被称为NativeModuleApp.xcodeproj

  2. 我们需要通过选择并右键单击与项目名称匹配的组/文件夹来创建一个新文件,然后点击 New File...,如下所示:

  1. 我们将制作一个 Cocoa 类,所以选择 Cocoa Class 并点击 Next。

  2. 我们将使用HelloManager作为类名,并将子类设置为 NSObject,语言设置为 Objective-C,如下所示:

  1. 点击 Next 后,我们将被提示选择新类的目录。我们希望将其保存到NativeModuleApp目录中。

  2. 创建这个新的 Cocoa 类已经向项目中添加了两个新文件:一个头文件(HelloManager.h)和一个实现文件(HelloManager.m)。

  3. 在头文件(HelloManager.h)中,您应该看到一些生成的代码来实现新的HelloManager协议。我们还需要导入 React 的RCTBridgeModule库。文件最终应该看起来像这样:

#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>

@interface HelloManager : NSObject <RCTBridgeModule>

@end
  1. 实现文件(HelloManager.m)包含了我们模块的功能。为了让我们的 React Native 应用能够从 JavaScript 层访问这个模块,我们需要在 React Bridge 中注册它。这是通过在@implementation标签后添加RCT_EXPORT_MODULE()来完成的。还要注意,头文件也应该已经被导入到这个文件中:
#import "HelloManager.h"

@implementation HelloManager
RCT_EXPORT_MODULE();

@end
  1. 我们需要添加我们将要导出到 React Native 应用的函数。我们将创建一个greetUser方法,它将接受两个参数,nameisAdmin。这些参数将用于使用字符串连接创建问候消息,然后通过callback发送回 JavaScript 层:
#import "HelloManager.h"

@implementation HelloManager
RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(
 greetUser: (NSString *)name isAdmin:(BOOL *)isAdmin callback: (RCTResponseSenderBlock) callback
) {
 NSString *greeting =
 [NSString stringWithFormat:
 @"Welcome %@, you %@ an administrator.", name, isAdmin ? @"are" : @"are not"];

 callback(@[greeting]);
}

@end
  1. 我们准备切换到 JavaScript 层,这将有一个 UI,将调用我们刚刚创建的原生HelloManager greetUser方法,然后显示其输出。幸运的是,React Native 桥为我们完成了所有繁重的工作,并留下了一个简单易用的 JavaScript 对象,模仿了NativeModules API。在这个例子中,我们将使用TextInputSwitch来为原生模块方法提供nameisAdmin值。让我们从App.js中开始导入:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
  NativeModules,
  TextInput,
  Switch
} from 'react-native';
import Button from 'react-native-button';
  1. 我们可以使用我们导入的NativeModules组件来从原生层获取我们创建的HelloManager协议:
const HelloManager = NativeModules.HelloManager; 
  1. 让我们创建App组件并定义初始的state对象。我们将添加一个greetingMessage属性来保存从原生模块接收到的消息,userName来存储输入的用户名,以及一个isAdmin布尔值来表示用户是否是管理员:
export default class App extends Component {
  state = {
    greetingMessage: null,
    userName: null,
    isAdmin: false
  }
  // Defined on following steps
}
  1. 我们准备开始构建render方法。首先,我们需要一个TextInput组件来从用户那里获取用户名,以及一个Switch组件来切换isAdmin状态:
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.label}>
          Enter User Name
        </Text>
        <TextInput
          ref="userName"
          autoCorrect={false}
          style={styles.inputField}
          placeholder="User Name"
          onChangeText={(text) => this.setState({ userName: text }) }
        />
        <Text style={styles.label}>
          Admin
        </Text>
        <Switch style={styles.radio}
          value={this.state.isAdmin}
          onValueChange={(value) =>
            this.setState({ isAdmin: value })
          }
        />

        // Continued below
      </View>
    );
  }
  1. UI 还需要Button来提交回调到原生模块,以及一个Text组件来显示从原生模块返回的消息:
  render() {
    return (
      // Defined above.
 <Button
 disabled={!this.state.userName}
 style={[
 styles.buttonStyle,
 !this.state.userName ? styles.disabled : null
 ]}
 onPress={this.greetUser}
 >
 Greet (callback)
 </Button>
 <Text style={styles.label}>
 Response:
 </Text>
 <Text style={styles.message}>
 {this.state.greetingMessage}
 </Text>
      </View>
    );
  }
  1. 随着 UI 渲染必要的组件,我们准备将ButtononPress处理程序连接到本地层的调用。这个函数将displayResults类方法作为第三个参数传递,这是本地greetUser函数要使用的回调。我们将在下一步中定义displayResults
  greetUser = () => {
    HelloManager.greetUser(
      this.state.userName,
      this.state.isAdmin,
      this.displayResults
    );
  }
  1. displayResults需要做两件事:使用与组件关联的refsblur TextInput,并将greetingMessage设置为从本地模块返回的results
  displayResults = (results) => {
    this.refs.userName.blur();
    this.setState({ greetingMessage: results });
  }
  1. 最后一步是向布局添加样式并设计应用程序的样式:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  inputField:{
    padding: 20,
    fontSize: 30
  },
  label: {
    fontSize: 18,
    marginTop: 18,
    textAlign: 'center',
  },
  radio: {
    marginBottom: 20
  },
  buttonStyle: {
    padding: 20,
    backgroundColor: '#1DA1F2',
    color: '#fff',
    fontSize: 18
  },
  message: {
    fontSize: 22,
    marginLeft: 50,
    marginRight: 50,
  },
  disabled: {
    backgroundColor: '#3C3C3C'
  }
});
  1. 我们现在有一个可以直接与本地 iOS 层通信的工作 React Native 应用程序:

工作原理...

我们在这个教程中构建的应用程序将成为本章后续许多教程的基础。这也是 Facebook 用来实现许多捆绑的 React Native API 的方法。

在未来,有几个重要的概念需要牢记。我们想要在 JavaScript 层中使用的任何本地模块类都必须扩展RCTBridgeModule,因为它包含了将我们的类注册到 React Native 桥上的功能。我们使用RCT_EXPORT_MODULE方法调用注册我们的类,一旦模块被注册,就会注册模块上的方法。注册模块以及其相应的方法和属性是允许我们从 JavaScript 层与本地层进行接口的。

当按下按钮时,将执行greetUser方法。这个函数反过来调用HelloManager.greetUser,传递state中的userNameisAdmin属性以及displayResults函数作为回调。displayResults设置state上的新greetingMessage,导致 UI 刷新并显示消息。

另请参阅

渲染自定义 iOS 视图组件

在我们的 React Native 应用程序中,在本地层执行代码时利用设备的处理能力非常重要,同样重要的是利用其渲染能力来显示本地 UI 组件。 React Native 可以在应用程序中呈现任何UIView的 UI 组件实现。 这些组件可以是列表、表单字段、表格、图形等等。

对于这个教程,我们将创建一个名为NativeUIComponent的 React Native 应用程序。

在这个教程中,我们将采用原生的UIButton并将其公开为 React Native 视图组件。 您将能够设置按钮标签并附加一个处理程序以在按下按钮时执行。

如何做...

  1. 让我们从在 Xcode 中打开 iOS 项目开始。 项目文件位于项目的ios/目录中,应该被称为NativeUIComponent.xcodeproj

  2. 选择并右键单击与项目名称匹配的组,并单击“新建文件...”:

  1. 我们将创建一个 Cocoa 类,所以选择Cocoa Class并单击下一步

  2. 我们将创建一个按钮,所以让我们将类命名为Button,将Subclass of设置为UIView,将Language设置为Objective-C

  1. 点击“下一步”后,我们将被提示选择新类的目录。 我们要将其保存到NativeUIComponent目录以创建该类。

  2. 我们还需要一个ButtonViewManager类。 您可以将步骤 2 到 5 重复使用ButtonViewManager作为类名和RCTViewManager作为子类。

  3. 首先,我们将实现我们的Button UI 类。 在头文件(Button.h)中,我们将从 React 中导入RCTComponent.h并添加一个onTap属性来连接我们的点击事件:

#import <UIKit/UIKit.h>
#import "React/RCTComponent.h"

@interface Button : UIView

@property (nonatomic, copy) RCTBubblingEventBlock onTap;

@end
  1. 让我们在实现文件(Button.m)上工作。 我们将首先创建我们的UIButton实例和将保存按钮标签的字符串的引用:
#import "Button.h"
#import "React/UIView+React.h"

@implementation Button {
  UIButton *_button;
  NSString *_buttonText;
}

// Defined in following steps
  1. 桥梁将寻找buttonText属性的 setter。 这是我们将设置UIButton实例标题字段的地方:
-(void) setButtonText:(NSString *)buttonText {
  NSLog(@"Set text %@", buttonText);
  _buttonText = buttonText;
  if(_button) {
    [_button setTitle:
     buttonText forState:UIControlStateNormal];
    [_button sizeToFit];
  }
}
  1. 我们的Button将从 React Native 应用程序接受一个onTap事件处理程序。我们需要通过动作选择器将其连接到我们的UIButton实例:
- (IBAction)onButtonTap:(id)sender {
  self.onTap(@{});
}
  1. 我们需要实例化UIButton并将其放置在 React Subview中。我们将称这个方法为layoutSubviews
-(void) layoutSubviews {
  [super layoutSubviews];
  if( _button == nil) {
    _button =
    [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [_button addTarget:self action:@selector(onButtonTap:)
      forControlEvents:UIControlEventTouchUpInside];
    [_button setTitle:
     _buttonText forState:UIControlStateNormal];
    [_button sizeToFit];
    [self insertSubview:_button atIndex:0];
  }
}
  1. 让我们在ButtonViewManager.h头文件中导入 React RCTViewManager
#import "React/RCTViewManager.h"

@interface ButtonViewManager : RCTViewManager

@end
  1. 现在我们需要实现我们的ButtonViewManager,它将与我们的 React Native 应用程序进行交互。让我们在实现文件(ButtonViewManager.m)上工作,使其发生。我们使用RCT_EXPORT_VIEW_PROPERTY来传递buttonText属性和onTap方法到 React Native 层:
#import "ButtonViewManager.h"
#import "Button.h"
#import "React/UIView+React.h"

@implementation ButtonViewManager
RCT_EXPORT_MODULE()

- (UIView *)view {
  Button *button = [[Button alloc] init];
  return button;
}

RCT_EXPORT_VIEW_PROPERTY(buttonText, NSString);
RCT_EXPORT_VIEW_PROPERTY(onTap, RCTBubblingEventBlock);

@end
  1. 我们准备切换到 React Native 层。我们需要一个自定义的Button组件,所以让我们在项目的根目录下创建一个新的components文件夹,并在其中创建一个新的Button.js文件。我们还需要从 React Native 中导入requireNativeComponent组件,以便与我们的原生 UI 组件进行交互:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View
} from 'react-native';
import Button from './components/Button';
  1. Button组件将通过requireNativeComponent React Native 助手获取我们之前创建的原生Button模块。调用以字符串作为组件在 React Native 层中的名称作为第一个参数,并且第二个参数将Button组件在文件中,有效地将两者连接在一起:
export default class Button extends Component {
  render() {
    return <ButtonView {...this.properties} />;
  }
}

const ButtonView = requireNativeComponent('ButtonView', Button);
  1. 我们准备在项目的根目录下的App.js文件中构建主要的App组件。我们将从导入开始,其中将包括我们在最后两个步骤中创建的Button组件:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View
} from 'react-native';
import Button from './components/Button';
  1. 让我们定义App组件和初始的state对象。count属性将跟踪Button组件被按下的次数:
export default class App extends Component {
 state = {
  count: 0
 }
 // Defined on following steps
}
  1. 我们准备好定义render方法,它将由Button组件和用于显示当前按钮按下计数的Text元素组成:
  render() {
    return (
      <View style={styles.container}>
        <Button buttonText="Click Me!"
        onTap={this.handleButtonTap}
        style={styles.button}
      />
        <Text>Button Pressed Count: {this.state.count}</Text>
      </View>
    );
  }
  1. 您可能还记得我们创建的Button组件具有一个onTap属性,它接受一个回调函数。在这种情况下,我们将使用此函数来增加state上的计数器:
  handleButtonTap = () => {
    this.setState({
      count: this.state.count + 1
    });
  }
  1. 让我们用一些基本的样式结束这个教程:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  button: {
    height: 40,
    width: 80
  }
});
  1. 应用程序完成了!当按下按钮时,将执行传递给onTap的函数,将计数器增加一:

工作原理...

在这个配方中,我们暴露了一个基本的原生 UI 组件。这是创建 React Native 内置的所有 UI 组件(例如SliderPickerListView)的方法。

创建 UI 组件最重要的要求是,你的ViewManager要扩展RCTViewManager并返回一个UIView的实例。在我们的情况下,我们用 React 特定的UIView扩展来包装UIButton,这样可以提高我们布局和样式组件的能力。

下一个重要因素是发送属性和对组件事件做出反应。在第 13 步中,我们使用了 React Native 提供的RCT_EXPORT_VIEW_PROPERTY方法来注册来自 JavaScript 层的buttonTextonTap视图属性,这些属性将传递给Button组件。然后创建并返回Button组件以在 JavaScript 层中使用:

- (UIView *)view {
  Button *button = [[Button alloc] init];
  return button;
}

暴露自定义的 Android 模块

通常,你会发现 React Native 应用程序需要与原生 iOS 和 Android 代码进行接口。在讨论了集成原生 iOS 模块之后,现在是时候覆盖 Android 中的等效配方了。

这个配方将带领我们编写我们的第一个 Android 原生模块。我们将创建一个HelloManager原生模块,其中包含一个greetUser方法,该方法接受name和一个isAdmin布尔值作为参数,然后返回一个我们将在 UI 中显示的问候消息。

准备工作

对于这个配方,我们需要创建另一个纯 React Native 应用程序。我们也将这个项目命名为NativeModuleApp

我们还将再次使用react-native-button库,可以使用npm安装:

npm install react-native-button --save

或者,也可以使用yarn进行安装:

yarn add react-native-button

如何做...

  1. 我们将首先在 Android Studio 中打开新项目的 Android 代码。从 Android Studio 的欢迎屏幕,你可以选择打开现有的 Android Studio 项目,然后选择项目文件夹内的android目录。

  2. 项目加载完成后,让我们在 Android Studio 左侧打开项目资源管理器(即目录树),并展开包结构以找到 Java 源文件,它应该位于app/java/com.nativemoduleapp中。该文件夹应该已经有两个.java文件,MainActivityMainApplication

  1. 右键单击 com.nativemoduleapp 包,选择 New | Java Class,并命名类为HelloManager。还要确保将 Kind 字段设置为 Class:

  2. 我们还需要在同一个目录中创建一个HelloPackage类。您可以重复步骤 2 和 3 来创建这个类,只需应用新名称并保持 Kind 字段设置为 Class。

  3. 让我们从实现我们的HelloManager本机模块开始。我们将从package名称和我们在此文件中需要的依赖项开始:

package com.nativemoduleapp;

import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
  1. ReactContextBaseJavaModule是所有 React Native 模块的基类,因此我们将创建HelloManager类作为其子类。我们还需要定义一个getName方法,该方法用于向 React Native 桥注册本机模块。这是与 iOS 本机模块实现的一个区别,因为那些是通过类名定义的:
public class HelloManager extends ReactContextBaseJavaModule {
  public HelloManager(ReactApplicationContext reactContext) {
    super(reactContext);
  }

  @Override
  public String getName() {
    return "HelloManager";
  }
}
  1. 现在我们已经设置好了HelloManager本机模块,是时候向其中添加greetUser方法了,该方法将期望作为参数nameisAdmin和将执行以将消息发送到 React Native 层的回调:
public class HelloManager extends ReactContextBaseJavaModule {
  // Defined in previous steps  

 @ReactMethod
 public void greetUser(String name, Boolean isAdmin, Callback callback) {
 System.out.println("User Name: " + name + ", Administrator: " + (isAdmin ? "Yes" : "No"));
 String greeting = "Welcome " + name + ", you " + (isAdmin ? "are" : "are not") + " an administrator";

 callback.invoke(greeting);
 }
}
  1. Android 独有的另一个步骤是必须将本机模块注册到应用程序中,这是一个两步过程。第一步是将我们的HelloManager模块添加到之前创建的HelloPackage类中。我们将从HelloPackage.java的依赖项开始:
package com.nativemoduleapp;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
  1. HelloPackage的实现只是遵循官方文档提供的模式(facebook.github.io/react-native/docs/native-modules-android.html)。这里最重要的部分是对modules.add的调用,其中传入了带有reactContext作为参数的HelloManager的新实例:
public class HelloPackage implements ReactPackage {

  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }

  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    List<NativeModule> modules = new ArrayList<>();
    modules.add(new HelloManager(reactContext));

    return modules;
  }
}
  1. 在将本机模块注册到 React Native 应用程序的第二步是将HelloPackage添加到MainApplication模块中。这里的大部分代码都是由 React Native 引导过程生成的。getPackages方法需要更新,以将new MainReactPackage()new HelloPackage()作为传递给Arrays.asList的参数:
package com.nativemoduleapp;

import android.app.Application;

import com.facebook.react.ReactApplication;
import com.facebook.react.ReactNativeHost;
import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainReactPackage;
import com.facebook.soloader.SoLoader;

import java.util.Arrays;
import java.util.List;

public class MainApplication extends Application implements ReactApplication {

 private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    public boolean getUseDeveloperSupport() {
      return BuildConfig.DEBUG;
    }

 @Override
 protected List<ReactPackage> getPackages() {
 return Arrays.asList(
 new MainReactPackage(),
 new HelloPackage()
 );
 }

    @Override
    protected String getJSMainModuleName() {
      return "index";
    }
  };

  @Override
  public ReactNativeHost getReactNativeHost() {
    return mReactNativeHost;
  }

  @Override
  public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
  }
}
  1. 我们在这个配方的 Java 部分已经完成了。 我们需要构建我们的 UI,这将调用本机的HelloManager greetUser方法并显示其输出。 在这个例子中,我们将使用TextInputSwitch来提供本机模块方法的nameisAdmin值。 这与我们在暴露自定义 iOS 模块配方中在 iOS 上实现的功能相同。 让我们开始构建App.js,首先是我们需要的依赖项:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View,
  NativeModules,
  TextInput,
  Switch,
  DeviceEventEmitter
} from 'react-native';
import Button from 'react-native-button';
  1. 我们需要引用存储在导入的NativeModules组件上的HelloManager对象:
const { HelloManager } = NativeModules;
  1. 让我们创建App类和初始state
export default class App extends Component {
  state = {
    userName: null,
    greetingMessage: null,
    isAdmin: false
  }
}
  1. 我们准备定义组件的render函数。 这段代码将不会被详细描述,因为它基本上与本章开头的暴露自定义 iOS 模块配方中定义的render函数相同:
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.label}>
          Enter User Name
        </Text>
        <TextInput
          ref="userName"
          autoCorrect={false}
          style={styles.inputField}
          placeholder="User Name"
          onChangeText={(text) => this.setState({ userName: text })
          }
        />
        <Text style={styles.label}>
          Admin
        </Text>
        <Switch
          style={styles.radio}
          onValueChange={
            value => this.setState({ isAdmin: value })
          }
          value={this.state.isAdmin}
        />
       <Button
          disabled={!this.state.userName}
          style={[
            styles.buttonStyle,
            !this.state.userName ? styles.disabled : null
          ]}
          onPress={this.greetUser}
        >
          Greet
        </Button>
        <Text style={styles.label}>
          Response:
        </Text>
        <Text style={styles.message}>
          {this.state.greetingMessage}
        </Text>
      </View>
    );
  }
  1. 随着 UI 渲染必要的组件,我们现在需要将ButtononPress处理程序连接起来,通过HelloManager.greetUser进行本机调用:
  updateGreetingMessage = (result) => {
    this.setState({
      greetingMessage: result
    });
  }

  greetUser = () => {
    this.refs.userName.blur();
    HelloManager.greetUser(
      this.state.userName,
      this.state.isAdmin,
      this.updateGreetingMessage
    );
  }
  1. 我们将添加样式来布局和设计应用程序。 再次强调,这些样式与本章开头的暴露自定义 iOS 模块配方中使用的样式相同:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  inputField:{
    padding: 20,
    fontSize: 30,
    width: 200
  },
  label: {
    fontSize: 18,
    marginTop: 18,
    textAlign: 'center',
  },
  radio: {
    marginBottom: 20
  },
  buttonStyle: {
    padding: 20,
    backgroundColor: '#1DA1F2',
    color: '#fff',
    fontSize: 18
  },
  message: {
    fontSize: 22,
    marginLeft: 50,
    marginRight: 50,
  },
  disabled: {
    backgroundColor: '#3C3C3C'
  }
});
  1. 最终的应用程序应该类似于以下截图:

它是如何工作的...

这个配方涵盖了我们将在未来的配方中添加本机 Android 模块的大部分基础知识。 所有本机模块类都需要扩展ReactContextBaseJavaModule,实现构造函数,并定义getName方法。 所有应该暴露给 React Native 层的方法都需要有@ReactMethod注解。 创建 React Native Android 本机模块的开销比 iOS 更大,因为您还必须将模块包装在实现ReactPackage的类中(在这个配方中,那就是HelloPackage模块),并将包注册到 React Native 项目中。 这是在步骤 7 和 8 中完成的。

在配方的 JavaScript 部分,当用户按下Button组件时,将执行greetUser函数。 这反过来又调用HelloManager.greetUser,并传递state中的userNameisAdmin属性以及updateGreetingMessage方法作为回调。 updateGreetingMessagestate上设置新的greetingMessage,导致 UI 刷新并显示消息。

渲染自定义 Android 视图组件

迄今为止,React Native 之所以如此受欢迎的一个原因是它能够渲染真正的本机 UI 组件。在 Android 上使用本机 UI 组件,我们不仅能够利用 GPU 渲染能力,还能获得本机组件的本机外观和感觉,包括本机字体、颜色和动画。在 Android 上,Web 和混合应用程序使用 CSS polyfills 来模拟本机动画,但在 React Native 中,我们可以得到真正的东西。

我们需要一个新的纯 React Native 应用程序来完成这个示例。让我们将其命名为NativeUIComponent。在这个示例中,我们将采用本机Button并将其公开为 React Native 视图组件。

如何做...

  1. 让我们从在 Android Studio 中打开 Android 项目开始。在 Android Studio 欢迎屏幕上,选择打开现有的 Android Studio 项目,并打开项目的android目录。

  2. 打开项目资源管理器,并展开包结构,直到您可以看到 Java 源文件(例如,app/java/com.nativeuicomponent):

  1. 右键单击包,然后选择 New | Java Class。使用ButtonViewManager作为类名,并将 Kind 字段设置为 Class。

  2. 使用相同的方法也创建一个ButtonPackage类。

  3. 让我们开始实现我们的ButtonViewManager类,它必须是SimpleViewManager<View>的子类。我们将从导入开始,并定义类本身:

package com.nativeuicomponent;

import android.view.View;
import android.widget.Button;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;

public class ButtonViewManager extends SimpleViewManager<Button> implements View.OnClickListener {
  // Defined on following steps
}

文件类名ButtonViewManager遵循 Android 命名约定,将后缀ViewManager添加到任何View组件。

  1. 让我们从getName方法开始类定义,该方法返回我们为组件分配的字符串名称,在本例中为ButtonView
public class ButtonViewManager extends SimpleViewManager<Button> implements View.OnClickListener{
 @Override
 public String getName() {
 return "ButtonView";
 }

  // Defined on following steps.
}
  1. createViewInstance方法是必需的,用于定义 React 应该如何初始化模块:
  @Override
  protected Button createViewInstance(ThemedReactContext reactContext) {
    Button button = new Button(reactContext);
    button.setOnClickListener(this);
    return button;
  }
  1. setButtonText将从 React Native 元素的属性中使用,以设置按钮上的文本:
  @ReactProp(name = "buttonText")
  public void setButtonText(Button button, String buttonText) {
    button.setText(buttonText);
  }
  1. onClick方法定义了按钮按下时会发生什么。此方法使用RCTEventEmitter来处理从 React Native 层接收事件:
  @Override
  public void onClick(View v) {
    WritableMap map = Arguments.createMap();
    ReactContext reactContext = (ReactContext) v.getContext();
    reactContext.getJSModule(RCTEventEmitter.class).receiveEvent(v.getId(), "topChange", map);
  }
  1. 就像在上一个示例中一样,我们需要将ButtonViewManager添加到ButtonPackage;但是,这次,我们将其定义为ViewManager而不是NativeModule
package com.nativeuicomponent;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class ButtonPackage implements ReactPackage {
  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Arrays.<ViewManager>asList(new ButtonViewManager());
  }

  @Override
  public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
    return Collections.emptyList();
  }
}
  1. Java 层的最后一步是将ButtonPackage添加到MainApplicationMainApplication.java中已经有相当多的样板代码,我们只需要更改getPackages方法:
    @Override
    protected List<ReactPackage> getPackages() {
      return Arrays.<ReactPackage>asList(
        new MainReactPackage(),
        new ButtonPackage()
      );
    }
  1. 切换到 JavaScript 层,让我们构建我们的 React Native 应用程序。首先,在项目的根目录中的components/Button.js中创建一个新的Button组件。这是原生按钮将存在于应用程序的 React Native 层内。render方法使用原生按钮作为ButtonView,我们将在下一步中定义:
import React, { Component } from 'react';
import { requireNativeComponent, View } from 'react-native';

export default class Button extends Component {
  onChange = (event) => {
    if (this.properties.onTap) {
      this.properties.onTap(event.nativeEvent.message);
    }
  }

  render() {
    return(
      <ButtonView
        {...this.properties}
        onChange={this.onChange}
      />
    );
  }
}
  1. 我们可以使用requireNativeComponent助手将原生按钮创建为 React Native 组件,它接受三个参数:字符串ButtonView来定义组件名称,上一步中定义的Button组件,以及选项对象。有关此对象的更多信息,请参阅本教程末尾的*它是如何工作的...*部分:
const ButtonView = requireNativeComponent(
  'ButtonView',
  Button, {
    nativeOnly: {
      onChange: true
    }
  }
);
  1. 我们准备好定义App类。让我们从依赖项开始,包括先前创建的Button组件:
import React, { Component } from 'react';
import {
  StyleSheet,
  Text,
  View
} from 'react-native';

import Button from './components/Button';
  1. 本教程中的App组件本质上与本章前面的渲染自定义 iOS 视图组件教程相同。当按下Button组件时,自定义的onTap属性被触发,将1添加到statecount属性中。
export default class App extends Component {
  state = {
    count: 0
  }

  onButtonTap = () => {
    this.setState({
      count : this.state.count + 1
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <Button buttonText="Press Me!"
          onTap={this.onButtonTap}
          style={styles.button}
        />
        <Text>
          Button Pressed Count: {this.state.count}
        </Text>
      </View>
    );
  }
}
  1. 让我们为布局添加一些样式,调整应用的 UI 大小:
const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  button: {
    height: 40,
    width: 150
  }
});
  1. 最终的应用程序应该类似于以下截图:

它是如何工作的...

当定义原生视图时,就像我们在ButtonViewManager类中所做的那样,它必须扩展SimpleViewManager并呈现一个扩展View的类型。在我们的教程中,我们呈现了一个Button视图,并使用了@ReactProp注释来定义属性。当我们需要与 JavaScript 层通信时,我们从原生组件触发一个事件,这是我们在本教程的步骤 9中实现的。

步骤 12中,我们创建了一个onChange监听器,它将执行从 Android 层传递的事件处理程序(event.nativeEvent.message)。

关于在步骤 13中使用nativeOnly选项,来自 React Native 文档:

有时您会有一些特殊属性,您需要为原生组件公开,但实际上不希望它们成为关联的 React 组件 API 的一部分。例如,Switch具有用于原始原生事件的自定义onChange处理程序,并公开一个onValueChange处理程序属性,该属性仅使用布尔值调用,而不是原始事件。由于您不希望这些仅限于原生的属性成为 API 的一部分,因此您不希望将它们放在propTypes中,但如果不这样做,就会出错。解决方案很简单,只需通过nativeOnly选项调用它们。