ReactNative-秘籍第二版-全-
ReactNative 秘籍第二版(全)
原文:
zh.annas-archive.org/md5/12592741083b1cbc7e657e9f51045dce
译者:飞龙
前言
本书的部分内容需要 macOS 才能使用的软件。虽然 React Native 开发可以在 Windows 机器上完成,但是某些方面,比如在 iOS 设备和 iOS 模拟器上运行应用程序,或者使用 Xcode 编辑本地代码,只能在 Mac 上完成。
开发人员有许多种方式来构建 iOS 或 Android 应用程序。React Native 是构建混合移动应用程序的最稳定、性能最佳和开发人员友好的选择之一。使用 React Native 开发移动应用程序允许开发人员在单个代码库中构建 iOS 和 Android 应用程序,并且可以在两个平台之间共享代码。
更好的是,有经验在 React 中构建 Web 应用程序的开发人员将会处于领先地位,因为许多相同的模式和约定都被延续到 React Native 中。如果您有使用 React 或其他基于模型、视图、组件(MVC)的框架构建 Web 应用程序的经验,那么在 React Native 中构建移动应用程序会让您感到宾至如归。
目前有两种广泛使用的方法来创建和开发 React Native 应用程序:使用 React Native CLI 进行纯 React Native 开发,或者使用 Expo(www.expo.io),这是一套全面的工具、库和服务,用于开发 React Native 应用程序。除非您需要访问 React Native 的某些通常更高级的功能,Expo 是我推荐的 React Native 开发工具。Expo 具有许多功能,可以改善开发体验,例如通过 Expo SDK 访问更多本地功能,更灵活和友好的 CLI,以及用于常见开发任务的基于浏览器的 GUI。这就是为什么本书中不需要纯 React Native 的所有食谱都使用 Expo 实现。有关 React Native 和 Expo 之间的区别,请查看第十章“应用程序工作流程和第三方插件”中的React Native 开发工具部分。
本书旨在成为构建各种应用程序时常见问题解决方案的参考手册。每一章都以一系列逐步说明的食谱呈现,每个食谱都解释了如何构建整个应用程序的单个功能。
React Native 是一种不断发展的语言。在撰写本文时,它仍处于开发生命周期的 0.5x 阶段,因此未来几个月甚至几年可能会发生一些变化。最佳实践可能会变成陈旧的想法,或者在这里突出显示的开源软件包可能会不受青睐。本书中的每个示例都经过了更新和修订,以反映开发过程的更新并提高清晰度。我已尽力使本文尽可能保持最新,但技术发展迅速,因此一本书无法单独跟上。本书中涵盖的所有代码存储在 GitHub 上。如果您发现这里的代码有任何问题,请提交问题。或者,如果您有更好的方法来做某事,请考虑提交拉取请求!
无论这本书中的任何内容有何更新,您都可以在 GitHub 存储库中找到详细信息和更改。
希望您在 React Native 的学习过程中找到这本书有所帮助。祝您开发愉快!
这本书适合谁
本书旨在面向初学者到中级水平的 React Native 开发人员。即使您没有太多的 Web 开发经验,本书中的 JavaScript 也希望不会超出您的理解范围。我尽量避免复杂性,以便将重点放在每个示例中所教授的课程上。
本书还假设开发人员使用运行 macOS 的计算机。虽然在 Windows 或 Linux 上开发 React Native 应用程序在技术上是可能的,但有许多限制使 macOS 机器在 React Native 开发中更可取,包括通过 Xcode 与本机 iOS 代码一起工作,在 iOS 模拟器上运行 iOS 代码,并使用最强大的 React Native 应用程序开发工具。
这本书涵盖了什么
第一章,设置您的环境,介绍了我们将安装的不同软件,以便开始开发 React Native 应用程序。
第二章,创建一个简单的 React Native 应用,介绍了构建布局和导航的基础知识。本章中的示例作为 React Native 开发的入门,并涵盖了大多数移动应用程序中的基本功能。
第三章《实现复杂用户界面-第一部分》涵盖了包括自定义字体和自定义可重用主题在内的功能。
第四章《实现复杂用户界面-第二部分》继续基于 UI 功能的更多技巧。它涵盖了处理屏幕方向变化和构建用户表单等功能。
第五章《实现复杂用户界面-第三部分》涵盖了构建复杂 UI 时可能需要的其他常见功能。本章涵盖了添加地图支持、实现基于浏览器的身份验证和创建音频播放器。
第六章《为您的应用添加基本动画》涵盖了创建动画的基础知识。
第七章《为您的应用添加高级动画》继续在上一章的基础上构建,增加了更高级的功能。
第八章《处理应用逻辑和数据》向我们介绍了处理数据的应用程序的构建。我们将涵盖一些主题,包括本地存储数据和优雅地处理网络丢失。
第九章《实现 Redux》涵盖了使用 Redux 库实现 Flux 数据模式。Redux 是处理 React 应用程序中的数据流的经过实战检验的方法,在 React Native 中同样有效。
第十章《应用程序工作流程和第三方插件》涵盖了开发人员可以使用的构建应用程序的不同方法,以及如何使用开源代码构建应用程序。这也将涵盖使用纯 React Native(使用 React Native CLI)构建应用程序和使用 Expo(一个全面的开发工具)构建应用程序之间的区别。
第十一章《添加原生功能-第一部分》涵盖了在 React Native 应用程序中使用原生 iOS 和 Android 代码的基础知识。
第十二章《添加原生功能-第二部分》涵盖了在 React Native 和原生层之间进行通信的更复杂的技术。
第十三章《与原生应用集成》涵盖了将 React Native 与现有原生应用集成的内容。并非每个应用都可以从头开始构建。这些技巧对于需要将他们的工作与已经在应用商店中的应用集成的开发人员应该是有帮助的。
第十四章部署您的应用程序介绍了部署 React Native 应用程序的基本流程,以及使用 HockeyApp 跟踪应用程序指标的详细信息。
第十五章,优化您的应用程序的性能,介绍了编写高性能 React Native 代码的一些技巧和最佳实践。
为了充分利用本书
假设您具有以下理解水平:
-
您具有一些基本的编程知识。
-
您熟悉 Web 开发基础知识。
如果您还具有以下内容,将会很有帮助:
-
具有 React、Vue 或 Angular 经验
-
至少具有中级水平的 JavaScript 经验
下载示例代码文件
您可以从www.packt.com的帐户中下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问www.packt.com/support并注册,将文件直接发送到您的邮箱。
您可以按照以下步骤下载代码文件:
-
登录或注册www.packt.com。
-
选择“支持”选项卡。
-
点击“代码下载和勘误”。
-
在搜索框中输入书名,然后按照屏幕上的说明操作。
文件下载后,请确保使用最新版本的解压缩或提取文件夹:
-
Windows 上使用 WinRAR/7-Zip
-
Mac 上使用 Zipeg/iZip/UnRarX
-
Linux 上使用 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上,网址为github.com/warlyware/react-native-cookbook
。如果代码有更新,将在现有的 GitHub 存储库上进行更新。
我们还有其他代码包,来自我们丰富的图书和视频目录,可在github.com/PacktPublishing/
上找到。去看看吧!
下载彩色图片
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图片。您可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781788991926_ColorImages.pdf
。
使用的约定
本书中使用了许多文本约定。
CodeInText
:表示文本中的代码词,数据库表名,文件夹名,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 用户名。这是一个例子:“我们将使用一个带有一个liked
布尔属性的state
对象来实现这个目的。”
代码块设置如下:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// We'll define the content on step 6
}
当我们希望引起您对代码块的特定部分的注意时,相关行或项目会以粗体显示:
onst styles = StyleSheet.create({
container: {
flex: 1,
},
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
alignItems: 'center',
},
任何命令行输入或输出都以以下方式编写:
expo init project-name
粗体:表示一个新术语,一个重要的词,或者你在屏幕上看到的词。例如,菜单或对话框中的单词会以这种方式出现在文本中。这是一个例子:“单击 Components 选项卡,并从提供的模拟器列表中安装一个模拟器。”
警告或重要说明会以这种方式出现。提示和技巧会以这种方式出现。
部分
在本书中,您会发现一些经常出现的标题(Getting ready,How to do it...,How it works...,There's more...,和 See also)。
为了清晰地说明如何完成一个食谱,使用以下部分:
准备工作
这一部分告诉您在食谱中可以期待什么,并描述如何设置食谱所需的任何软件或任何初步设置。
如何做…
这一部分包含了遵循食谱所需的步骤。
它是如何工作的…
这一部分通常包括对前一部分发生的事情的详细解释。
还有更多…
这一部分包含了有关食谱的额外信息,以使您对食谱更加了解。
另请参阅
这一部分为食谱提供了有用的链接到其他有关食谱的信息。
第一章:设置您的环境
自第一版以来,React Native 生态系统已经发生了相当大的变化。特别是,开源工具 Expo.io 已经简化了项目初始化和开发阶段,使得在 React Native 中的工作比 0.36 版本中更加愉快。
使用 Expo 工作流程,您将能够仅使用 JavaScript 构建本机 iOS 和 Android 应用程序,在 iOS 模拟器和 Android 模拟器中进行实时重新加载,并通过 Expo 应用程序轻松测试您的应用程序在任何真实设备上。除非您需要访问原生代码(比如,与来自单独代码库的遗留原生代码集成),否则您可以完全在 JavaScript 中开发应用程序,而无需使用 Xcode 或 Android Studio。如果您的项目最终发展成必须支持原生代码的应用程序,Expo 提供了将您的项目弹出的功能,这将使您的应用程序变成原生代码,以便在 Xcode 和 Android Studio 中使用。有关弹出您的 Expo 项目的更多信息,请参阅第十章,应用程序工作流程和第三方插件。
Expo 是一种很棒的方式,可以在 Android 和 iOS 设备上构建功能齐全的应用程序,而无需处理原生代码。让我们开始吧!
在本章中,我们将涵盖以下主题:
-
安装依赖项
-
初始化您的第一个应用程序
-
在模拟器/仿真器中运行您的应用程序
-
在真实设备上运行您的应用程序
技术要求
本章将涵盖您在本书中将要使用的工具的安装。它们包括:
-
Expo
-
Xcode(仅适用于 iOS 模拟器,仅限 macOS)
-
Android Studio
-
Node.js
-
看门人
安装依赖项
构建我们的第一个 React Native 应用程序的第一步是安装依赖项以开始。
安装 Xcode
如本章介绍的,Expo 为我们提供了一种工作流程,可以避免完全在 Xcode 和 Android Studio 中工作,因此我们可以完全使用 JavaScript 进行开发。但是,为了在 iOS 模拟器中运行您的应用程序,您需要安装 Xcode。
Xcode 需要 macOS,因此只有在 macOS 上才能在 iOS 模拟器中运行您的 React Native 应用程序。
Xcode 应该从 App Store 下载。您可以在 App Store 中搜索 Xcode,或使用以下链接:
itunes.apple.com/app/xcode/id497799835
.
Xcode 是一个庞大的下载,所以这部分需要一些时间。安装 Xcode 后,您可以通过 Finder 中的应用程序
文件夹运行它:
- 这是您启动 Xcode 时将看到的第一个屏幕。请注意,如果这是您第一次安装 Xcode,您将看不到最近的项目列在右侧:
- 从菜单栏中选择
Xcode | 偏好设置...
如下:
- 单击组件选项卡,并从提供的模拟器列表中安装一个模拟器:
- 安装完成后,您可以从菜单栏中打开模拟器:
Xcode | 打开开发人员工具
|模拟器
:
安装 Android Studio
Android Studio 附带官方的 Android 模拟器,这是 Expo 在开发过程中推荐使用的模拟器。
操作步骤如下...
-
从
developer.android.com/studio/
下载 Android Studio。 -
打开下载的文件,并将
Android Studio.app
图标拖动到应用程序
文件夹图标中:
-
安装完成后,我们需要更改 Android Studio 的偏好设置。打开 Android Studio,然后从系统栏中的 Android Studio 菜单中打开
偏好设置
。在偏好设置
子菜单中,选择外观和行为
|系统设置
|Android SDK
。在SDK 工具
选项卡下,确保您已安装某个版本的 Android SDK 构建工具,并且如果尚未安装,则安装它。 -
我们还需要通过编辑
~/.bash_profile
或~/.bashrc
将 Android SDK 位置添加到系统的PATH
中。您可以通过添加以下行来实现:
export PATH=$PATH:/Users/MY_USER_NAME/Library/Android/sdk
请务必将MY_USER_NAME
替换为您的系统用户名。
- 在 macOS 上,您还需要在
~/.bash_profile
或~/.bashrc
中的PATH
中添加platform-tools
。您可以通过添加以下行来实现:
PATH=$PATH:/Users/MY_USER_NAME/Library/Android/platform-tools
请务必将MY_USER_NAME
替换为您的系统用户名。
如果您以前从未编辑过.bash_profile
或.bashrc
文件,或者对PATH
不熟悉,您可以从以下资源获取有关它们的作用以及如何使用它们的更多信息:
-
www.rc.fas.harvard.edu/resources/documentation/editing-your-bashrc/
-
www.cyberciti.biz/faq/appleosx-bash-unix-change-set-path-environment-variable/
-
如果
PATH
已正确更新,则adb
命令应在终端中起作用。更改生效可能需要重新启动终端。 -
在安装 Android Studio 的新环境中,您将看到一个欢迎屏幕。开始一个新的应用程序以完全打开软件。然后,从窗口右上角的按钮中选择 AVD 管理器,如下面的步骤所示:
-
在打开的模态中按“创建虚拟设备”。
-
在“选择硬件”屏幕中选择一个设备,然后按“下一步”:
- 在“系统映像”屏幕的“推荐”选项卡下下载一个系统映像:
- 在最后一个屏幕上按“完成”,Android Studio 将创建您的新虚拟设备。可以通过按右上角按钮行中的播放按钮随时运行该设备:
在开发过程中在 Android 模拟器上运行您的应用程序,Expo 以前建议使用出色的第三方模拟器 Genymotion。然而,从 Expo 版本 29 开始,他们现在建议使用随 Android Studio 一起提供的官方模拟器。
您可以按照官方 Expo 文档中提供的逐步指南,确保 Android Studio 设置正确,以便与 Expo 开发工作流程正常工作。该指南可以在docs.expo.io/versions/latest/workflow/android-studio-emulator
找到。
这就是您开始使用 Expo 开发第一个 React Native 应用程序所需的所有设置!但是,对于纯 React Native 应用程序(非 Expo 应用程序),您需要执行一些额外的步骤。纯 React Native 应用程序开发将在第十章应用程序工作流和第三方插件中深入介绍。由于此设置过程涉及的内容较多且可能会更改,我建议参考官方指南。您可以在 React Native: 入门指南中找到这些说明,网址为facebook.github.io/react-native/docs/getting-started.html
,位于使用本地代码构建项目部分。
一旦模拟器打开,通过菜单栏选择您想要的 iOS 设备:硬件|设备| [IOS 版本] | [iOS 设备]。将来在模拟器中运行 Expo 应用程序时,应该会自动使用相同的设备。
如果您在终端中使用 Expo CLI 运行以下命令,即可启动应用程序:
expo start
该命令将构建您的应用程序并在 Web 浏览器中打开 Expo 开发者工具。在 Expo 开发者工具中,选择在 iOS 模拟器上运行。
还有更多...
一旦您在模拟器中启动了应用程序,您就可以在不从 Xcode 中打开模拟器的情况下按下在 iOS 模拟器上运行按钮。它还应该记住您的设备选择。从 Xcode 中打开模拟器提供了一种简单的方式来选择您首选的 iOS 设备进行模拟。
如果您按照 Expo 指南中的步骤进行操作,可以在安装 Android Studio部分找到,您还会发现它涵盖了安装虚拟设备,我们可以将其作为模拟器运行。要在模拟器上启动您的应用程序,只需在 Android Studio 中打开您安装的 Android 虚拟设备,在终端中运行expo start
命令,并选择在 Android 设备/模拟器上运行。
安装 Node.js
Node.js 是构建在 Chrome 的 V8 JavaScript 引擎上的 JavaScript 运行时,旨在构建可扩展的网络应用程序。Node 允许在终端中执行 JavaScript,并且是任何 Web 开发人员的必不可少的工具。有关 Node.js 的更多信息,您可以阅读项目的关于 Node.js页面,网址为nodejs.org/en/about/
。
根据博览会安装文档,Node.js 在技术上并不是必需的,但一旦你开始实际构建东西,你会想要它。Node.js 本身不在本书的范围之内,但你可以在本章末尾的进一步阅读部分查看更多关于使用 Node.js 的资源。
有许多安装 Node.js 的方法,因此很难推荐特定的安装方法。在 macOS 上,你可以通过以下方式之一安装 Node.js:
-
从项目网站
nodejs.org/en/download/
下载并安装 Node.js。 -
通过 Homebrew 进行安装。如果你熟悉 Homebrew,这个过程在
medium.com/@katopz/how-to-install-specific-nodejs-version-c6e1cec8aa11
中有简洁的说明。 -
通过 Node Version Manager(NVM;
github.com/creationix/nvm
)进行安装。NVM 允许你安装多个版本的 Node.js,并轻松在它们之间切换。使用存储库的 README 中提供的说明来安装 NVM。这是推荐的方法,因为它灵活,只要你习惯在终端中工作。
安装 Expo
Expo 项目曾经有一个基于 GUI 的开发环境叫做 Expo XDE,现在已经被一个名为 Expo Developer Tools 的基于浏览器的 GUI 取代。由于 Expo XDE 已经被弃用,现在创建新的 Expo 应用程序总是使用 Expo CLI。这可以通过终端使用 npm(Node.js 的一部分)安装,使用以下命令:
npm install expo-cli -g
在本书中,我们将会大量使用 Expo 来创建和构建 React Native 应用程序,特别是那些不需要访问原生 iOS 或 Android 代码的应用程序。使用 Expo 构建的应用程序在开发中有一些非常好的优势,帮助混淆原生代码,简化应用程序发布和推送通知,并提供许多有用的功能内置到 Expo SDK 中。有关 Expo 如何工作以及它如何适应 React Native 开发的更多信息,请参见第十章应用程序工作流程和第三方插件。
安装 Watchman
Watchman 是 React Native 内部使用的工具。它的目的是监视文件更新,并在发生更改时触发响应(如实时重新加载)。Expo 文档建议安装 Watchman,因为有报道称一些 macOS 用户在没有它的情况下遇到了问题。安装 Watchman 的推荐方法是通过 Homebrew。作为 macOS 的缺失软件包管理器,Homebrew 允许您直接从终端安装各种有用的程序。这是一个不可或缺的工具,应该在每个开发者的工具包中。
- 如果您还没有安装 Homebrew,请在终端中运行以下命令进行安装(您可以在
brew.sh/
了解更多信息并查看官方文档):
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 安装 Homebrew 后,在终端中运行以下两个命令来安装
watchman
:
brew update
brew install watchman
初始化您的第一个应用程序
这是你开始使用 Expo 开发第一个 React Native 应用所需的所有设置!但是,对于使用纯 React Native 应用(非 Expo 应用)需要执行一些额外的步骤。纯 React Native 应用的开发将在第十章 应用工作流程和第三方插件中进行深入讨论。由于这个设置过程有点复杂并且可能会发生变化,我建议参考官方指南。您可以在 React Native | 入门指南中找到这些说明
facebook.github.io/react-native/docs/getting-started.html
在使用本机代码构建项目选项卡下。从现在开始,我们可以使用 Expo 提供的魔法轻松创建新的开发应用。
我们将使用 Expo 通过 Expo CLI 创建我们的第一个应用程序。创建一个新应用程序就像运行以下命令一样简单:
expo init project-name
运行此命令将首先提示您要创建哪种类型的应用程序:blank
应用程序,没有添加功能,或者tabs
应用程序,将创建一个具有最小标签导航的新应用程序。在本书的示例中,我们将使用blank
应用程序选项。
选择了首选的应用程序类型后,在新的project-name
目录中创建了一个新的、空的 Expo-powered React Native 应用,以及开始开发所需的所有依赖项。你只需要开始编辑新项目目录中的App.js
文件就可以开始工作了。
要运行我们的新应用程序,我们可以cd
进入目录,然后使用expo start
命令。这将自动构建和提供应用程序,并在新的浏览器窗口中打开 Expo 开发者工具,用于开发中的 React Native 应用程序。
有关 Expo CLI 的所有可用命令列表,请查看docs.expo.io/versions/latest/guides/expo-cli.html
上的文档。
创建了我们的第一个应用程序后,让我们继续在 iOS 模拟器和/或 Android 模拟器中运行该应用程序。
在模拟器/模拟器中运行您的应用程序
您已经创建了一个新项目,并在上一步中使用 Expo 开始运行该项目。一旦我们开始对 React Native 代码进行更改,能够看到这些更改的结果将是件好事,对吧?由于 Expo,运行已安装的 iOS 模拟器或 Android 模拟器中的项目也变得更加简单。
在 iOS 模拟器上运行您的应用程序
在 Xcode 模拟器中运行您的应用程序只需点击几下。
-
打开 Xcode。
-
从菜单栏中打开模拟器:Xcode | 打开开发者工具 | 模拟器:
- 如果您运行以下命令,应用程序可以在终端中使用 Expo CLI 启动:
expo start
该命令将构建您的应用程序并在您的 Web 浏览器中打开 Expo 开发者工具。在 Expo 开发者工具中,选择在 iOS 模拟器上运行。
- 第一次通过“在 iOS 模拟器上运行”在 iOS 模拟器上运行 React Native 应用程序时,Expo 应用程序将安装在模拟器上,并且您的应用程序将自动在 Expo 应用程序中打开。模拟的 iOS 将询问您是否要“在“Expo”中打开”?选择“打开”:
- 加载后,您将看到 Expo 开发者菜单。您可以通过在键盘上按下command键 + D 来在此菜单和您的 React Native 应用程序之间切换:
还有更多...
一旦您在模拟器中启动了应用程序,您将能够按下“在 iOS 模拟器上运行”按钮,而无需从 Xcode 中打开模拟器。它还应该记住您的设备选择。从 Xcode 中打开模拟器提供了一种简单的方式来选择您首选的 iOS 设备进行模拟。
您可以通过在键盘上按下command键 + M来在您的 React Native 应用程序和 Expo 开发者菜单之间切换,后者是一个列出了开发中有用功能的列表。Expo 开发者菜单应该看起来像这样:
在真实设备上运行您的应用程序
在真实设备上运行您的开发应用程序就像在模拟器上运行应用程序一样简单。通过原生 Expo 应用程序和 QR 码的巧妙组合,只需点击几下,就可以在真实设备上运行应用程序!
在 iPhone 或 Android 上运行您的应用程序
你可以通过三个简单的步骤在手机上运行正在开发中的应用程序:
-
在 iPhone 上打开 App Store,或者在 Android 设备上打开 Google Play 商店。
-
搜索并下载 Expo Client 应用程序。
-
当您的应用程序在开发机器上运行时,您还应该在浏览器中打开 Expo 开发者工具。您应该在 Expo 开发者工具左侧菜单底部看到一个 QR 码。使用 iPhone 的原生相机应用程序,或 Android 上 Expo 客户端应用程序中的扫描 QR 码按钮,扫描 QR 码。这将在 Expo 客户端应用程序内打开您正在开发的应用程序。
您的 React Native 应用现在应该在您的真实设备上运行,并配备了实时重新加载!您还可以摇动设备,在 React Native 应用和 Expo 开发者菜单之间切换。
摘要
在本章中,我们已经介绍了开发 React Native 应用程序所需的所有步骤,包括初始化新项目,在计算机上模拟运行新项目以及在真实设备上运行开发应用程序。由于 Expo 的强大功能,现在比以往任何时候都更容易开始工作了。
现在您已经设置好了一切,是时候开始构建了!
进一步阅读
以下是涵盖类似主题的其他资源列表:
-
Expo 安装文档在
docs.expo.io/versions/latest/introduction/installation.html
。 -
Node.js Web Development 在
www.packtpub.com/mapt/book/web_development/9781785881503
-
介绍热重载 - React Native 在
facebook.github.io/react-native/blog/2016/03/24/introducing-hot-reloading.html
。这篇来自 React Native 团队的博客文章深入介绍了热重载的工作原理。 -
使用 Expo 发布在
docs.expo.io/versions/latest/guides/publishing.html
。Expo 具有发布功能,允许您通过创建持久 URL 与其他开发人员共享正在开发中的 React Native 应用程序。 -
在
snack.expo.io
上体验 Expo Snack。类似于codepen.io或jsfiddle.net,Snack 允许您在浏览器中实时编辑 React Native 应用程序!
第二章:创建一个简单的 React Native 应用程序
在本章中,我们将涵盖以下内容:
-
向元素添加样式
-
使用图像模拟视频播放器
-
创建一个切换按钮
-
显示项目列表
-
使用 flexbox 创建布局
-
设置和使用导航
React Native 是一个快速增长的库。在过去的几年里,它在开源社区中变得非常受欢迎。几乎每隔一周就会有一个新版本发布,改进性能,添加新组件,或者提供对设备上新 API 的访问。
在本章中,我们将学习库中最常见的组件。为了逐步完成本章中的所有配方,我们将不得不创建一个新的应用程序,所以确保您的环境已经准备就绪。
向元素添加样式
我们有几个组件可供使用,但容器和文本是创建布局或其他组件最常见和有用的组件。在这个配方中,我们将看到如何使用容器和文本,但更重要的是我们将看到样式在 React Native 中是如何工作的。
准备工作
按照上一章的说明创建一个新应用程序。我们将把这个应用程序命名为fake-music-player
。
在使用 Expo 创建新应用程序时,App.js
文件中的root
文件夹将添加少量样板代码。这将是您构建的任何 React Native 应用程序的起点。随时在每个配方的开头删除所有样板代码,因为所有代码(包括在App.js
样板中使用的代码)都将被讨论。
如何做...
- 在
App.js
文件中,我们将创建一个无状态组件。这个组件将模拟一个小型音乐播放器。它只会显示歌曲的名称和一个用来显示进度的条。第一步是导入我们的依赖项:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
- 一旦我们导入了依赖项,我们就可以构建组件:
export default class App extends React.Component {
render() {
const name = '01 - Blue Behind Green Bloches';
return (
<View style={styles.container}>
<View style={styles.innerContainer} />
<Text style={styles.title}>
<Text style={styles.subtitle}>Playing:</Text> {name}
</Text>
</View>
);
}
}
- 我们的组件已经准备好了,现在我们需要添加一些样式,以添加颜色和字体:
const styles = StyleSheet.create({
container: {
margin: 10,
marginTop: 100,
backgroundColor: '#e67e22',
borderRadius: 5,
},
innerContainer: {
backgroundColor: '#d35400',
height: 50,
width: 150,
borderTopLeftRadius: 5,
borderBottomLeftRadius: 5,
},
title: {
fontSize: 18,
fontWeight: '200',
color: '#fff',
position: 'absolute',
backgroundColor: 'transparent',
top: 12,
left: 10,
},
subtitle: {
fontWeight: 'bold',
},
});
- 只要我们的模拟器和模拟器正在运行我们的应用程序,我们应该看到变化:
它是如何工作的...
在步骤 1中,我们包含了我们组件的依赖项。在这种情况下,我们使用了View
,它是一个容器。如果您熟悉 Web 开发,View
类似于div
。我们可以在其他View
内添加更多的View
,Text
,List
,以及我们创建或从第三方库导入的任何其他自定义组件。
如果您熟悉 React,您会注意到,这是一个无状态组件,这意味着它没有任何状态;它是一个纯函数,不支持任何生命周期方法。
我们在组件中定义了一个name
常量,但在实际应用中,这些数据应该来自 props。在返回中,我们定义了我们需要渲染组件的JavaScript XML (JSX),以及对样式的引用。
每个组件都有一个名为style
的属性。该属性接收一个包含我们想要应用于给定组件的所有样式的对象。样式不会被子组件继承(除了Text
组件),这意味着我们需要为每个组件设置单独的样式。
在步骤 3中,我们为我们的组件定义了样式。我们正在使用StyleSheet
API 来创建所有样式。我们本可以使用包含样式的普通对象,但是通过使用StyleSheet
API 而不是对象,我们可以获得一些性能优化,因为样式将被重用于每个渲染器,而不是在每次执行渲染方法时创建一个对象。
还有更多...
我想要引起您对步骤 3中title
样式定义的注意。在这里,我们定义了一个名为backgroundColor
的属性,并将transparent
设置为其值。作为一个很好的练习,让我们注释掉这行代码并查看结果:
在 iOS 上,文本将具有橙色背景颜色,这可能不是我们真正想要在我们的 UI 中发生的事情。为了解决这个问题,我们需要将文本的背景颜色设置为透明。但问题是,为什么会发生这种情况?原因是 React Native 通过将颜色从父元素的背景颜色设置为文本添加了一些优化。这将提高渲染性能,因为渲染引擎不必计算文本每个字母周围的像素,渲染将更快地执行。
在将背景颜色设置为transparent
时要仔细考虑。如果组件将频繁更新内容,特别是如果文本太长,可能会出现一些性能问题。
使用图像模仿视频播放器
图像是任何 UI 的重要组成部分,无论我们是用它们来显示图标、头像还是图片。在这个食谱中,我们将使用图像来创建一个模拟视频播放器。我们还将显示来自本地设备的图标和来自远程服务器(由 Flickr 托管)的大图像。
准备工作
为了按照这个食谱中的步骤,让我们创建一个新的应用程序。我们将把它命名为fake-video-player
。
我们将在我们的应用程序中显示一些图像,以模仿视频播放器,所以您需要为您的应用程序准备相应的图像。我建议使用我在 GitHub 上的食谱存储库中下载的图标,网址为github.com/warlyware/react-native-cookbook/tree/master/chapter-2/fake-video-player/images
。
如何做...
-
我们要做的第一件事是在项目的根目录下创建一个名为
Images
的新文件夹。将您下载的图像添加到新文件夹中。 -
在
App.js
文件中,我们包括了这个组件所需的所有依赖项:
import React from 'react';
import { StyleSheet, View, Image } from 'react-native';
- 我们需要
require
在我们的组件中显示的图像。通过在常量中定义它们,我们可以在不同的地方使用相同的图像:
const playIcon = require('./images/play.png');
const volumeIcon = require('./images/sound.png');
const hdIcon = require('./images/hd-sign.png');
const fullScreenIcon = require('./images/full-screen.png');
const flower = require('./images/flower.jpg');
const remoteImage = { uri: `https://farm5.staticflickr.com/4702/24825836327_bb2e0fc39b_b.jpg` };
- 我们将使用一个无状态组件来渲染 JSX。我们将使用在上一步中声明的所有图像。
export default class App extends React.Component {
render() {
return (
<View style={styles.appContainer}>
<ImageBackground source={remoteImage} style=
{styles.videoContainer} resizeMode="contain">
<View style={styles.controlsContainer}>
<Image source={volumeIcon} style={styles.icon} />
<View style={styles.progress}>
<View style={styles.progressBar} />
</View>
<Image source={hdIcon} style={styles.icon} />
<Image source={fullScreenIcon} style={styles.icon} />
</View>
</ImageBackground>
</View>
);
}
};
- 一旦我们有了要渲染的元素,我们需要为每个元素定义样式:
const styles = StyleSheet.create({
flower: {
flex: 1,
},
appContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
videoContainer: {
backgroundColor: '#000',
flexDirection: 'row',
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
controlsContainer: {
padding: 10,
backgroundColor: '#202020',
flexDirection: 'row',
alignItems: 'center',
marginTop: 175,
},
icon: {
tintColor: '#fff',
height: 16,
width: 16,
marginLeft: 5,
marginRight: 5,
},
progress: {
backgroundColor: '#000',
borderRadius: 7,
flex: 1,
height: 14,
margin: 4,
},
progressBar: {
backgroundColor: '#bf161c',
borderRadius: 5,
height: 10,
margin: 2,
paddingTop: 3,
width: 80,
alignItems: 'center',
flexDirection: 'row',
},
});
- 我们完成了!现在,当您查看应用程序时,您应该看到类似以下的内容:
它是如何工作的...
在步骤 2中,我们需要Image
组件。这是负责从设备的本地文件系统或远程服务器上渲染图像的组件。
在步骤 3中,我们需要所有的图像。最好的做法是在组件外部需要图像,以便只需要一次。在每个渲染器上,React Native 将使用相同的图像。如果我们处理来自远程服务器的动态图像,那么我们需要在每个渲染器上需要它们。
require
函数接受图像路径作为参数。路径是相对于我们类所在的文件夹的。对于远程图像,我们需要使用一个定义uri
的对象来指定我们的文件在哪里。
在 步骤 4 中,声明了一个无状态组件。我们使用 remoteImage
作为我们应用程序的背景,通过一个 ImageBackground
元素,因为 Image
元素不能有子元素。这个元素类似于 CSS 中的 background-url
属性。
Image
的 source
属性接受一个对象来加载远程图像或所需文件的引用。非常重要的是要明确地要求我们想要使用的每个图像,因为当我们准备我们的应用程序进行分发时,图像将自动添加到捆绑包中。这就是我们应该避免做任何动态操作的原因,比如以下操作:
const iconName = playing ? 'pause' : 'play';
const icon = require(iconName);
上述代码不会将图像包含在最终的捆绑包中。因此,当尝试访问这些图像时,会出现错误。相反,我们应该将我们的代码重构为类似于这样的东西:
const pause = require('pause');
const play = require('playing');
const icon = playing ? pause : play;
这样,当准备我们的应用程序进行分发时,捆绑包将包括两个图像,并且我们可以在运行时动态决定显示哪个图像。
在 步骤 5 中,我们定义了样式。大多数属性都是不言自明的。尽管我们用于图标的图像是白色的,但我添加了 tintColor
属性来展示它如何用于着色图像。试一试!将 tintColor
改为 #f00
,看看图标变成红色。
Flexbox 被用来对齐布局的不同部分。在 React Native 中,Flexbox 的行为基本上与 web 开发中的行为相同。我们将在本章后面的 使用 flexbox 创建布局 部分更多地讨论 flexbox,但是 flexbox 本身的复杂性超出了本书的范围。
创建一个切换按钮
按钮是每个应用程序中必不可少的 UI 组件。在这个部分中,我们将创建一个切换按钮,默认情况下将不被选中。当用户点击它时,我们将改变应用于按钮的样式,使其看起来被选中。
我们将学习如何检测点击事件,使用图像作为 UI,保持按钮的状态,并根据组件状态添加样式。
准备工作
让我们创建一个新的应用程序。我们将把它命名为 toggle-button
。在这个部分中,我们将使用一张图片。您可以从 GitHub 上托管的相应存储库中下载这个部分的资产,网址为 github.com/warlyware/react-native-cookbook/tree/master/chapter-2/toggle-button/images
。
如何做...
-
我们将在项目的根目录中创建一个名为
images
的新文件夹,并将心形图片添加到新文件夹中。 -
让我们导入这个类的依赖项。
import React, { Component } from 'react';
import {
StyleSheet,
View,
Image,
Text,
TouchableHighlight,
} from 'react-native';
const heartIcon = require('./images/heart.png');
- 对于这个示例,我们需要跟踪按钮是否被按下。我们将使用一个带有
liked
布尔属性的state
对象来实现这个目的。初始类应该是这样的:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
- 我们需要在
render
方法中定义我们新组件的内容。在这里,我们将定义Image
按钮和其下方的Text
元素:
export default class App extends React.Component {
state = {
liked: false,
};
handleButtonPress = () => {
// Defined in a later step
}
render() {
return (
<View style={styles.container}>
<TouchableHighlight
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={styles.icon}
/>
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
}
- 让我们定义一些样式来设置尺寸、位置、边距、颜色等等:
const styles = StyleSheet.create({
container: {
marginTop: 50,
alignItems: 'center',
},
button: {
borderRadius: 5,
padding: 10,
},
icon: {
width: 180,
height: 180,
tintColor: '#f1f1f1',
},
liked: {
tintColor: '#e74c3c',
},
text: {
marginTop: 20,
},
});
- 当我们在模拟器上运行项目时,我们应该看到类似以下截图的内容:
- 为了响应触摸事件,我们需要定义
handleButtonPress
函数的内容,并将其分配为onPress
属性的回调函数:
handleButtonPress = () => {
this.setState({
liked: !this.state.liked,
});
}
render() {
return (
<View style={styles.container}>
<TouchableHighlight
onPress={this.handleButtonPress}
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={styles.icon}
/>
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
- 如果我们测试我们的代码,我们不会看到 UI 上的任何变化,即使当我们按下按钮时组件上的状态发生变化。让我们在状态改变时为图片添加不同的颜色。这样,我们就能看到 UI 的响应:
render() {
const likedStyles = this.state.liked ? styles.liked : undefined;
return (
<View style={styles.container}>
<TouchableHighlight
onPress={this.handleButtonPress}
style={styles.button}
underlayColor="#fefefe"
>
<Image
source={heartIcon}
style={[styles.icon, likedStyles]} />
</TouchableHighlight>
<Text style={styles.text}>Do you like this app?</Text>
</View>
);
}
它是如何工作的...
在步骤 2中,我们导入了TouchableHighlight
组件。这是负责处理触摸事件的组件。当用户触摸活动区域时,内容将根据我们设置的underlayColor
值进行高亮显示。
在步骤 3中,我们定义了Component
的状态。在这种情况下,状态只有一个属性,但我们可以根据需要添加多个属性。在第三章中,实现复杂用户界面-第一部分,我们将看到更多关于在更复杂场景中处理状态的示例。
在步骤 6中,我们使用setState
方法来改变liked
属性的值。这个方法是从我们正在扩展的Component
类继承而来的。
在步骤 7中,基于liked
属性的当前状态,我们使用样式将图片的颜色设置为红色,或者返回undefined
以避免应用任何样式。当将样式分配给Image
组件时,我们使用数组来分配多个对象。这非常方便,因为组件将所有样式合并为一个单一对象。具有最高索引的对象将覆盖数组中具有最低对象索引的属性:
还有更多...
在实际应用中,我们将使用多个按钮,有时带有左对齐的图标,标签,不同的大小,颜色等。强烈建议创建一个可重用的组件,以避免在整个应用程序中重复编写代码。在第三章,实现复杂用户界面-第一部分中,我们将创建一个按钮组件来处理其中一些情况。
显示项目列表
列表随处可见:用户历史记录中的订单列表,商店中可用商品的列表,要播放的歌曲列表。几乎任何应用程序都需要在列表中显示某种信息。
对于这个示例,我们将在list
组件中显示多个项目。我们将定义一个带有一些数据的 JSON 文件,然后使用简单的require
加载此文件,最后使用漂亮但简单的布局渲染每个项目。
准备工作
让我们从创建一个空应用程序开始。我们将把这个应用程序命名为list-items
。我们需要一个图标来显示在每个项目上。获取图像的最简单方法是从托管在 GitHub 上的此示例的存储库中下载它们:github.com/warlyware/react-native-cookbook/tree/master/chapter-2/list-items/images
。
如何做...
-
我们将首先创建一个
images
文件夹,并将basket.png
添加到其中。还要在项目的根目录中创建一个名为sales.json
的空文件。 -
在
sales.json
文件中,我们将定义要在列表中显示的数据。以下是一些示例数据:
[
{
"items": 5,
"address": "140 Broadway, New York, NY 11101",
"total": 38,
"date": "May 15, 2016"
}
]
-
为了避免使本书的页面混乱,我只定义了一个记录,但请继续向数组中添加更多内容。多次复制和粘贴相同的对象将起作用。此外,您可以更改数据中的一些值,以便每个项目在 UI 中显示唯一的数据。
-
在我们的
App.js
文件中,让我们导入我们需要的依赖项:
import React, { Component } from 'react'; import {
StyleSheet,
View,
ListView,
Image,
Text,
} from 'react-native';
import data from './sales.json';
const basketIcon = require('./images/basket.png');
- 现在,我们需要创建用于渲染项目列表的类。我们将在状态中保留销售数据;这样,我们可以轻松地插入或删除元素:
export default class App extends React.Component {
constructor(props) {
super(props);
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
dataSource: dataSource.cloneWithRows(data),
};
}
renderRow(record) {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
- 在
render
方法中,我们需要定义ListView
组件,并使用renderRow
方法来渲染每个项目。dataSource
属性定义了我们将在列表上渲染的元素数组:
render() {
return (
<View style={styles.mainContainer}>
<Text style={styles.title}>Sales</Text>
<ListView dataSource={this.state.dataSource} renderRow={this.renderRow} />
</View>
);
}
- 现在,我们可以定义
renderRow
的内容。这个方法接收包含我们需要的所有信息的每个对象。我们将在三列中显示数据。在第一列中,我们将显示一个图标;在第二列中,我们将显示每个销售的物品数量和订单将发货的地址;第三列将显示日期和总计:
return (
<View style={styles.row}>
<View style={styles.iconContainer}>
<Image source={basketIcon} style={styles.icon} />
</View>
<View style={styles.info}>
<Text style={styles.items}>{record.items} Items</Text>
<Text style={styles.address}>{record.address}</Text>
</View>
<View style={styles.total}>
<Text style={styles.date}>{record.date}</Text>
<Text style={styles.price}>${record.total}</Text>
</View>
</View>
);
- 一旦我们定义了 JSX,就该添加样式了。首先,我们将为主容器、标题和行容器定义颜色、边距、填充等样式。为了为每一行创建三列,我们需要使用
flexDirection: 'row'
属性。我们将在本章后面的使用 flexbox 创建布局中更多了解这个属性:
const styles = StyleSheet.create({
mainContainer: {
flex: 1,
backgroundColor: '#fff',
},
title: {
backgroundColor: '#0f1b29',
color: '#fff',
fontSize: 18,
fontWeight: 'bold',
padding: 10,
paddingTop: 40,
textAlign: 'center',
},
row: {
borderColor: '#f1f1f1',
borderBottomWidth: 1,
flexDirection: 'row',
marginLeft: 10,
marginRight: 10,
paddingTop: 20,
paddingBottom: 20,
},
});
- 如果我们刷新模拟器,应该看到类似于以下截图的东西:
- 现在,在
StyleSheet
定义内部,让我们为图标添加样式。我们将添加一个黄色的圆作为背景,并将图标的颜色改为白色:
iconContainer: {
alignItems: 'center',
backgroundColor: '#feb401',
borderColor: '#feaf12',
borderRadius: 25,
borderWidth: 1,
justifyContent: 'center',
height: 50,
width: 50,
},
icon: {
tintColor: '#fff',
height: 22,
width: 22,
},
- 在这个改变之后,我们将在每一行的左侧看到一个漂亮的图标,就像下面的截图所示:
- 最后,我们将为文本添加样式。我们需要设置
color
、size
、fontWeight
、padding
和其他一些属性:
info: {
flex: 1,
paddingLeft: 25,
paddingRight: 25,
},
items: {
fontWeight: 'bold',
fontSize: 16,
marginBottom: 5,
},
address: {
color: '#ccc',
fontSize: 14,
},
total: {
width: 80,
},
date: {
fontSize: 12,
marginBottom: 5,
},
price: {
color: '#1cad61',
fontSize: 25,
fontWeight: 'bold',
}
- 最终结果应该类似于以下截图:
它是如何工作的...
在步骤 5中,我们创建了数据源并向状态添加了数据。ListView.DataSource
类实现了ListView
组件的性能数据处理。rowHasChanged
属性是必需的,它应该是一个比较下一个元素的函数。在我们的情况下,如果变化与当前数据不同,表示为(r1, r2) => r1 !== r2
,那么 React Native 将知道如何响应并重新渲染 UI。
在用数据填充数据源时,我们需要调用cloneWithRows
方法并发送一个记录数组。
如果我们想添加更多数据,我们应该再次使用包含先前和新数据的数组调用cloneWithRows
方法。数据源将确保计算差异并根据需要重新渲染列表。
在步骤 7中,我们定义了渲染列表的 JSX。列表只需要两个属性:我们已经从步骤 6中得到的数据源和renderRow
。
renderRow
属性接受一个函数作为值。这个函数需要返回每一行的 JSX。
还有更多...
我们使用 flexbox 创建了一个简单的布局;但是,在本章中还有另一个教程,我们将更详细地介绍如何使用 flexbox。
一旦我们有了我们的列表,很有可能我们需要查看每个订单的详细信息。您可以使用TouchableHighlight
组件作为每行的主容器,所以继续尝试一下。如果您不确定如何使用TouchableHighlight
组件,请参阅本章早期的创建切换按钮教程。
使用 flexbox 创建布局
在这个教程中,我们将学习有关 flexbox 的知识。在本章的先前教程中,我们一直在使用 flexbox 来创建布局,但在这个教程中,我们将专注于我们可以使用的属性,通过重新创建 App Store 上名为Nominazer的随机名称生成应用程序的布局(itunes.apple.com/us/app/nominazer/id765422087?mt=8
)。
在 React Native 中使用 flexbox 基本上与在 CSS 中使用 flexbox 相同。这意味着如果您习惯于使用 flexbox 布局开发网站,那么您已经知道如何在 React Native 中创建布局!这个练习将涵盖在 React Native 中使用 flexbox 的基础知识,但是要查看您可以使用的所有布局属性的列表,请参考布局属性的文档(facebook.github.io/react-native/docs/layout-props.html
)。
准备工作
让我们从创建一个新的空白应用程序开始。我们将其命名为flexbox-layout
。
如何做...
- 在
App.js
中,让我们导入我们应用程序所需的依赖项:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
- 我们的应用程序只需要一个
render
方法,因为我们正在构建一个静态布局。渲染的布局包括一个容器View
元素和应用程序每个彩色部分的三个子View
元素。
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<View style={styles.topSection}> </View>
<View style={styles.middleSection}></View>
<View style={styles.bottomSection}></View>
</View> );
}
}
- 接下来,我们可以开始添加我们的样式。我们将添加的第一个样式将应用于包裹整个应用程序的
View
元素。将flex
属性设置为1
将导致所有子元素填充所有空白空间:
const styles = StyleSheet.create({
container: {
flex: 1,
}
});
- 现在,我们可以为三个子
View
元素添加样式。每个部分都应用了flexGrow
属性,这决定了每个元素应该占用多少可用空间。topSection
和bottomSection
都设置为3
,所以它们将占用相同的空间。由于middleSection
的flexGrow
属性设置为1
,这个元素将占用topSection
和bottomSection
占用空间的三分之一:
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
},
middleSection: {
flexGrow: 1,
backgroundColor: '#FFF',
},
bottomSection: {
flexGrow: 3,
backgroundColor: '#FD909E',
},
- 如果我们在模拟器中打开我们的应用程序,我们应该已经能够看到基本布局正在形成:
- 在这里,我们可以在步骤 2中创建的三个子
View
元素中的每一个添加一个Text
元素。请注意,新增的代码已经被突出显示:
render() {
return (
<View style={styles.container}>
<View style={styles.topSection}>
<Text style={styles.topSectionText}>
4 N A M E S
</Text>
</View>
<View style={styles.middleSection}>
<Text style={styles.middleSectionText}>
I P S U M
</Text>
</View>
<View style={styles.bottomSection}>
<Text style={styles.bottomSectionText}>
C O M
</Text>
</View>
</View>
);
}
- 每个部分的文本默认显示在该部分的左上角。我们可以使用 flexbox 来使每个元素按照期望的位置进行对齐和排列。所有三个子
View
元素的alignItems
flex 属性都设置为'center'
,这将导致每个元素的子元素沿着x轴居中。justifyContent
在中间和底部部分上使用,定义了子元素沿着y轴应该如何对齐:
onst styles = StyleSheet.create({
container: {
flex: 1,
},
topSection: {
flexGrow: 3,
backgroundColor: '#5BC2C1',
alignItems: 'center',
},
middleSection: {
flexGrow: 1,
backgroundColor: '#FFF',
justifyContent: 'center',
alignItems: 'center',
},
bottomSection: {
flexGrow: 3,
backgroundColor: '#FD909E',
alignItems: 'center',
justifyContent: 'flex-end'
}
});
- 唯一剩下的就是为
Text
元素添加基本样式,增加fontSize
、fontWeight
和所需的margin
:
topSectionText: {
fontWeight: 'bold',
marginTop: 50
},
middleSectionText: {
fontSize: 30,
fontWeight: 'bold'
},
bottomSectionText: {
fontWeight: 'bold',
marginBottom: 30
}
- 如果我们在模拟器中打开我们的应用程序,我们应该能够看到我们完成的布局:
工作原理...
我们的应用程序看起来非常不错,而且通过使用 flexbox 很容易实现。我们通过将flexGrow
属性分别设置为3
、1
和3
来创建了三个不同的部分,这使得顶部和底部部分的垂直大小相等,而中间部分是顶部和底部部分的三分之一。
在使用 flexbox 时,我们有两个方向来布置子内容,row
和column
:
-
row
:这允许我们水平排列容器的子元素。 -
column
:这允许我们垂直排列容器的子元素。这是 React Native 中的默认方向。
当我们像对容器View
元素所做的那样设置flex: 1
时,我们告诉该元素占用所有可用空间。如果我们移除flex: 1
或将flex
设置为0
,我们会看到布局在自身内部收缩,因为容器不再在所有空白空间中伸展:
Flexbox 也非常适合支持不同的屏幕分辨率。即使不同设备可能有不同的分辨率,我们可以确保一致的布局,使其在任何设备上都看起来很好。
还有更多...
React Native 中的 flexbox 工作方式与 CSS 中的工作方式有一些不同。首先,在 CSS 中,默认的flexDirection
属性是row
,而在 React Native 中,默认的flexDirection
属性是column
。
flex
属性在 React Native 中的行为也有些不同。与将flex
设置为字符串值不同,它可以设置为正整数、0
或-1
。正如官方的 React Native 文档所述:
当 flex 为正数时,它使组件具有灵活性,并且其大小将与其 flex 值成比例。因此,将 flex 设置为 2 的组件将占据比将 flex 设置为 1 的组件多一倍的空间。当 flex 为 0 时,组件的大小根据宽度和高度确定,是不灵活的。当 flex 为-1 时,组件通常根据宽度和高度确定大小。但是,如果空间不足,组件将收缩到其最小宽度和最小高度。
关于 flexbox 还有很多要讨论的,但目前我们已经有所了解。在第三章 实现复杂用户界面-第一部分中,我们将学习更多关于布局的知识。我们将学习更多关于布局,并创建一个使用更多可用布局属性的复杂布局。
另请参阅
-
React Native 布局属性文档(
facebook.github.io/react-native/docs/layout-props.html
) -
React Native 文本样式属性文档(
facebook.github.io/react-native/docs/text-style-props.html
) -
Yoga (
github.com/facebook/yoga
)——React Native 使用的 Facebook 的 Flexbox 实现。 -
一篇优秀的 Stack Overflow 帖子介绍了 React Native 弹性属性的工作原理和示例-
stackoverflow.com/questions/43143258/flex-vs-flexgrow-vs-flexshrink-vs-flexbasis-in-react-native
设置和使用导航
对于任何具有多个视图的应用程序,导航系统至关重要。导航在应用程序开发中是如此普遍,以至于 Expo 在创建新应用程序时提供了两个模板:空白或标签导航。这个教程是基于 Expo 提供的非常简化的标签导航应用程序模板。我们仍将从一个空白应用程序开始,并从头开始构建我们的基本标签导航应用程序,以更好地理解所有必需的部分。完成此教程后,我鼓励您使用标签导航模板开始一个新应用程序,以查看我们将在后面章节中涵盖的一些更高级的功能,包括推送通知和堆栈导航。
准备工作
让我们继续创建一个名为simple-navigation
的新空白应用程序。我们还需要一个第三方包来处理我们的导航。我们将使用react-navigation
包的 1.5.9 版本。使用此包的更新版本将无法正确使用此代码,因为该包的 API 最近经历了重大变化。在终端中,转到新项目的根目录,并使用以下命令安装此包:
yarn add react-navigation@1.5.9
这就是我们需要的所有设置。让我们开始构建吧!
如何做到...
- 在
App.js
文件中,让我们导入我们的依赖项:
import React from 'react'; import { StyleSheet, View } from 'react-native';
- 这个应用程序的
App
组件将非常简单。我们只需要一个带有渲染我们应用程序容器的App
类和一个render
函数。我们还将添加填充窗口和添加白色背景的样式:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
}
});
App.js
的下一步是导入并使用MainTabNavigator
组件,这是我们将在步骤 4中创建的新组件:
React.Component {
render() {
return (
<View style={styles.container}>
<MainTabNavigator />
</View>
);
}
}
-
我们需要为我们的
MainTabNavigator
组件创建一个新文件。让我们在项目的根目录中创建一个名为navigation
的新文件夹。在这个新文件夹中,我们将为我们的导航组件创建MainTabNavigator.js
。 -
在
MainTabNavigator.js
中,我们可以导入我们需要的所有导航依赖项。这些依赖项包括三个屏幕(HomeScreen
、LinksScreen
和SettingsScreen
)。我们将在后面的步骤中添加这些屏幕:
import React from 'react';
import { Ionicons } from '@expo/vector-icons';
import { TabNavigator, TabBarBottom } from 'react-navigation';
import HomeScreen from '../screens/HomeScreen';
import LinksScreen from '../screens/LinksScreen';
import SettingsScreen from '../screens/SettingsScreen';
- 我们的导航组件将使用
react-navigation
提供的TabNavigator
方法来定义应用程序的路由和导航。TabNavigator
接受两个参数:一个RouteConfig
对象来定义每个路由,以及一个TabNavigatorConfig
对象来定义我们的TabNavigator
组件的选项:
export default TabNavigator({
// RouteConfig, defined in step 7.
}, {
// TabNavigatorConfig, defined in steps 8 and 9.
});
- 首先,我们将定义
RouteConfig
对象,它将为我们的应用程序创建一个路由映射。RouteConfig
对象中的每个键都作为路由的名称。我们为每个路由的屏幕属性设置为我们希望在该路由上显示的相应屏幕组件:
export default TabNavigator({
Home: {
screen: HomeScreen,
},
Links: {
screen: LinksScreen,
},
Settings: {
screen: SettingsScreen,
},
}, {
// TabNavigatorConfig, defined in steps 8 and 9\.
});
TabNavigatorConfig
还有一些内容。我们将通过将react-navigation
提供的TabBarBottom
组件传递给tabBarComponent
属性来声明我们想要使用什么样的选项卡栏(在本例中,是设计用于屏幕底部的选项卡栏)。tabBarPosition
定义了栏是在屏幕顶部还是底部。animationEnabled
指定了过渡是否是动画的,swipeEnabled
声明了视图是否可以通过滑动来改变:
export default TabNavigator({
// Route Config, defined in step 7\.
}, {
navigationOptions: ({ navigation }) => ({
// navigationOptions, defined in step 9.
}),
tabBarComponent: TabBarBottom,
tabBarPosition: 'bottom',
animationEnabled: false,
swipeEnabled: false,
});
- 在
TabNavigatorConfig
对象的navigationOptions
属性中,我们将通过声明一个函数来为每个路由定义动态的navigationOptions
,该函数接受当前路由/屏幕的导航 prop。我们可以使用此函数来决定选项卡栏如何针对每个路由/屏幕进行操作,因为它被设计为返回一个为适当屏幕设置navigationOptions
的对象。我们将使用此模式来定义每个路由的tabBarIcon
属性的外观:
navigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused }) => {
// Defined in step 10
},
}),
tabBarIcon
属性设置为一个函数,其参数是当前路由的 props。我们将使用focused
属性来决定是渲染有颜色的图标还是轮廓图标,这取决于当前路由。我们通过navigation.state
从导航 prop 中获取routeName
,为我们的三条路线定义图标,并返回适当路线的渲染图标。我们将使用 Expo 提供的Ionicons
组件来创建每个图标,并根据图标的路线是否focused
来定义图标的颜色:
navigationOptions: ({ navigation }) => ({
tabBarIcon: ({ focused }) => {
const { routeName } = navigation.state;
let iconName;
switch (routeName) {
case 'Home':
iconName = `ios-information-circle`;
break;
case 'Links':
iconName = `ios-link`;
break;
case 'Settings':
iconName = `ios-options`;
}
return (
<Ionicons name={iconName}
size={28} style={{marginBottom: -3}}
color={focused ? Colors.tabIconSelected :
Colors.tabIconDefault}
/>
);
},
}),
- 设置
MainTabNavigator
的最后一步是创建用于给每个图标上色的Colors
常量:
const Colors = {
tabIconDefault: '#ccc',
tabIconSelected: '#2f95dc',
}
-
我们的路由现在已经完成!现在剩下的就是为我们导入和定义在
MainTabNavigator.js
中的三个路由创建三个屏幕组件。为简单起见,这三个屏幕将具有相同的代码,除了背景颜色和标识文本不同。 -
在项目的根目录中,我们需要创建一个
screens
文件夹来存放我们的三个屏幕。在新文件夹中,我们需要创建HomeScreen.js
、LinksScreen.js
和SettingsScreen.js
。 -
让我们从打开新创建的
HomeScreen.js
并添加必要的依赖项开始:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
HomeScreen
组件本身非常简单,只是一个全彩色页面,屏幕中间有一个Home
字样,显示我们当前所在的屏幕:
export default class HomeScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Home
</Text>
</View>
);
}
}
- 我们还需要为我们的
Home
屏幕布局添加样式:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#608FA0',
},
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
- 现在剩下的就是重复步骤 14、步骤 15和步骤 16,为剩下的两个屏幕做一些微小的更改。
LinksScreen.js
应该看起来像HomeScreen.js
,并更新以下突出显示的部分:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
export default class LinksScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Links
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F8759D', },
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
- 同样,在
SettingsScreen.js
内部,我们可以使用与前两个屏幕相同的结构创建第三个屏幕组件:
import React from 'react';
import {
StyleSheet,
Text,
View,
} from 'react-native';
export default class SettingsScreen extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.headline}>
Settings
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#F0642E',
},
headline: {
fontWeight: 'bold',
fontSize: 30,
color: 'white',
}
});
- 我们的应用程序已经完成!当我们在模拟器中查看我们的应用程序时,屏幕底部应该有一个选项卡栏,可以在三个路由之间切换:
它是如何工作的...
在这个教程中,我们介绍了原生应用中最常见和基本的导航模式之一,即选项卡栏。React Navigation 库是一个非常强大、功能丰富的导航解决方案,很可能能够为您的应用程序提供任何所需的导航。我们将在第三章中介绍更多关于 React Navigation 的用法,*实现复杂的用户
接口 - 第一部分。*
另请参阅
-
React Navigation 官方文档(
reactnavigation.org/
) -
Expo 的路由和导航指南(
docs.expo.io/versions/latest/guides/routing-and-navigation.html
)
第三章:实现复杂的用户界面-第一部分
在本章中,我们将实现复杂的用户界面。我们将学习如何使用 flexbox 创建适用于不同屏幕尺寸的组件,如何检测方向变化等。
本章将涵盖以下教程:
-
创建具有主题支持的可重用按钮
-
使用 flexbox 为平板电脑构建复杂的布局
-
包括自定义字体
-
使用字体图标
创建具有主题支持的可重用按钮
在开发软件时,可重用性非常重要。我们应该避免一遍又一遍地重复相同的事情,而是应该创建可以尽可能多次重用的小组件。
在本教程中,我们将创建一个Button
组件,并且我们还将定义几个属性来改变其外观和感觉。在学习本教程的过程中,我们将学习如何动态地将不同的样式应用到组件上。
准备工作
我们需要创建一个空的应用程序。让我们将其命名为reusable-button
。
如何做...
-
在我们新应用程序的根目录中,我们需要为可重用的按钮相关代码创建一个新的
Button
文件夹。让我们还在新的Button
文件夹中创建index.js
和styles.js
。 -
我们将从导入新组件的依赖项开始。在
Button/index.js
文件中,我们将创建一个Button
组件。这意味着我们需要导入Text
和TouchableOpacity
组件。您会注意到我们还在导入尚不存在的样式。我们将在本教程的后续部分中定义这些样式。在Button/index.js
文件中,我们应该有以下导入:
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
} from 'react-native';
import {
Base,
Default,
Danger,
Info,
Success
} from './styles';
- 现在我们已经导入了依赖项,让我们为这个组件定义类。我们将需要一些属性和两种方法。还需要导出此组件,以便我们可以在其他地方使用它:
export default class Button extends Component {
getTheme() {
// Defined in a later step
}
render() {
// Defined in a later step
}
}
- 我们需要根据给定的
属性
选择要应用于我们组件的样式。为此,我们将定义getTheme
方法。该方法将检查任何属性
是否为true
,并返回相应的样式。如果没有一个是true
,它将返回Default
样式:
getTheme() {
const { danger, info, success } = this.properties;
if (info) {
return Info;
}
if (success) {
return Success;
}
if (danger) {
return Danger;
}
return Default;
}
- 所有组件都需要一个
render
方法。在这里,我们需要返回此组件的 JSX 元素。在这种情况下,我们将获取给定属性
的样式,并将其应用于TouchableOpacity
组件。
我们还为按钮定义了一个标签。在这个标签内,我们将渲染children
属性。如果接收到回调函数,那么当用户按下这个组件时它将被执行:
render() {
const theme = this.getTheme();
const {
children,
onPress,
style,
rounded,
} = this.properties;
return (
<TouchableOpacity
activeOpacity={0.8}
style={[
Base.main,
theme.main,
rounded ? Base.rounded : null ,
style,
]}
onPress={onPress}
>
<Text style={[Base.label, theme.label]}>{children}</Text>
</TouchableOpacity>
);
}
- 我们的
Button
组件几乎完成了。我们仍然需要定义我们的样式,但首先让我们转到项目根目录下的App.js
文件。我们需要导入依赖项,包括我们创建的Button
组件。
当用户点击按钮时,我们将显示警报消息,因此我们还需要导入Alert
组件:
import React from 'react';
import {
Alert,
StyleSheet,
View
} from 'react-native';
import Button from './Button';
- 一旦我们有了所有的依赖项,让我们定义一个无状态组件,渲染几个按钮。第一个按钮将使用默认样式,第二个按钮将使用成功样式,这将为按钮的背景添加一个漂亮的绿色。最后一个按钮将在按下时显示一个警报。为此,我们需要定义使用
Alert
组件的回调函数,只需设置标题和消息:
export default class App extends React.Component {
handleButtonPress() {
Alert.alert('Alert', 'You clicked this button!');
}
render() {
return(
<View style={styles.container}>
<Button style={styles.button}>
My first button
</Button>
<Button success style={styles.button}>
Success button
</Button>
<Button info style={styles.button}>
Info button
</Button>
<Button danger rounded style={styles.button}
onPress={this.handleButtonPress}>
Rounded button
</Button>
</View>
);
}
}
- 我们将为主要布局的对齐和每个按钮的对齐方式添加一些样式,以及一些边距:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
margin: 10,
},
});
- 如果我们现在尝试运行应用程序,将会出现一些错误。这是因为我们还没有为按钮声明样式。让我们现在来解决这个问题。在
Button/styles.js
文件中,我们需要定义基本样式。这些样式将应用于按钮的每个实例。在这里,我们将定义半径、填充、字体颜色和我们需要的所有常见样式:
import { StyleSheet } from 'react-native';
const Base = StyleSheet.create({
main: {
padding: 10,
borderRadius: 3,
},
label: {
color: '#fff',
},
rounded: {
borderRadius: 20,
},
});
- 一旦我们有了按钮的常见样式,我们需要为
Danger
、Info
、Success
和Default
主题定义样式。为此,我们将为每个主题定义不同的对象。在每个主题内,我们将使用相同的对象,但具有该主题的特定样式。
为了保持简单,我们只会改变backgroundColor
,但我们可以使用尽可能多的样式属性:
const Danger = StyleSheet.create({
main: {
backgroundColor: '#e74c3c',
},
});
const Info = StyleSheet.create({
main: {
backgroundColor: '#3498db',
},
});
const Success = StyleSheet.create({
main: {
backgroundColor: '#1abc9c',
},
});
const Default = StyleSheet.create({
main: {
backgroundColor: 'rgba(0 ,0 ,0, 0)',
},
label: {
color: '#333',
},
});
- 最后,让我们导出样式。这一步是必要的,这样
Button
组件就可以导入每个主题的所有样式:
export {
Base,
Danger,
Info,
Success,
Default,
};
- 如果我们打开应用程序,我们应该能够看到我们完成的布局:
工作原理...
在这个例子中,我们使用了TouchableOpacity
组件。这个组件允许我们定义一个漂亮的动画,当用户按下按钮时改变不透明度。
我们可以使用activeOpacity
属性来设置按钮被按下时的不透明度值。该值可以是0
到1
之间的任何数字,其中0
是完全透明的。
如果我们按下圆形按钮,我们将看到一个原生的警报消息,如下面的截图所示:
使用 flexbox 为平板电脑构建复杂布局
当涉及到创建响应式布局时,flexbox 真的非常方便。React Native 使用 flexbox 作为布局系统,如果你已经熟悉这些概念,那么对于你来说开始创建任何类型的布局将会非常容易。
如前一章所述,在 React Native 中,flexbox 的工作方式与 CSS 中的工作方式有一些不同。有关 React Native 和 CSS flexbox 之间的区别的更多信息,请参阅第二章中如何工作...部分的使用 flexbox 创建布局教程。
在这个教程中,我们将创建一个布局来显示博客文章列表。每篇文章都将是一个带有图片、摘录和阅读更多按钮的小卡片。我们将使用 flexbox 根据屏幕大小在主容器上排列这些文章。这将允许我们通过正确对齐横向和纵向的卡片来处理屏幕旋转。
准备工作
我们需要一个新的应用程序来完成这个教程。让我们把它命名为tablet-flexbox
。
当我们使用 Expo 创建一个新应用程序时,项目的基础部分会创建一个app.json
,其中提供了一些基本配置。在这个教程中,我们正在构建一个应用程序,我们希望它在平板电脑上看起来很好,特别是在横向模式下。当我们打开app.json
时,我们应该看到一个orientation
属性设置为'portrait'
。该属性确定应用程序内允许哪些方向。orientation
属性接受'portrait'
(锁定应用程序为纵向模式)、'landscape'
(锁定应用程序为横向模式)和'default'
(允许应用程序根据设备的方向调整屏幕方向)。对于我们的应用程序,我们将把orientation
设置为'landscape'
,这样我们就可以支持横向和纵向布局。
我们还将使用一些图片,这些图片需要远程托管,以便为该示例正确模拟加载远程数据并使用Image
组件显示图片。我已经将这些图片上传到www.imgur.com图像托管服务,并在data.json
文件中引用了这些远程图片,该文件是该示例用于其可消耗数据的。如果由于任何原因这些远程图片对您来说加载不正常,它们也包含在该示例的存储库中的/assets
文件夹中。请随意将它们上传到任何服务器或托管服务,并相应地更新data.json
中的图片 URL。该存储库可以在 GitHub 上找到,网址为github.com/warlyware/react-native-cookbook/tree/master/chapter-3/tablet-flexbox
。
如何做...
-
首先,我们需要在项目的根目录中创建一个
Post
文件夹。我们还需要在新的Post
文件夹中创建一个index.js
和一个styles.js
文件。我们将使用这个Post
组件来为我们的应用程序显示每个帖子。最后,我们需要在项目的根目录中添加一个data.json
文件,我们将使用它来定义一个帖子列表。 -
现在我们可以继续构建
App.js
组件。首先,我们需要导入这个类的依赖项。我们将使用ListView
组件来渲染帖子列表。我们还需要Text
和View
组件作为内容容器。我们将创建一个自定义的Post
组件来渲染列表中的每个帖子,并且我们还需要导入data.json
文件:
import React, { Component } from 'react';
import { ListView, StyleSheet, Text, View } from 'react-native';
import Post from './Post';
import data from './data.json';
- 让我们为
App
组件创建类。在这里,我们将使用.json
文件中的数据来创建列表的dataSource
。我们将在下一步向我们的data.json
文件中添加一些实际数据。在render
方法中,我们将定义一个简单的顶部工具栏和List
组件。我们将使用Post
组件来处理每条记录,并从state
中获取dataSource
。
如果您对ListView
组件有任何疑问,您应该查看第二章中的示例,创建一个简单的 React Native 应用程序,在那里我们创建了一个订单列表:
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2,
});
export default class App extends Component {
state = {
dataSource: dataSouce.cloneWithRows(data.posts),
};
render() {
return (
<View style={styles.container}>
<View style={styles.toolbar}>
<Text style={styles.title}>Latest posts</Text>
</View>
<ListView
dataSource={this.state.dataSource}
renderRow={post => <Post {...post} />}
style={styles.list}
contentContainerStyle={styles.content}
/>
</View>
);
}
}
- 还缺少两个文件:包含数据的
.json
文件和Post
组件。在这一步中,我们将创建我们将用于每个帖子的数据。为了简化事情,在以下代码片段中只有一条数据记录,但我在这个示例中使用的其余POST
对象可以在本示例的代码存储库中的data.json
文件中找到,位于github.com/warlyware/react-native-cookbook/blob/master/chapter-3/tablet-flexbox/data.json
:
{
"posts": [
{
"title": "The Best Article Ever Written",
"img": "https://i.imgur.com/mf9daCT.jpg",
"content": "Lorem ipsum dolor sit amet...",
"author": "Bob Labla"
},
// Add more records here.
]
}
- 现在我们有了一些数据,我们准备开始处理
Post
组件。在这个组件中,我们需要显示图片、标题和按钮。由于这个组件不需要知道状态,我们将使用一个无状态组件。以下代码使用了我们在第二章中学到的所有组件,创建一个简单的 React Native 应用。如果有什么不清楚的地方,请再次查看那一章。这个组件将接收数据作为参数,然后我们将用它来显示组件中的内容。Image
组件将使用data.json
文件中每个对象上定义的img
属性来显示远程图片。
import React from 'react';
import {
Image,
Text,
TouchableOpacity,
View
} from 'react-native';
import styles from './styles';
const Post = ({ content, img, title }) => (
<View style={styles.main}>
<Image
source={{ uri: img }}
style={styles.image}
/>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<Text>{content}</Text>
</View>
<TouchableOpacity style={styles.button} activeOpacity={0.8}>
<Text style={styles.buttonText}>Read more</Text>
</TouchableOpacity>
</View>
);
export default Post;
- 一旦我们定义了组件,我们还需要为每个帖子定义样式。让我们创建一个空的
StyleSheet
导出,以便依赖于styles.js
的Post
组件能够正常运行。
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
// Defined in later steps
});
export default styles;
-
如果我们尝试运行应用程序,我们应该能够在屏幕上看到来自
.json
文件的数据。不过,它不会很漂亮,因为我们还没有应用任何样式。 -
我们在屏幕上已经有了所需的一切。现在我们准备开始布局工作。首先,让我们为我们的
Post
容器添加样式。我们将设置width
、height
、borderRadius
和其他一些样式。让我们把它们添加到/Post/styles.js
文件中。
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
borderRadius: 3,
height: 340,
margin: 5,
width: 240,
}
});
- 到目前为止,我们应该看到垂直对齐的小框。这是一些进展,但我们需要为图片添加更多样式,这样我们才能在屏幕上看到它。让我们在上一步的相同
styles
常量中添加一个image
属性。resizeMode
属性将允许我们设置我们想要如何调整图片的大小。在这种情况下,通过选择cover
,图片将保持原始的宽高比。
image: {
backgroundColor: '#ccc',
height: 120,
resizeMode: 'cover',
}
- 对于帖子的
content
,我们希望占据卡片上所有可用的高度,因此我们需要使其灵活并添加一些填充。我们还将向内容添加overflow: hidden
以避免溢出View
元素。对于title
,我们只需要更改fontSize
并在底部添加margin
:
content: {
padding: 10,
overflow: 'hidden',
flex: 1,
},
title: {
fontSize: 18,
marginBottom: 5,
},
- 最后,对于按钮,我们将
backgroundColor
设置为绿色,文本设置为白色。我们还需要添加一些padding
和margin
来进行间距:
button: {
backgroundColor: '#1abc9c',
borderRadius: 3,
padding: 10,
margin: 10,
},
buttonText: {
color: '#fff',
textAlign: 'center',
}
- 如果我们刷新模拟器,我们应该能够看到我们的帖子以小卡片的形式显示。目前,卡片是垂直排列的,但我们希望将它们全部水平渲染。我们将在以下步骤中解决这个问题:
已为所有帖子元素添加了主要样式
- 目前,我们只能在列表中以列的形式看到前三个项目,而不是横向排列在屏幕上。让我们返回
App.js
文件并开始添加我们的样式。我们在container
中添加flex: 1
,以便我们的布局始终填满屏幕。我们还希望在顶部显示一个工具栏。为此,我们只需要定义一些padding
和color
如下:
const styles = StyleSheet.create({
container: {
flex: 1,
},
toolbar: {
backgroundColor: '#34495e',
padding: 10,
paddingTop: 20,
},
title: {
color: '#fff',
fontSize: 20,
textAlign: 'center',
}
});
- 让我们也为
list
添加一些基本样式。一个漂亮的背景颜色和一些填充。我们还将添加flex
属性,这将确保列表占据屏幕上所有可用的高度。我们这里只有两个组件:工具栏和列表。工具栏占用大约 50 像素。如果我们使列表灵活,它将占据所有剩余的可用空间,这正是我们在旋转设备或在不同屏幕分辨率下运行应用程序时想要的效果:
list: {
backgroundColor: '#f0f3f4',
flex: 1,
paddingTop: 5,
paddingBottom: 5,
}
- 如果我们再次在模拟器中检查应用程序,我们应该能够看到工具栏和列表按预期布局:
已应用样式给每个帖子,使它们看起来像卡片
- 我们几乎完成了这个应用程序。我们唯一剩下的事情就是将卡片水平排列。这可以通过 flexbox 在三个简单的步骤中实现:
content: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-around',
},
第一步是通过ListView
组件中的contentContainerStyle
属性应用这些content
样式。在内部,ListView
组件将这些样式应用于包裹所有子视图的内容容器。
然后我们将flexDirection
设置为row
。这将水平对齐列表上的卡片;然而,这提出了一个新问题:我们只能看到一行帖子。为了解决这个问题,我们需要包裹这些项目。我们通过将flexWrap
属性设置为wrap
来实现这一点,这将自动将不适合视图的项目移动到下一行。最后,我们使用justifyContent
属性并将其设置为center
,这将使我们的ListView
居中在应用程序的中间。
- 我们现在有一个响应灵敏的应用程序,在横向模式下在平板电脑上看起来很好:
横向模式下 iPad 和 Android 平板电脑截图的并排比较
并且在纵向模式下看起来也很好:
纵向模式下 iPad 和 Android 平板电脑截图的并排比较
还有更多...
Expo 还提供了一个ScreenOrientation
助手,用于更改应用程序的方向配置。该助手还允许更精细的方向设置(例如ALL_BUT_UPSIDE_DOWN
或LANDSCAPE_RIGHT
)。如果您的应用程序需要动态、细粒度的屏幕方向控制,请参阅ScreenOrientation
Expo 文档获取信息:docs.expo.io/versions/v24.0.0/sdk/screen-orientation.html
。
另请参阅
有关静态图像资源和<Image>
组件的官方文档可以在 https://facebook.github.io/react-native/docs/images.html找到。
包括自定义字体
在某个时候,我们可能会想要使用自定义字体系列显示文本。到目前为止,我们一直在使用默认字体,但我们可以使用任何其他我们喜欢的字体。
在 Expo 之前,添加自定义字体的过程更加困难,需要使用原生代码,并且需要在 iOS 和 Android 中实现不同的方式。幸运的是,通过使用 Expo 的字体助手库,这一切都变得简化和简化了。
在这个示例中,我们将导入一些字体,然后使用每个导入的字体系列显示文本。我们还将使用不同的字体样式,如粗体和斜体。
准备工作
为了在这个示例上工作,我们需要一些字体。你可以使用任何你喜欢的字体。我建议去 Google Fonts(fonts.google.com/
)下载你喜欢的字体。在这个示例中,我们将使用 Josefin Sans 和 Raleway 字体。
一旦你下载了字体,让我们创建一个空的应用程序并将其命名为custom-fonts
。当我们使用 Expo 创建一个空白应用程序时,它会在项目的根目录中创建一个assets
文件夹,用于放置所有资产(图像、字体等),因此我们将遵循标准,并将我们的字体添加到此文件夹中。让我们创建/assets/fonts
文件夹并将从 Google Fonts 下载的自定义字体文件添加到此文件夹中。
从 Google Fonts 下载字体时,你会得到一个包含每个字体系列变体的.ttf
文件的.zip
文件。我们将使用常规、粗体和斜体变体,因此将每个系列的对应.ttf
文件复制到我们的/assets/fonts
文件夹中。
如何做...
- 放置好我们的字体文件后,第一步是打开
App.js
并添加我们需要的导入:
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { Font } from 'expo';
- 接下来,我们将添加一个简单的组件来显示一些我们想要用我们自定义字体样式的文本。我们将从一个
Text
元素开始,显示 Roboto 字体的常规变体:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
</View>
);
}
}
- 让我们也为我们刚刚创建的组件添加一些初始样式。现在,我们只会增加我们的
josefinSans
类样式的字体大小:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
josefinSans: {
fontSize: 40,
}
});
- 如果我们现在在模拟器中打开应用程序,我们将看到“Hello, Josefin Sans!”文本以默认字体显示在屏幕中央:
- 让我们加载我们的
JosefinSans-Regular.ttf
字体文件,以便我们可以用它来样式化我们的文本。我们将使用 React Native 提供的componentDidMount
生命周期钩子来告诉我们的应用程序何时开始加载字体:
export default class App extends React.Component {
componentDidMount() {
Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-Regular.ttf'),
});
}
render() {
return (
<View style={styles.container}>
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
</View>
);
}
}
- 接下来,我们将添加我们正在加载的字体到应用于我们的
Text
元素的样式中:
const styles = StyleSheet.create({
// Other styles from step 3
josefinSans: {
fontSize: 40,
fontFamily: 'josefin-sans-regular'
}
});
- 我们现在有样式了,对吗?嗯,并不完全是。如果我们回头看看我们的模拟器,我们会看到我们得到了一个错误:
console.error: "fontFamily 'josefin-sans-regular' is not a system font and has not been loaded through Expo.Font.loadAsync"
- 但是我们刚刚通过
Expo.Font.loadAsync
加载了字体!怎么回事?事实证明我们面临一个竞争条件。我们为Text
元素定义的josefinSans
样式被应用在 Josefin Sans 字体加载之前。为了解决这个问题,我们需要使用组件的state
来跟踪字体的加载状态:
export default class App extends React.Component {
state = {
fontLoaded: false
};
- 现在,我们的组件有了一个
state
,一旦字体加载完成,我们就可以将状态的fontLoaded
属性更新为true
。使用 ES6 特性async
/await
使这变得简洁而直接。让我们在我们的componentDidMount
代码块中这样做:
async componentDidMount() {
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
});
}
- 由于我们现在正在等待
Font.loadAsync()
调用,一旦调用完成,我们可以将fontLoaded
的状态设置为true
:
async componentDidMount() {
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
});
this.setState({ fontLoaded: true });
}
- 现在要做的就是更新我们的
render
方法,只有在fontLoaded
状态属性为true
时才渲染依赖于自定义字体的Text
元素:
<View style={styles.container}>
{
this.state.fontLoaded ? (
<Text style={styles.josefinSans}>
Hello, Josefin Sans!
</Text>
) : null
}
</View>
- 现在,当我们在模拟器中查看我们的应用程序时,我们应该看到我们的自定义字体被应用:
- 让我们加载其余的字体,这样我们也可以在应用程序中使用它们:
await Font.loadAsync({
'josefin-sans-regular': require('./assets/fonts/JosefinSans-
Regular.ttf'),
'josefin-sans-bold': require('./assets/fonts/JosefinSans-
Bold.ttf'),
'josefin-sans-italic': require('./assets/fonts/JosefinSans-
Italic.ttf'),
'raleway-regular': require('./assets/fonts/Raleway-
Regular.ttf'),
'raleway-bold': require('./assets/fonts/Raleway-Bold.ttf'),
'raleway-italic': require('./assets/fonts/Raleway-
Italic.ttf'),
});
- 我们还需要
Text
元素来显示每个新字体系列/变体中的文本。请注意,由于 JSX 表达式要求只有一个父节点,我们还需要将所有的Text
元素包装在另一个View
元素中。我们现在还将style
属性传递给一个样式数组,以便在下一步中应用fontSize
和padding
样式:
render() {
return (
<View style={styles.container}>
{
this.state.fontLoaded ? (
<View style={styles.container}>
<Text style={[styles.josefinSans,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.josefinSansBold,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.josefinSansItalic,
styles.textFormatting]}>
Hello, Josefin Sans!
</Text>
<Text style={[styles.raleway, styles.textFormatting]}>
Hello, Raleway!
</Text>
<Text style={[styles.ralewayBold,
styles.textFormatting]}>
Hello, Raleway!
</Text>
<Text style={[styles.ralewayItalic,
styles.textFormatting]}>
Hello, Raleway!
</Text>
</View>
) : null
}
</View>
);
}
- 剩下的就是将我们的自定义字体应用到
StyleSheet
中的新样式:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
josefinSans: {
fontFamily: 'josefin-sans-regular',
},
josefinSansBold: {
fontFamily: 'josefin-sans-bold',
},
josefinSansItalic: {
fontFamily: 'josefin-sans-italic',
},
raleway: {
fontFamily: 'raleway-regular',
},
ralewayBold: {
fontFamily: 'josefin-sans-bold'
},
ralewayItalic: {
fontFamily: 'josefin-sans-italic',
},
textFormatting: {
fontSize: 40,
paddingBottom: 20
}
});
- 现在,在我们的应用程序中,我们将看到六个不同的文本元素,每个都使用自己的自定义字体样式:
工作原理...
在步骤 5和步骤 6中,我们使用了componentDidMount
React 生命周期钩子来告诉我们的应用程序何时完成加载。虽然使用componentWillMount
可能很诱人,但这也会引发错误,因为componentWillMount
不能保证等待我们的Font.loadAsync
完成。通过使用componentDidMount
,我们还可以确保不阻止应用程序的初始渲染。
在步骤 9中,我们使用了 ES6 特性async
/await
。如果您是 Web 开发人员,您可能已经熟悉了这种模式,但如果您想了解更多信息,我在本教程末尾的另请参阅部分中包含了一篇来自ponyfoo.com的精彩文章,该文章很好地解释了async
/await
的工作原理。
在步骤 11中,我们使用了一个三元语句,如果加载了我们的自定义字体样式的Text
元素,则渲染它,如果没有加载,则返回null
。
通过 Expo 加载的字体目前不支持fontWeight
或fontStyle
属性-您需要加载字体的这些变体,并按名称指定它们,就像我们在这里使用粗体和斜体一样。
另请参阅
关于async
/await
的一篇很棒的文章可以在ponyfoo.com/articles/understanding-javascript-async-await
找到。
使用字体图标
图标是几乎任何应用程序的必不可少的部分,特别是在导航和按钮中。与上一章中介绍的 Expo 字体助手类似,Expo 还有一个图标助手,使添加图标字体比使用原始的 React Native 要方便得多。在这个示例中,我们将看到如何使用图标助手模块与流行的FontAwesome
和Ionicons
图标字体库。
准备工作
我们需要为这个示例创建一个新项目。让我们将这个项目命名为font-icons
。
操作步骤
- 我们将首先打开
App.js
并导入构建应用程序所需的依赖项:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { FontAwesome, Ionicons } from '@expo/vector-icons';
- 接下来,我们可以添加应用程序的外壳,我们将在其中显示图标:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
</View>
);
}
}
- 在
View
元素内,让我们再添加两个View
元素来容纳每个图标集中的图标:
export default class App extends React.Component {
render() {
return (
<View style={styles.container}>
<View style={styles.iconRow}>
</View>
<View style={styles.iconRow}>
</View>
</View>
);
}
}
- 现在,让我们为我们声明的每个元素添加样式。正如我们在之前的示例中看到的,
container
样式使用flex: 1
填充屏幕,并使用alignItems
和justifyContent
将项目居中设置为center
。iconRow
属性将flexDirection
设置为row
,这样我们的图标将排成一行:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
iconRow: {
flexDirection: 'row',
},
});
- 现在我们的应用程序的基本结构已经就位,让我们添加我们的图标。在第一行图标中,我们将使用四个
FontAwesome
组件来显示FontAwesome
字体库中的四个图标。name
属性确定应该使用哪个图标,size
属性设置图标的像素大小,color
设置图标的颜色:
<View style={styles.iconRow}>
<FontAwesome style={styles.iconPadding} name="glass" size={48} color="green" />
<FontAwesome style={styles.iconPadding} name="beer" size={48} color="red" />
<FontAwesome style={styles.iconPadding} name="music" size={48} color="blue" />
<FontAwesome style={styles.iconPadding} name="taxi" size={48} color="#1CB5AD" />
</View>
就像在 CSS 中一样,color
属性可以是 CSS 规范中定义的颜色关键字(您可以在 MDN 文档的完整列表中查看developer.mozilla.org/en-US/docs/Web/CSS/color_value
),也可以是给定颜色的十六进制代码。
- 在接下来的
View
元素中,我们将添加来自Ionicons
字体库的图标。正如您所看到的,Ionicons
元素接受与上一步中使用的FontAwesome
元素相同的属性:
<View style={styles.iconRow}>
<Ionicons style={styles.iconPadding} name="md-pizza" size={48} color="orange" />
<Ionicons style={styles.iconPadding} name="md-tennisball" size={48} color="maroon" />
<Ionicons style={styles.iconPadding} name="ios-thunderstorm" size={48} color="purple" />
<Ionicons style={styles.iconPadding} name="ios-happy" size={48} color="#DF7977" />
</View>
- 这个配方的最后一步是添加剩下的样式
iconPadding
,它只是为每个图标添加一些填充,以均匀地间隔开每个图标:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
iconRow: {
flexDirection: 'row',
},
iconPadding: {
padding: 8,
}
});
- 就是这样!当我们查看我们的应用程序时,将会有两行图标,每一行分别展示来自
FontAwesome
和Ionicons
的图标:
它是如何工作的...
Expo 提供的vector-icons
包可以访问 11 个完整的图标集。你所需要做的就是导入相关的组件(例如,FontAwesome
组件用于 Font Awesome 图标),并为其提供与你想要使用的图标集中相对应的图标名称。你可以在vector-icons
目录中找到一个完整的、可搜索的图标列表,该目录托管在expo.github.io/vector-icons/
。只需将元素的name
属性设置为目录中列出的图标名称,添加size
和color
属性,就完成了!
正如 GitHub 上vector-icons
的 README 所述,这个库是为了在 Expo 中使用react-native-vector-icons
包提供的图标而创建的兼容层。你可以在github.com/oblador/react-native-vector-icons
找到这个包。如果你正在构建一个没有 Expo 的 React Native 应用程序,你可以使用react-native-vector-icons
库来获得相同的功能。
另请参阅
vector-icons
库中所有可用图标的目录可以在expo.github.io/vector-icons/
找到。
第四章:实现复杂用户界面-第二部分
本章将涵盖更多使用 React Native 构建 UI 的技巧。我们将首次了解如何链接到其他应用程序和网站,处理设备方向的变化,以及如何构建用于收集用户输入的表单。
在本章中,我们将涵盖以下技巧:
-
处理通用应用程序
-
检测方向变化
-
使用 WebView 嵌入外部网站
-
链接到网站和其他应用程序
-
创建一个表单组件
处理通用应用程序
使用 React Native 的好处之一是它能够轻松创建通用应用程序。我们可以在手机和平板应用程序之间共享大量代码。布局可能会根据设备而改变,但我们可以在布局之间重用代码片段。
在这个示例中,我们将构建一个可以在手机和平板上运行的应用程序。平板版本将包括不同的布局,但我们将重用相同的内部组件。
准备工作
对于这个示例,我们将展示一个联系人列表。目前,我们将从.json
文件中加载数据。我们将在以后的章节中探讨如何从表述性状态转移(REST)API 中加载远程数据。
让我们打开以下 URL 并将生成的 JSON 复制到名为data.json
的文件中,放在项目的根目录。我们将使用这些数据来渲染联系人列表。它返回一个假用户数据的 JSON 对象,网址是api.randomuser.me/?results=20
。
让我们创建一个名为universal-app
的新应用。
如何做...
- 让我们打开
App.js
并导入我们在上一节“准备工作”中创建的依赖项以及我们的data.json
文件。我们还将从./utils/Device
导入Device
实用程序,我们将在以后的步骤中构建它:
import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import Device from './utils/Device';
import data from './data.json';
- 在这里,我们将创建主要的
App
组件及其基本布局。这个顶层组件将决定是渲染手机还是平板 UI。我们只渲染两个Text
元素。renderDetail
文本应该只在平板上显示,而renderMaster
文本应该在手机和平板上显示:
export default class App extends Component {
renderMaster() {
return (
<Text>Render on phone and tablets!!</Text>
);
}
renderDetail() {
if (Device.isTablet()) {
return (
<Text>Render on tablets only!!</Text>
);
}
}
render() {
return (
<View style={styles.content}>
{this.renderMaster()}
{this.renderDetail()}
</View>
);
}
}
- 在
App
组件下,我们将添加一些基本样式。这些样式临时包括paddingTop: 40
,以便我们渲染的文本不会被设备的系统栏覆盖:
const styles = StyleSheet.create({
content: {
paddingTop: 40,
flex: 1,
flexDirection: 'row',
},
});
- 如果我们尝试按原样运行我们的应用程序,它将失败,并显示错误,告诉我们找不到
Device
模块,所以让我们创建它。这个实用程序类的目的是根据屏幕尺寸计算当前设备是手机还是平板电脑。它将有一个isTablet
方法和一个isPhone
方法。我们需要在项目的根目录中创建一个utils
文件夹,并添加一个Device.js
作为实用程序。现在我们可以添加实用程序的基本结构:
import { Dimensions, Alert } from 'react-native';
// Tablet portrait dimensions
const tablet = {
width: 552,
height: 960,
};
class Device {
// Added in next steps
}
const device = new Device();
export default device;
- 让我们开始构建实用程序,通过创建两种方法:一个用于获取纵向尺寸,另一个用于获取横向尺寸。根据设备旋转,
width
和height
的值将改变,这就是为什么我们需要这两种方法始终获取正确的值,无论设备是landscape
还是portrait
:
class Device {
getPortraitDimensions() {
const { width, height } = Dimensions.get("window");
return {
width: Math.min(width, height),
height: Math.max(width, height),
};
}
getLandscapeDimensions() {
const { width, height } = Dimensions.get("window");
return {
width: Math.max(width, height),
height: Math.min(width, height),
};
}
}
- 现在让我们创建我们的应用程序将用来确定应用程序是在平板电脑上运行还是在手机上运行的两种方法。为了计算这一点,我们需要获取纵向模式下的尺寸,并将它们与我们为平板电脑定义的尺寸进行比较:
isPhone() {
const dimension = this.getPortraitDimensions();
return dimension.height < tablet.height;
}
isTablet() {
const dimension = this.getPortraitDimensions();
return dimension.height >= tablet.height;
}
- 现在,如果我们打开应用程序,我们应该看到两种不同的文本被呈现,取决于我们是在手机上还是在平板上运行应用程序:
- 实用程序按预期工作!让我们回到主
App.js
的renderMaster
方法上。我们希望这个方法渲染存储在data.json
文件中的联系人列表。让我们导入一个新的组件,我们将在接下来的步骤中构建它,并更新renderMaster
方法以使用我们的新组件:
import UserList from './UserList';
export default class App extends Component {
renderMaster() {
return (
<UserList contacts={data.results} />
);
}
//...
}
- 让我们创建一个新的
UserList
文件夹。在这个文件夹里,我们需要为新组件创建index.js
和styles.js
文件。我们需要做的第一件事是将依赖项导入到新的index.js
中,创建UserList
类,并将其导出为default
:
import React, { Component } from 'react';
import {
StyleSheet,
View,
Text,
ListView,
Image,
TouchableOpacity,
} from 'react-native';
import styles from './styles';
export default class UserList extends Component {
// Defined in the following steps
}
- 我们已经介绍了如何创建列表。如果您不清楚
ListView
组件的工作原理,请阅读第二章中的显示项目列表配方,创建一个简单的 React Native 应用程序。在类的构造函数中,我们将创建dataSource
,然后将其添加到state
中:
export default class UserList extends Component {
constructor(properties) {
super(properties);
const dataSource = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.state = {
dataSource: dataSource.cloneWithRows(properties.contacts),
};
}
//...
}
render
方法也遵循了在第二章中介绍的ListView
配方中引入的相同模式,显示项目列表:
render() {
return (
<View style={styles.main}>
<Text style={styles.toolbar}>
My contacts!
</Text>
<ListView dataSource={this.state.dataSource}
renderRow={this.renderContact}
style={styles.main} />
</View> );
}
- 正如您所看到的,我们需要定义
renderContact
方法来呈现每一行。我们正在使用TouchableOpacity
组件作为主要包装器,这将允许我们在列表项被按下时执行一些操作的回调函数。目前,当按钮被按下时我们并没有做任何事情。我们将在第九章中学习如何使用 Redux 在组件之间进行通信,实现 Redux:
renderContact = (contact) => {
return (
<TouchableOpacity style={styles.row}>
<Image source={{uri: `${contact.picture.large}`}} style=
{styles.img} />
<View style={styles.info}>
<Text style={styles.name}>
{this.capitalize(contact.name.first)}
{this.capitalize(contact.name.last)}
</Text>
<Text style={styles.phone}>{contact.phone}</Text>
</View>
</TouchableOpacity>
);
}
- 我们没有使用样式来使文本大写,所以我们需要使用 JavaScript。
capitalize
函数非常简单,它将给定字符串的第一个字母设置为大写:
capitalize(value) {
return value[0].toUpperCase() + value.substring(1);
}
- 我们几乎完成了这个组件。剩下的只有
styles
。让我们打开/UserList/styles.js
文件,并为主容器和工具栏添加样式:
import { StyleSheet } from 'react-native';
export default StyleSheet.create({
main: {
flex: 1,
backgroundColor: '#dde6e9',
},
toolbar: {
backgroundColor: '#2989dd',
color: '#fff',
paddingTop: 50,
padding: 20,
textAlign: 'center',
fontSize: 20,
},
// Remaining styles added in next step.
});
- 现在,对于每一行,我们希望在左边呈现每个联系人的图像,右边是联系人的姓名和电话号码:
row: {
flexDirection: 'row',
padding: 10,
},
img: {
width: 70,
height: 70,
borderRadius: 35,
},
info: {
marginLeft: 10,
},
name: {
color: '#333',
fontSize: 22,
fontWeight: 'bold',
},
phone: {
color: '#aaa',
fontSize: 16,
},
- 让我们切换到
App.js
文件,并删除我们在步骤 7中用于使文本可读的paddingTop
属性;要删除的行已用粗体显示:
const styles = StyleSheet.create({
content: {
paddingTop: 40,
flex: 1,
flexDirection: 'row',
},
});
- 如果我们尝试运行我们的应用程序,我们应该能够在手机和平板上看到一个非常漂亮的列表,并且在两个不同的设备上看到相同的组件:
- 我们已经根据当前设备显示了两种不同的布局!现在我们需要在
UserDetail
视图上进行工作,它将显示所选的联系人。让我们打开App.js
,导入UserDetail
视图,并更新renderDetail
方法,如下所示:
import UserDetail from './UserDetail';
export default class App extends Component {
renderMaster() {
return (
<UserList contacts={data.results} />
);
}
renderDetail() {
if (Device.isTablet()) {
return (
<UserDetail contact={data.results[0]} />
);
}
}
}
如前所述,在这个食谱中,我们不专注于从一个组件向另一个组件发送数据,而是专注于在平板电脑和手机上呈现不同的布局。因此,对于这个食谱,我们将始终将第一条记录发送到用户详细信息视图。
- 为了简化事情并尽可能缩短食谱,对于用户详细信息视图,我们将只显示一个工具栏和一些显示给定记录的名字和姓氏的文本。我们将在这里使用一个无状态组件:
import React from 'react';
import {
View,
Text,
} from 'react-native';
import styles from './styles';
const UserList = ({ contact }) => (
<View style={styles.main}>
<Text style={styles.toolbar}>Details should go here!</Text>
<Text>
This is the detail view:{contact.name.first} {contact.name.last}
</Text>
</View>
);
export default UserList;
- 最后,我们需要为这个组件设置样式。我们希望将屏幕的四分之三分配给详细页面,四分之一分配给主列表。这可以通过使用 flexbox 轻松实现。由于
UserList
组件具有flex
属性为1
,我们可以将UserDetail
的flex
属性设置为3
,允许UserDetail
占据屏幕的 75%。以下是我们将添加到/UserDetail/styles.js
文件的样式:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
main: {
flex: 3,
backgroundColor: '#f0f3f4',
},
toolbar: {
backgroundColor: '#2989dd',
color: '#fff',
paddingTop: 50,
padding: 20,
textAlign: 'center',
fontSize: 20,
},
});
export default styles;
- 如果我们再次尝试运行我们的应用程序,我们会发现在平板上,它将呈现一个漂亮的布局,显示列表视图和详细视图,而在手机上,它只显示联系人列表。
工作原理...
在Device
实用程序中,我们导入了 React Native 提供的名为Dimension
的依赖项,用于获取当前设备的尺寸。我们还在Device
实用程序中定义了一个名为tablet
的常量,它是一个包含width
和height
的对象,用于与Dimension
一起计算设备是否为平板电脑。这个常量的值是基于市场上最小的 Android 平板电脑。
在步骤 5中,我们通过调用Dimensions.get("window")
方法获得了宽度和高度,然后根据我们想要的方向获得了最大值和最小值。
在步骤 12中,重要的是要注意我们使用箭头函数来定义renderContact
方法。使用箭头函数可以保持正确的绑定范围,否则,在调用this.capitalize
时,this
将绑定到错误的范围。查看另请参阅部分,了解有关this
关键字和箭头函数工作原理的更多信息。
另请参阅
-
从 ponyfoo 的[https://ponyfoo.com/articles/es6-arrow-functions-in-depth]中获得了对 ES6 箭头函数的良好解释。
-
Kyle Simpson 在[https://github.com/getify/You-Dont-Know-JS/blob/master/this %26 object prototypes/ch2.md]深入探讨了 JavaScript 中
this
的工作原理。
检测方向变化
在构建复杂的界面时,根据设备的方向渲染不同的 UI 组件是非常常见的。这在处理平板电脑时尤其如此。
在这个示例中,我们将根据屏幕方向渲染菜单。在横向时,我们将渲染一个带有图标和文本的扩展菜单,而在纵向时,我们只会渲染图标。
准备工作
为了支持方向变化,我们将使用 Expo 的辅助工具ScreenOrientation
。
我们还将使用 Expo 软件包@expo/vector-icons
提供的FontAwesome
组件。第二章中的使用字体图标一节描述了如何使用这个组件。
在开始之前,让我们创建一个名为screen-orientation
的新应用程序。我们还需要对 Expo 在目录根目录中创建的app.json
文件进行微调。这个文件有一些 Expo 在构建应用程序时使用的基本设置。其中之一是orientation
,它自动设置为portrait
,用于每个新应用程序。此设置确定应用程序允许的方向,并且可以设置为portrait
、landscape
或default
。如果我们将其更改为default
,我们的应用程序将允许纵向和横向方向。
要看到这些更改生效,请确保重新启动 Expo 项目。
如何做...
- 我们将首先打开
App.js
并添加我们将使用的导入:
import React from 'react';
import {
Dimensions,
StyleSheet,
Text,
View
} from 'react-native';
- 接下来,我们将添加空的
App
组件类,以及一些基本样式:
export default class App extends React.Component {
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff'
},
text: {
fontSize: 40,
}
});
- 在我们的应用程序框架就位后,我们现在可以添加
render
方法。在render
方法中,您会注意到我们使用了View
组件,并使用了onLayout
属性,这将在设备方向发生变化时触发。然后onLayout
将运行this.handleLayoutChange
,我们将在下一步中定义。在Text
元素中,我们只是显示state
对象上orientation
的值:
export default class App extends React.Component {
render() {
return (
<View
onLayout={() => this.handleLayoutChange}
style={styles.container}
>
<Text style={styles.text}>
{this.state.orientation}
</Text>
</View>
);
}
}
- 让我们创建组件的
handleLayoutChange
方法,以及handleLayoutChange
方法调用的getOrientation
函数。getOrientation
函数使用 React Native 的Dimensions
工具来获取屏幕的宽度和高度。如果height > width
,我们就知道设备处于纵向方向,如果不是,那么它就是横向方向。通过更新state
,将启动重新渲染,并且this.state.orientation
的值将反映方向:
handleLayoutChange() {
this.getOrientation();
}
getOrientation() {
const { width, height } = Dimensions.get('window');
const orientation = height > width ? 'Portrait' : 'Landscape';
this.setState({
orientation
});
}
- 如果我们此时运行应用程序,将会得到错误类型错误:null 不是对象:(评估'this.state.orientation')。这是因为
render
方法试图在this.state.orientation
值甚至被定义之前读取它。我们可以通过 React 生命周期componentWillMount
钩子在render
首次运行之前轻松解决这个问题来获取方向:
componentWillMount() {
this.getOrientation();
}
- 这就是我们寻找的基本功能所需的全部内容!再次运行应用程序,您应该看到显示的文本反映了设备的方向。旋转设备,方向文本应该更新:
- 现在
orientation
状态值已经正确更新,我们可以专注于 UI。如前所述,我们将创建一个菜单,根据当前方向稍微不同地呈现选项。让我们导入一个Menu
组件,我们将在接下来的步骤中构建它,并更新我们的App
组件的render
方法以使用新的Menu
组件。请注意,我们现在将this.state.orientation
传递给Menu
组件的orientation
属性:
import Menu from './Menu';
export default class App extends React.Component {
// ...
render() {
return (
<View
onLayout={() => {this.handleLayoutChange()}}
style={styles.container}
>
<Menu orientation={this.state.orientation} />
<View style={styles.main}>
<Text>Main Content</Text>
</View>
</View>
);
}
}
- 让我们也更新我们的
App
组件的样式。您可以用以下代码替换步骤 2中的样式。通过在container
样式上将flexDirection
设置为row
,我们将能够水平显示两个组件:
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
},
main: {
flex: 1,
backgroundColor: '#ecf0f1',
justifyContent: 'center',
alignItems: 'center',
}
});
- 接下来,让我们构建
Menu
组件。我们需要创建一个新的/Menu/index.js
文件,其中将定义Menu
类。这个组件将接收orientation
属性,并根据orientation
值决定如何呈现菜单选项。让我们首先导入这个类的依赖项:
import React, { Component } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import { FontAwesome } from '@expo/vector-icons';
- 现在我们可以定义
Menu
类。在state
对象上,我们将定义一个options
数组。这些option
对象将用于定义图标。如前一章中的使用字体图标中讨论的,我们可以通过关键字来定义图标,如在expo.github.io/vector-icons/
中的矢量图标目录中定义的那样:
export default class Menu extends Component {
state = {
options: [
{title: 'Dashboard', icon: 'dashboard'},
{title: 'Inbox', icon: 'inbox'},
{title: 'Graphs', icon: 'pie-chart'},
{title: 'Search', icon: 'search'},
{title: 'Settings', icon: 'gear'},
],
};
// Remainder defined in following steps
}
- 这个组件的
render
方法循环遍历state
对象中的options
数组:
render() {
return (
<View style={styles.content}>
{this.state.options.map(this.renderOption)}
</View>
);
}
- 正如您在上一步中的 JSX 中所看到的,有一个对
renderOption
的调用。在这个方法中,我们将为每个选项呈现图标和标签。我们还将使用方向值来切换显示标签,并更改图标的大小:
renderOption = (option, index) => {
const isLandscape = this.properties.orientation === 'Landscape';
const title = isLandscape
? <Text style={styles.title}>{option.title}</Text>
: null;
const iconSize = isLandscape ? 27 : 35;
return (
<View key={index} style={[styles.option, styles.landscape]}>
<FontAwesome name={option.icon} size={iconSize} color="#fff" />
{title}
</View>
);
}
在上一个代码块中,请注意我们正在定义key
属性。在动态创建新组件时,我们总是需要设置key
属性。该属性对于每个项目应该是唯一的,因为它在内部被 React 使用。在这种情况下,我们使用循环迭代的索引。这样,我们可以确保每个项目都有一个唯一的key
值,因为数据是静态的。您可以在官方文档中阅读更多关于它的信息reactjs.org/docs/lists-and-keys.html
。
- 最后,我们将为菜单定义样式。首先,我们将把
backgroundColor
设置为深蓝色,然后,对于每个选项,我们将改变flexDirection
以水平渲染图标和标签。其余的样式添加边距和填充,以便菜单项之间有很好的间距:
const styles = StyleSheet.create({
content: {
backgroundColor: '#34495e',
paddingTop: 50,
},
option: {
flexDirection: 'row',
paddingBottom: 15,
},
landscape: {
paddingRight: 30,
paddingLeft: 30,
},
title: {
color: '#fff',
fontSize: 16,
margin: 5,
marginLeft: 20,
},
});
- 如果我们现在运行我们的应用程序,它将根据屏幕的方向不同显示菜单 UI。旋转设备,布局将自动更新:
还有更多...
在这个示例中,我们查看了作为每个 Expo 项目一部分存在的app.json
文件。在这个文件中有许多有用的设置,可以调整这些设置会影响项目的构建过程。您可以使用这个文件来调整方向锁定,定义应用程序图标,并设置启动画面,以及其他许多设置。您可以在 Expo 配置文档中查看app.json
支持的所有设置,托管在docs.expo.io/versions/latest/guides/configuration.html
。
Expo 还提供了ScreenOrientation
实用程序,可以用来声明应用程序的允许方向。使用实用程序的主要方法ScreenOrientation.allow(orientation)
,将覆盖app.json
中的相应设置。该实用程序还提供比app.json
中设置更精细的选项,例如ALL_BUT_UPSIDE_DOWN
和LANDSCAPE_RIGHT
。有关此实用程序的更多信息,您可以阅读文档docs.expo.io/versions/latest/sdk/screen-orientation.html
。
使用 WebView 嵌入外部网站
对于许多应用程序,需要访问和在应用程序中显示外部链接是必需的。这可以用于显示第三方网站、在线帮助以及使用您的应用程序的条款和条件等。
在这个教程中,我们将看到如何在我们的应用程序中通过点击按钮打开一个 WebView,并动态设置 URL 值。我们还将在这个教程中使用react-navigation
包来创建基本的堆栈导航。请查看第三章中的设置和使用导航教程,深入了解构建导航。
如果您的应用程序更适合通过设备浏览器加载外部网站,请参阅下一个教程链接到网站和其他应用程序。
准备工作
我们需要为基于 WebView 的教程创建一个新的应用程序。让我们将我们的新应用命名为web-view
。我们还将使用react-navigation
,所以一定要安装这个包。您可以使用yarn
或npm
来安装这个包。在项目的根目录中,运行以下命令:
yarn add react-navigation
或者,使用npm
进行安装:
npm install --save react-navigation
如何做...
- 让我们从打开
App.js
文件开始。在这个文件中,我们将使用react-navigation
包提供的StackNavigator
组件。首先,让我们添加在这个文件中将要使用的导入。HomeScreen
是我们将在本教程中稍后构建的一个组件:
import React, { Component } from 'react';
import { StackNavigator } from 'react-navigation';
import HomeScreen from './HomeScreen';
- 现在我们有了导入,让我们使用
StackNavigator
组件来定义第一个路由;我们将使用一个带有链接的Home
路由,这些链接应该使用 React Native 的WebView
组件显示。navigationOptions
属性允许我们定义要在导航标题中显示的标题:
const App = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: ({ navigation }) => ({
title: 'Home'
}),
},
});
export default App;
- 现在我们准备好创建
HomeScreen
组件了。让我们在项目的根目录中创建一个名为HomeScreen
的新文件夹,并在文件夹中添加一个index.js
文件。和往常一样,我们可以从导入开始:
import React, { Component } from 'react';
import {
TouchableOpacity,
View,
Text,
SafeAreaView,
} from 'react-native';
import styles from './styles';
- 现在我们可以声明我们的
HomeScreen
组件。让我们还向组件添加一个state
对象,其中包含一个links
数组。这个数组中有一个对象,代表我们将在这个组件中使用的每个链接。我已经为您提供了四个links
供您使用;但是,您可以编辑每个links
数组对象中的title
和url
到任何您喜欢的网站:
export default class HomeScreen extends Component {
state = {
links: [
{
title: 'Smashing Magazine',
url: 'https://www.smashingmagazine.com/articles/'
},
{
title: 'CSS Tricks',
url: 'https://css-tricks.com/'
},
{
title: 'Gitconnected Blog',
url: 'https://medium.com/gitconnected'
},
{
title: 'Hacker News',
url: 'https://news.ycombinator.com/'
}
],
};
}
- 我们准备向该组件添加一个
render
函数。在这里,我们使用SafeAreaView
作为容器元素。这与普通的View
元素一样工作,但也考虑了 iPhone X 上的刘海区域,以便我们的布局不会被设备边框遮挡。您会注意到我们正在使用map
来遍历上一步中的links
数组,将每个传递给renderButton
函数:
render() {
return (
<SafeAreaView style={styles.container}>
<View style={styles.buttonList}>
{this.state.links.map(this.renderButton)}
</View>
</SafeAreaView>
);
}
- 现在我们已经定义了
render
方法,我们需要创建它正在使用的renderButton
方法。该方法将每个链接作为名为button
的参数,并且我们将使用index
作为key
的唯一标识符,用于renderButton
创建的每个元素。有关此点的更多信息,请参见本章第二个食谱中检测方向更改的步骤 12中的提示。
当按下TouchableOpacity
按钮元素时,将触发this.handleButtonPress(button)
:
renderButton = (button, index) => {
return (
<TouchableOpacity
key={index}
onPress={() => this.handleButtonPress(button)}
style={styles.button}
>
<Text style={styles.text}>{button.title}</Text>
</TouchableOpacity>
);
}
- 现在我们需要创建
handleButtonPress
方法,该方法在上一步中使用。此方法使用传入的button
参数的url
和title
属性。然后,我们可以在调用this.properties.navigation.navigate()
时使用这些属性,传递要导航到的路由的名称和应传递到该路由的参数。我们可以访问名为navigation
的property
,因为我们正在使用StackNavigator
,这是我们在步骤 2中设置的:
handleButtonPress(button) {
const { url, title } = button;
this.properties.navigation.navigate('Browser', { url, title });
}
HomeScreen
组件已经完成,除了样式。让我们在HomeScreen
文件夹中添加一个styles.js
文件来定义这些样式:
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
buttonList: {
flex: 1,
justifyContent: 'center',
},
button: {
margin: 10,
backgroundColor: '#c0392b',
borderRadius: 3,
padding: 10,
paddingRight: 30,
paddingLeft: 30,
},
text: {
color: '#fff',
textAlign: 'center',
},
});
export default styles;
- 现在,如果我们打开应用程序,我们应该看到
HomeScreen
组件呈现为带有四个链接按钮的列表,并且在每个设备上以本机样式呈现标题为 Home。然而,由于我们的StackNavigator
中没有Browser
路由,当按下按钮时实际上不会发生任何事情:
- 让我们返回
App.js
文件并添加Browser
路由。首先,我们需要导入BrowserScreen
组件,我们将在接下来的步骤中创建:
import BrowserScreen from './BrowserScreen';
- 现在
BrowserScreen
组件已经被导入,我们可以将它添加到StackNavigator
对象中,以创建一个Browser
路由。在navigationOptions
中,我们正在根据传递给路由的参数定义动态标题。这些参数与我们在步骤 7中作为第二个参数传递给navigation.navigate()
调用的对象相同:
const App = StackNavigator({
Home: {
screen: HomeScreen,
navigationOptions: ({ navigation }) => ({
title: 'Home'
}),
},
Browser: {
screen: BrowserScreen,
navigationOptions: ({ navigation }) => ({
title: navigation.state.params.title
}),
},
});
- 我们准备创建
BrowserScreen
组件。让我们在项目的根目录中创建一个名为BrowserScreen
的新文件夹,并在其中添加一个新的index.js
文件,然后添加此组件所需的导入:
import React, { Component } from 'react';
import { WebView } from 'react-native';
BrowserScreen
组件非常简单。它只包括一个渲染方法,该方法从传递给navigation.state
属性的params
属性中读取,以调用在步骤 7中定义的this.properties.navigation.navigate
,当按下按钮时触发。我们只需要渲染WebView
组件并将其source
属性设置为具有uri
属性设置为params.url
的对象:
export default class BrowserScreen extends Component {
render() {
const { params } = this.properties.navigation.state;
return(
<WebView
source={{uri: params.url}}
/>
);
}
}
- 现在,如果我们回到模拟器中运行的应用程序,我们可以看到我们的 WebView 在运行!
Hacker News 和 Smashing Magazine 从我们的应用程序中访问
它是如何工作的...
使用WebView
打开外部网站是让用户在我们的应用程序中消费外部网站的好方法。许多应用程序都这样做,允许用户轻松返回到应用程序的主要部分。
在步骤 6中,我们使用箭头函数将onPress
属性中的函数绑定到当前类实例的范围,因为我们在循环遍历链接数组时使用了这个函数。
在步骤 7中,每当按下按钮时,我们使用绑定到该按钮的标题和 URL,将它们作为参数传递,当我们导航到Browser
屏幕时。 步骤 11中的navigationOptions
使用相同的标题值作为屏幕的标题。navigationOptions
接受一个函数,其第一个参数是包含navigation
的对象,该对象在导航时使用参数。在步骤 11中,我们从这个对象中构造导航,以便我们可以将视图的标题设置为navigation.state.params.title
。
由于react-navigation
提供的StackNavigator
组件,我们得到了一个具有特定于操作系统的动画和内置返回按钮的标题。您可以阅读StackNavigation
文档,了解有关此组件的更多信息reactnavigation.org/docs/stack-navigator.html
。
第 13 步使用传递给BrowserScreen
组件的 URL 来使用 WebView,在 WebView 的source
属性中使用 URL 来呈现。您可以在官方文档中找到所有可用的 WebView 属性列表,位于facebook.github.io/react-native/docs/webview.html
。
链接到网站和其他应用程序
我们已经学会了如何使用WebView
将第三方网站呈现为我们应用程序的嵌入部分。然而,有时候,我们可能希望使用原生浏览器打开网站,链接到其他原生系统应用程序(如电子邮件、电话和短信),甚至深层链接到一个完全独立的应用程序。
在这个配方中,我们将通过原生浏览器和应用程序内的浏览器模态链接到外部网站,创建到电话和消息应用程序的链接,并创建一个深层链接,将打开 Slack 应用程序并自动加载gitconnected.com Slack 群中的#general 频道。
您需要在真实设备上运行此应用程序,以便在此应用程序中打开使用设备系统应用程序的链接,例如电子邮件、电话和短信链接。根据我的经验,这在模拟器中是行不通的。
准备工作
让我们为这个配方创建一个新的应用程序。我们将其称为linking-app
。
如何做到...
- 让我们从打开
App.js
并添加我们将要使用的导入开始:
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Platform } from 'react-native';
import { Linking } from 'react-native';
import { WebBrowser } from 'expo';
- 接下来,让我们添加一个
App
组件和一个state
对象。在这个应用程序中,state
对象将包含我们在这个配方中将使用的所有链接,放在一个名为links
的数组中。请注意,每个links
对象中的url
属性都附有一个协议(tel
、mailto
、sms
等)。设备使用这些协议来正确处理每个链接:
export default class App extends React.Component {
state = {
links: [
{
title: 'Call Support',
url: 'tel:+12025550170',
type: 'phone'
},
{
title: 'Email Support',
url: 'mailto:support@email.com',
type: 'email',
},
{
title: 'Text Support',
url: 'sms:+12025550170',
type: 'text message',
},
{
title: 'Join us on Slack',
url: 'slack://channel?team=T5KFMSASF&id=C5K142J57',
type: 'slack deep link',
},
{
title: 'Visit Site (internal)',
url: 'https://google.com',
type: 'internal link'
},
{
title: 'Visit Site (external)',
url: 'https://google.com',
type: 'external link'
}
]
}
}
在文本支持和呼叫支持按钮中使用的电话号码是在撰写时未使用的号码,由fakenumber.org/
生成。这个号码很可能仍然未被使用,但这可能会发生变化。请随意为这些链接使用不同的虚假号码,只需确保保持协议不变。
- 接下来,让我们为我们的应用程序添加
render
函数。这里的 JSX 很简单:我们从上一步中的state.links
数组中映射,将每个传递给我们在下一步中定义的renderButton
函数:
render() {
return(
<View style={styles.container}>
<View style={styles.buttonList}>
{this.state.links.map(this.renderButton)}
</View>
</View>
);
}
- 让我们来构建上一步中使用的
renderButton
方法。对于每个链接,我们使用TouchableOpacity
创建一个按钮,并将onPress
属性设置为执行handleButtonPress
并传递button
属性:
renderButton = (button, index) => {
return(
<TouchableOpacity
key={index}
onPress={() => this.handleButtonPress(button)}
style={styles.button}
>
<Text style={styles.text}>{button.title}</Text>
</TouchableOpacity>
);
}
- 接下来,我们可以构建
handleButtonPress
函数。在这里,我们将使用links
数组中每个对象中添加的type
属性。如果类型是'internal link'
,我们希望使用 Expo 的WebBrowser
组件的openBrowserAsync
方法在我们的应用程序中打开 URL,并且对于其他所有情况,我们将使用 React Native 的Linking
组件的openURL
方法。
如果openURL
调用出现问题,并且 URL 使用了slack://
协议,这意味着设备不知道如何处理该协议,可能是因为 Slack 应用未安装。我们将使用handleMissingApp
函数来处理这个问题,我们将在下一步中添加它:
handleButtonPress(button) {
if (button.type === 'internal link') {
WebBrowser.openBrowserAsync(button.url);
} else {
Linking.openURL(button.url).catch(({ message }) => {
if (message.includes('slack://')) {
this.handleMissingApp();
}
});
}
}
- 现在我们可以创建我们的
handleMissingApp
函数。在这里,我们使用 React Native 助手Platform
,它提供有关应用程序运行的平台的信息。Platform.OS
将始终返回操作系统,对于手机,应该始终解析为'ios'
或'android'
。您可以在官方文档中阅读有关Platform
功能的更多信息facebook.github.io/react-native/docs/platform-specific-code.html
。
如果 Slack 应用的链接不像预期那样工作,我们将再次使用Linking.openURL
,这次是为了在适合设备的应用商店中打开应用程序:
handleMissingApp() {
if (Platform.OS === 'ios') {
Linking.openURL(`https://itunes.apple.com/us/app/id618783545`);
} else {
Linking.openURL(
`https://play.google.com/store/applications/details?id=com.Slack`
);
}
}
- 我们的应用程序还没有任何样式,所以让我们添加一些。这里没有什么花哨的东西,只是将按钮居中对齐在屏幕中,着色和居中文本,并在每个按钮上提供填充:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
justifyContent: 'center',
alignItems: 'center',
},
buttonList: {
flex: 1,
justifyContent: 'center',
},
button: {
margin: 10,
backgroundColor: '#c0392b',
borderRadius: 3,
padding: 10,
paddingRight: 30,
paddingLeft: 30,
},
text: {
color: '#fff',
textAlign: 'center',
},
});
- 这就是这个应用程序的全部内容。一旦我们加载应用程序,应该会有一列按钮,代表我们的每个链接。
Call Support
和Email Support
按钮在 iOS 模拟器上不起作用。在真实设备上运行此示例,以查看所有链接正常工作。
它是如何工作的...
在步骤 2中,我们定义了应用程序使用的所有链接。每个链接对象都有一个type
属性,我们在步骤 5中定义的handleButtonPress
方法中使用它。
这个 handleButtonPress
函数使用链接的类型来确定将使用两种策略中的哪一种。如果链接的类型是 'internal link'
,我们希望在应用程序内部弹出一个模态框,使用设备浏览器打开链接。为此,我们可以使用 Expo 的 WebBrowser
助手,将 URL 传递给它的 openBrowserAsync
方法。如果链接的类型是 'external link'
,我们将使用 React Native 的 Linking
助手打开链接。这让您可以看到从应用程序中打开网站的不同方式。
Linking
助手还可以处理除了 HTTP 和 HTTPS 之外的其他协议。通过在传递给 Linking.openURL
的链接中简单地使用适当的协议,我们可以打开电话 (tel:
)、短信 (sms:
) 或电子邮件 (mailto:
)。
Linking.openURL
也可以处理到其他应用程序的深层链接,只要您要链接到的应用程序具有相应的协议,例如我们如何使用 slack://
协议打开 Slack。有关 Slack 的深层链接协议以及您可以使用它做什么的更多信息,请访问他们的文档 api.slack.com/docs/deep-linking
。
在 步骤 5 中,我们通过调用 Linking.openURL
引起的任何错误,检查错误是否是由 Slack 协议引起的 message.includes('slack://')
,如果是,我们知道 Slack 应用程序未安装在设备上。在这种情况下,我们触发 handleMissingApp
,使用由 Platform.OS
确定的适当链接打开 Slack 的应用商店链接。
另请参阅
Linking
模块的官方文档可以在 docs.expo.io/versions/latest/guides/linking.html
找到。
创建一个表单组件
大多数应用程序都需要一种输入数据的方式,无论是一个简单的注册和登录表单,还是一个具有许多输入字段和控件的更复杂的组件。
在这个示例中,我们将创建一个表单组件来处理文本输入。我们将使用不同的键盘收集数据,并显示包含结果信息的警报消息。
准备工作
我们需要创建一个空的应用。让我们把它命名为 user-form
。
如何做...
- 让我们首先打开
App.js
并添加我们的导入。导入包括我们稍后将构建的UserForm
组件:
import React from 'react';
import {
Alert,
StyleSheet,
ScrollView,
SafeAreaView,
Text,
TextInput,
} from 'react-native';
import UserForm from './UserForm';
- 由于这个组件将非常简单,我们将为我们的
App
创建一个无状态组件。我们将只在UserForm
组件的ScrollView
中渲染一个顶部工具栏:
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Fitness App</Text>
<ScrollView style={styles.content}>
<UserForm />
</ScrollView>
</SafeAreaView>
);
const styles = StyleSheet.create({
// Defined in a later step
});
export default App;
- 我们需要为这些组件添加一些样式。我们将添加一些颜色和填充,还将把
main
类设置为flex: 1
,以填充屏幕的其余部分:
const styles = StyleSheet.create({
main: {
flex: 1,
backgroundColor: '#ecf0f1',
},
toolbar: {
backgroundColor: '#1abc9c',
padding: 20,
color: '#fff',
fontSize: 20,
},
content: {
padding: 10,
},
});
- 我们已经定义了主要的
App
组件。现在让我们开始实际的表单工作。让我们在项目的基础上创建一个名为UserForm
的新目录,并添加一个index.js
文件。然后,我们将为这个类导入所有的依赖项:
import React, { Component } from 'react';
import {
Alert,
StyleSheet,
View,
Text,
TextInput,
TouchableOpacity,
} from 'react-native';
- 这个类将渲染输入并跟踪数据。我们将把数据保存在
state
对象上,所以我们将从初始化state
作为空对象开始:
export default class UserForm extends Component {
state = {};
// Defined in a later step
}
const styles = StyleSheet.create({
// Defined in a later step
});
- 在
render
方法中,我们将定义我们想要显示的组件,这种情况下是三个文本输入和一个按钮。我们将定义一个renderTextfield
方法,它接受一个配置对象作为参数。我们将定义字段的name
、placeholder
和应该在输入上使用的keyboard
类型。此外,我们还调用一个renderButton
方法,它将渲染保存按钮:
render() {
return (
<View style={styles.panel}>
<Text style={styles.instructions}>
Please enter your contact information
</Text>
{this.renderTextfield({ name: 'name', placeholder: 'Your
name' })}
{this.renderTextfield({ name: 'phone', placeholder: 'Your
phone number', keyboard: 'phone-pad' })}
{this.renderTextfield({ name: 'email', placeholder: 'Your
email address', keyboard: 'email-address'})}
{this.renderButton()}
</View>
);
}
- 要渲染文本字段,我们将在
renderTextfield
方法中使用TextInput
组件。这个TextInput
组件由 React Native 提供,在 iOS 和 Android 上都可以使用。keyboardType
属性允许我们设置要使用的键盘。在两个平台上有四种可用的键盘,分别是default
、numeric
、email-address
和phone-pad
:
renderTextfield(options) {
return (
<TextInput
style={styles.textfield}
onChangeText={(value) => this.setState({ [options.name]:
value })}
placeholder={options.label}
value={this.state[options.name]}
keyboardType={options.keyboard || 'default'}
/>
);
}
- 我们已经知道如何渲染按钮并响应
Press
操作。如果这不清楚,我建议阅读第三章中的使用主题支持创建可重用按钮配方,实现复杂用户界面-第一部分:
renderButton() {
return (
<TouchableOpacity
onPress={this.handleButtonPress}
style={styles.button}
>
<Text style={styles.buttonText}>Save</Text>
</TouchableOpacity>
);
}
- 我们需要定义
onPressButton
回调。为简单起见,我们将只显示一个带有我们在state
对象上的输入数据的警报:
handleButtonPress = () => {
const { name, phone, email } = this.state;
Alert.alert(`User's data`,`Name: ${name}, Phone: ${phone},
Email: ${email}`);
}
- 我们几乎完成了这个配方!我们需要做的就是应用一些样式-一些颜色、填充和边距;真的没什么花哨的:
const styles = StyleSheet.create({
panel: {
backgroundColor: '#fff',
borderRadius: 3,
padding: 10,
marginBottom: 20,
},
instructions: {
color: '#bbb',
fontSize: 16,
marginTop: 15,
marginBottom: 10,
},
textfield: {
height: 40,
marginBottom: 10,
},
button: {
backgroundColor: '#34495e',
borderRadius: 3,
padding: 12,
flex: 1,
},
buttonText: {
textAlign: 'center',
color: '#fff',
fontSize: 16,
},
});
- 如果我们运行我们的应用程序,我们应该能够看到一个在 Android 和 iOS 上都使用本机控件的表单,这是预期的:
当在模拟器上运行应用程序时,您可能无法看到由
keyboardType
定义的键盘。在真实设备上运行应用程序,以确保keyboardType
正确地为每个TextInput
更改键盘。
工作原理...
在步骤 8中,我们定义了TextInput
组件。在 React(和 React Native)中,我们可以使用两种类型的输入:受控和未受控组件。在这个示例中,我们正在使用受控输入组件,这是 React 团队推荐的。
受控组件将有一个value
属性,并且组件将始终显示value
属性的内容。这意味着我们需要一种方法在用户开始输入时更改值。如果我们不更新该值,那么输入框中的文本永远不会改变,即使用户尝试输入东西。
为了更新value
,我们可以使用onChangeText
回调并设置新值。在这个例子中,我们使用状态来跟踪数据,并在状态上设置一个新的键,其中包含输入的内容。
另一方面,一个未受控制的组件将不会有一个分配的value
属性。我们可以使用defaultValue
属性分配一个初始值。未受控制的组件有它们自己的状态,我们可以使用onChangeText
回调来获取它们的值,就像我们可以使用受控组件一样。
第五章:实施复杂用户界面-第三部分
在本章中,我们将涵盖以下示例:
-
创建地图应用程序
-
创建音频播放器
-
创建图像轮播
-
将推送通知添加到您的应用程序
-
实施基于浏览器的身份验证
介绍
在本章中,我们将介绍您可能需要添加到应用程序的一些更高级功能。本章中我们将构建的应用程序包括构建完全功能的音频播放器,地图集成以及实施基于浏览器的身份验证,以便您的应用程序可以连接到开发人员的公共 API。
创建地图应用程序
使用移动设备是一种便携式体验,因此地图是许多 iOS 和 Android 应用程序的常见部分并不奇怪。您的应用程序可能需要告诉用户他们在哪里,他们要去哪里,或者其他用户实时在哪里。
在这个示例中,我们将制作一个简单的应用程序,该应用程序在 Android 上使用 Google Maps,在 iOS 上使用 Apple 的地图应用程序,以显示以用户位置为中心的地图。我们将使用 Expo 的Location
辅助库来获取用户的纬度和经度,并将使用这些数据来使用 Expo 的MapView
组件渲染地图。MapView
是由 Airbnb 创建的 react-native-maps 包的 Expo 版本,因此您可以期望 react-native-maps 文档适用,该文档可以在github.com/react-community/react-native-maps
找到。
准备工作
我们需要为这个示例创建一个新的应用程序。让我们称之为map-app
。由于此示例中的用户图标将使用自定义图标,因此我们还需要一个图像。我使用了 Maico Amorim 的图标 You Are Here,您可以从thenounproject.com/term/you-are-here/12314/
下载。随意使用任何您喜欢的图像来代表用户图标。将图像保存到项目根目录的assets
文件夹中。
如何做...
- 我们将首先打开
App.js
并添加我们的导入:
import React from 'react';
import {
Location,
Permissions,
MapView,
Marker
} from 'expo';
import {
StyleSheet,
Text,
View,
} from 'react-native';
- 接下来,让我们定义
App
类和初始state
。在这个示例中,state
只需要跟踪用户的位置
,我们将其初始化为null
:
export default class App extends Component {
state = {
location: null
}
// Defined in following steps
}
- 接下来,我们将定义
componentDidMount
生命周期钩子,它将要求用户授予访问设备地理位置的权限。如果用户授予应用程序使用其位置的权限,返回的对象将具有一个值为'granted'
的status
属性。如果授予了权限,我们将使用this.getLocation
获取用户的位置,这是在下一步中定义的:
async componentDidMount() {
const permission = await Permissions.askAsync(Permissions.LOCATION);
if (permission.status === 'granted') {
this.getLocation();
}
}
getLocation
函数很简单。它使用Location
组件的getCurrentPositionAsync
方法从设备的 GPS 中获取位置信息,然后将该位置信息保存到state
中。该信息包含用户的纬度和经度,在渲染地图时我们将使用它:
async getLocation() {
let location = await Location.getCurrentPositionAsync({});
this.setState({
location
});
}
- 现在,让我们使用该位置信息来渲染我们的地图。首先,我们将检查
state
上是否保存了一个location
。如果是,我们将渲染MapView
,否则渲染null
。我们需要设置的唯一属性来渲染地图是initialRegion
属性,它定义了地图在首次渲染时应该显示的位置。我们将在具有保存到state
的纬度和经度的对象上传递这个属性,并使用latitudeDelta
和longitudeDelta
定义一个起始缩放级别:
renderMap() {
return this.state.location ?
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
// Map marker is defined in next step
</MapView> : null
}
- 在
MapView
中,我们需要在用户当前位置添加一个标记。Marker
组件是MapView
的父组件的一部分,所以在 JSX 中,我们将定义MapView.Marker
作为MapView
元素的子元素。这个元素需要用户的位置、标题和描述以在图标被点击时显示,以及通过image
属性定义一个自定义图像:
<MapView
style={styles.map}
initialRegion={{
latitude: this.state.location.coords.latitude,
longitude: this.state.location.coords.longitude,
latitudeDelta: 0.09,
longitudeDelta: 0.04,
}}
>
<MapView.Marker
coordinate={this.state.location.coords}
title={"User Location"}
description={"You are here!"}
image={require('./assets/you-are-here.png')}
/>
</MapView> : null
- 现在,让我们定义我们的
render
函数。它简单地在一个包含的View
元素中渲染地图:
render() {
return (
<View style={styles.container}>
{this.renderMap()}
</View>
);
}
- 最后,让我们添加我们的样式。我们将在容器和地图上都将
flex
设置为1
,以便两者都填满屏幕:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
map: {
flex: 1
}
});
- 现在,如果我们打开应用程序,我们将看到一个地图在设备提供的位置上渲染了我们自定义的用户图标!不幸的是,Google 地图集成可能无法在 Android 模拟器中工作,因此可能需要一个真实的设备来测试应用程序的 Android 实现。查看本食谱末尾的还有更多...部分以获取更多信息。不要惊讶,iOS 应用程序在模拟器上运行时显示用户的位置在旧金山;这是由于 Xcode 位置默认设置的工作方式。在真实的 iOS 设备上运行它,以查看它是否渲染了你的位置:
工作原理...
通过利用 Expo 提供的MapView
组件,在你的 React Native 应用中实现地图现在比以前简单直接得多。
在步骤 3中,我们利用了Permissions
帮助库。Permissions
有一个叫做askAsync
的方法,它接受一个参数,定义了你的应用想要从用户那里请求什么类型的权限。Permissions
还为你可以从用户那里请求的每种类型的权限提供了常量。这些权限类型包括LOCATION
,NOTIFICATIONS
(我们将在本章后面使用),CAMERA
,AUDIO_RECORDING
,CONTACTS
,CAMERA_ROLL
和CALENDAR
。由于我们在这个示例中需要位置,我们传入了常量Permissions.LOCATION
。一旦askAsync
返回 promise 解析,返回对象将有一个status
属性和一个expiration
属性。如果用户已经允许了请求的权限,status
将被设置为'granted'
字符串。如果被授予,我们将触发我们的getLocation
方法。
在步骤 4中,我们定义了从设备 GPS 获取位置的函数。我们调用Location
组件的getCurrentPositionAsync
方法。这个方法将返回一个带有coords
属性和timestamp
属性的对象。coords
属性让我们可以访问latitude
和longitude
,以及altitude
,accuracy
(位置的不确定性半径,以米为单位测量),altitudeAccuracy
(高度值的精度,以米为单位(仅限 iOS)),heading
和speed
。一旦接收到,我们将位置保存到state
中,这样render
函数将被调用,我们的地图将被渲染。
在步骤 5中,我们定义了renderMap
方法来渲染地图。首先,我们检查是否有位置,如果有,我们渲染MapView
元素。这个元素只需要我们定义一个属性的值:initialRegion
。这个属性接受一个带有四个属性的对象:latitude
,longitude
,latitudeDelta
和longitudeDelta
。我们将latitude
和longitude
设置为state
对象中的值,并为latitudeDelta
和longitudeDelta
提供初始值。这两个属性决定了地图应该以什么初始缩放级别进行渲染;这个数字越大,地图就会显示得越远。我建议尝试这两个值,看看它们如何影响渲染的地图。
在步骤 6中,我们通过将MapView.Marker
元素作为MapView
元素的子元素添加到地图上。我们通过将保存在state
(state.location.coords
)上的信息传递给coords
属性来定义坐标,并在被点击时为标记的弹出窗口设置了title
和description
。我们还可以通过在image
属性中使用require
语句内联我们的自定义图像来轻松定义自定义图钉。
还有更多...
如前所述,您可以阅读 react-native-maps 项目的文档,了解这个优秀库的更多功能(github.com/react-community/react-native-maps
)。例如,您可以使用 Google 地图样式向导(mapstyle.withgoogle.com/
)轻松自定义 Google 地图的外观,生成mapStyle
JSON 对象,然后将该对象传递给MapView
组件的customMapStyle
属性。或者,您可以使用Polygon
和Circle
组件向地图添加几何形状。
一旦您准备部署您的应用程序,您需要采取一些后续步骤来确保地图在 Android 上正常工作。您可以阅读 Expo 文档中有关如何使用MapView
组件部署到独立 Android 应用程序的详细信息:docs.expo.io/versions/latest/sdk/map-view#deploying-to-a-standalone-app-on-android
。
在 Android 模拟器中渲染 Google 地图可能会出现问题。您可以参考以下 GitHub 链接获取更多信息:github.com/react-native-community/react-native-maps/issues/942
。
创建音频播放器
音频播放器是许多应用程序内置的常见界面。无论您的应用程序需要在设备上本地播放音频文件还是从远程位置流式传输音频,Expo 的Audio
组件都可以帮助您。
在这个食谱中,我们将构建一个功能齐全的基本音频播放器,具有播放/暂停、下一曲和上一曲功能。为简单起见,我们将硬编码我们将使用的曲目信息,但在现实世界的情况下,您可能会使用类似我们定义的对象:一个带有曲目标题、专辑名称、艺术家名称和远程音频文件 URL 的对象。我从互联网档案馆的现场音乐档案中随机选择了三个现场曲目(archive.org/details/etree
)。
准备工作
我们需要为这个食谱创建一个新的应用。让我们称之为audio-player
。
如何做...
- 让我们从打开
App.js
并添加我们需要的依赖开始:
import React, { Component } from 'react';
import { Audio } from 'expo';
import { Feather } from '@expo/vector-icons';
import {
StyleSheet,
Text,
TouchableOpacity,
View,
Dimensions
} from 'react-native';
- 音频播放器需要音频来播放。我们将创建一个
playlist
数组来保存音频曲目。每个曲目由一个带有title
、artist
、album
和uri
的对象表示:
const playlist = [
{
title: 'People Watching',
artist: 'Keller Williams',
album: 'Keller Williams Live at The Westcott Theater on 2012-09-22',
uri: 'https://ia800308.us.archive.org/7/items/kwilliams2012-09-22.at853.flac16/kwilliams2012-09-22at853.t16.mp3'
},
{
title: 'Hunted By A Freak',
artist: 'Mogwai',
album: 'Mogwai Live at Ancienne Belgique on 2017-10-20',
uri: 'https://ia601509.us.archive.org/17/items/mogwai2017-10-20.brussels.fm/Mogwai2017-10-20Brussels-07.mp3'
},
{
title: 'Nervous Tic Motion of the Head to the Left',
artist: 'Andrew Bird',
album: 'Andrew Bird Live at Rio Theater on 2011-01-28',
uri: 'https://ia800503.us.archive.org/8/items/andrewbird2011-01-28.early.dr7.flac16/andrewbird2011-01-28.early.t07.mp3'
}
];
- 接下来,我们将定义我们的
App
类和初始的state
对象,其中包含四个属性:
-
isPlaying
用于定义播放器是正在播放还是暂停 -
playbackInstance
用于保存Audio
实例 -
volume
和currentTrackIndex
用于当前播放的曲目 -
isBuffering
用于在曲目在播放开始时缓冲时显示缓冲中...
消息
如下所示的代码:
export default class App extends Component {
state = {
isPlaying: false,
playbackInstance: null,
volume: 1.0,
currentTrackIndex: 0,
isBuffering: false,
}
// Defined in following steps
}
- 让我们接下来定义
componentDidMount
生命周期钩子。我们将使用这个方法通过setAudioModeAsync
方法配置Audio
组件,传入一个带有一些推荐设置的options
对象。这些将在食谱末尾的它是如何工作...部分进行更多讨论。之后,我们将使用loadAudio
加载音频,定义在下一步中:
async componentDidMount() {
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
playThroughEarpieceAndroid: true,
interruptionModeIOS: Audio.INTERRUPTION_MODE_IOS_DO_NOT_MIX,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
interruptionModeAndroid:
Audio.INTERRUPTION_MODE_ANDROID_DO_NOT_MIX,
});
this.loadAudio();
}
loadAudio
函数将处理我们播放器的音频加载。首先,我们将创建一个新的Audio.Sound
实例。然后,我们将在我们的新Audio
实例上调用setOnPlaybackStatusUpdate
方法,传入一个处理程序,每当实例内的播放状态发生变化时将被调用。最后,我们在实例上调用loadAsync
,传入一个来自playlist
数组的源,以及一个带有音量和state
的isPlaying
值的shouldPlay
属性的状态对象。第三个参数决定我们是否希望在播放之前等待文件下载完成,因此我们传入false
:
async loadAudio() {
const playbackInstance = new Audio.Sound();
const source = {
uri: playlist[this.state.currentTrackIndex].uri
}
const status = {
shouldPlay: this.state.isPlaying,
volume: this.state.volume,
};
playbackInstance
.setOnPlaybackStatusUpdate(
this.onPlaybackStatusUpdate
);
await playbackInstance.loadAsync(source, status, false);
this.setState({
playbackInstance
});
}
- 我们仍然需要定义处理状态更新的回调。在这个函数中,我们需要做的就是将从
setOnPlaybackStatusUpdate
函数调用中传入的status
参数上的isBuffering
值设置到state
上的isBuffering
值:
onPlaybackStatusUpdate = (status) => {
this.setState({
isBuffering: status.isBuffering
});
}
- 我们的应用现在知道如何从
playlist
数组中加载音频文件,并更新state
中加载的音频文件的当前缓冲状态,我们稍后将在render
函数中使用它向用户显示消息。现在剩下的就是为播放器本身添加行为。首先,我们将处理播放/暂停状态。handlePlayPause
方法检查this.state.isPlaying
的值,以确定是否应播放或暂停曲目,并相应地调用playbackInstance
上的关联方法。最后,我们需要更新state
中的isPlaying
的值:
handlePlayPause = async () => {
const { isPlaying, playbackInstance } = this.state;
isPlaying ? await playbackInstance.pauseAsync() : await playbackInstance.playAsync();
this.setState({
isPlaying: !isPlaying
});
}
- 接下来,让我们定义处理跳转到上一首曲目的函数。首先,我们通过调用
unloadAsync
从playbackInstance
中清除当前曲目。然后,我们将state
中的currentTrackIndex
值更新为当前值减一,或者如果我们在playlist
数组的开头,则更新为0
。然后,我们将调用this.loadAudio
来加载正确的曲目:
handlePreviousTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex === 0 ? currentTrackIndex = playlist.length
- 1 : currentTrackIndex -= 1;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
- 毫不奇怪,
handleNextTrack
与前面的函数相同,但这次我们要么将当前索引加1
,要么如果我们在playlist
数组的末尾,则将索引设置为0
:
handleNextTrack = async () => {
let { playbackInstance, currentTrackIndex } = this.state;
if (playbackInstance) {
await playbackInstance.unloadAsync();
currentTrackIndex < playlist.length - 1 ? currentTrackIndex +=
1 : currentTrackIndex = 0;
this.setState({
currentTrackIndex
});
this.loadAudio();
}
}
- 现在是时候定义我们的
render
函数了。在我们的 UI 中,我们需要三个基本部分:当曲目正在播放但仍在缓冲时显示“缓冲中…”的消息,用于显示当前曲目信息的部分,以及用于保存播放器控件的部分。当且仅当this.state.isBuffering
和this.state.isPlaying
都为true
时,“缓冲中…”消息才会显示。歌曲信息是通过renderSongInfo
方法呈现的,我们将在步骤 12中定义:
render() {
return (
<View style={styles.container}>
<Text style={[styles.largeText, styles.buffer]}>
{this.state.isBuffering && this.state.isPlaying ?
'Buffering...' : null}
</Text>
{this.renderSongInfo()}
<View style={styles.controls}>
// Defined in next step.
</View>
</View>
);
}
- 播放器控件由三个
TouchableOpacity
按钮元素组成,每个按钮都有来自 Feather 图标库的相应图标。您可以在第三章中找到有关使用图标的更多信息,实现复杂用户界面-第一部分。根据this.state.isPlaying
的值,我们将确定是显示播放图标还是暂停图标:
<View style={styles.controls}>
<TouchableOpacity
style={styles.control}
onPress={this.handlePreviousTrack}
>
<Feather name="skip-back" size={32} color="#fff"/>
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handlePlayPause}
>
{this.state.isPlaying ?
<Feather name="pause" size={32} color="#fff"/> :
<Feather name="play" size={32} color="#fff"/>
}
</TouchableOpacity>
<TouchableOpacity
style={styles.control}
onPress={this.handleNextTrack}
>
<Feather name="skip-forward" size={32} color="#fff"/>
</TouchableOpacity>
</View>
renderSongInfo
方法返回用于显示当前播放的曲目相关元数据的基本 JSX:
renderSongInfo() {
const { playbackInstance, currentTrackIndex } = this.state;
return playbackInstance ?
<View style={styles.trackInfo}>
<Text style={[styles.trackInfoText, styles.largeText]}>
{playlist[currentTrackIndex].title}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].artist}
</Text>
<Text style={[styles.trackInfoText, styles.smallText]}>
{playlist[currentTrackIndex].album}
</Text>
</View>
: null;
}
- 现在剩下的就是添加样式。这里定义的样式现在已经是老生常谈了,不超出居中、颜色、字体大小以及添加填充和边距的范围:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#191A1A',
alignItems: 'center',
justifyContent: 'center',
},
trackInfo: {
padding: 40,
backgroundColor: '#191A1A',
},
buffer: {
color: '#fff'
},
trackInfoText: {
textAlign: 'center',
flexWrap: 'wrap',
color: '#fff'
},
largeText: {
fontSize: 22
},
smallText: {
fontSize: 16
},
control: {
margin: 20
},
controls: {
flexDirection: 'row'
}
});
- 您现在可以在模拟器中查看您的应用,应该有一个完全正常工作的音频播放器!请注意,Android 模拟器中的音频播放速度可能太慢,无法正常工作,并且可能听起来非常杂乱。在真实的 Android 设备上打开应用程序以听到音轨正常播放:
工作原理...
在步骤 4中,一旦应用程序完成加载,我们就在componentDidMount
方法中对Audio
组件进行了初始化。Audio
组件的setAudioModeAsync
方法将一个选项对象作为其唯一参数。
让我们回顾一些我们在这个配方中使用的选项:
-
interruptionModeIOS
和interruptionModeAndroid
设置了您的应用中的音频应该如何与设备上其他应用程序的音频进行交互。我们分别使用了Audio
组件的INTERRUPTION_MODE_IOS_DO_NOT_MIX
和INTERRUPTION_MODE_ANDROID_DO_NOT_MIX
枚举来声明我们的应用音频应该中断任何其他正在播放音频的应用程序。 -
playsInSilentModeIOS
是一个布尔值,用于确定当设备处于静音模式时,您的应用是否应该播放音频。 -
shouldDuckAndroid
是一个布尔值,用于确定当另一个应用的音频中断您的应用时,您的应用的音频是否应该降低音量(减小)。虽然此设置默认为true
,但我已将其添加到配方中,以便您知道这是一个选项。
在步骤 5中,我们定义了loadAudio
方法,该方法在这个示例中承担了大部分工作。首先,我们创建了Audio.Sound
类的新实例,并将其保存到playbackInstance
变量中以供以后使用。接下来,我们设置将传递到playbackInstance
的loadAsync
函数的source
和status
变量,用于实际加载音频文件。在source
对象中,我们将uri
属性设置为playlist
数组中对象中的相应uri
属性的索引存储在this.state.currentTrackIndex
中。在status
对象中,我们将音量设置为state
上保存的volume
值,并将shouldPlay
设置为一个布尔值,用于确定音频是否应该播放,最初设置为this.state.isPlaying
。由于我们希望流式传输远程 MP3 文件而不是等待整个文件下载,因此我们将第三个参数downloadFirst
设置为false
。
在调用loadAsync
方法之前,我们首先调用了playbackInstance
的setOnPlaybackStatusUpdate
,它接受一个回调函数,当playbackInstance
的状态发生变化时应该被调用。我们在步骤 6中定义了该处理程序。该处理程序简单地将回调的status
参数中的isBuffering
值保存到state
的isBuffering
属性中,这将触发重新渲染,相应地更新 UI 中的'缓冲中...'消息。
在步骤 7中,我们定义了handlePlayPause
函数,用于在应用程序中切换播放和暂停功能。如果有曲目正在播放,this.state.isPlaying
将为true
,因此我们将在playbackInstance
上调用pauseAsync
函数,否则,我们将调用playAsync
来重新开始播放音频。一旦我们播放或暂停,我们就会更新state
上的isPlaying
的值。
在步骤 8和步骤 9中,我们创建了处理跳转到下一首和上一首曲目的函数。每个函数根据需要增加或减少this.state.currentTrackIndex
的值,因此在每个函数底部调用this.loadAudio
时,它将加载与playlist
数组中对象相关联的曲目的新索引。
还有更多...
我们当前应用程序的功能比大多数音频播放器更基本,但您可以利用所有工具来构建功能丰富的音频播放器。例如,您可以通过在setOnPlaybackStatusUpdate
回调中利用status
参数上的positionMillis
属性在 UI 中显示当前曲目时间。或者,您可以使用 React Native 的Slider
组件允许用户调整音量或播放速率。Expo 的Audio
组件提供了构建出色音频播放器应用程序的所有基本组件。
创建图像轮播
有各种应用程序使用图像轮播。每当有一组图像,您希望用户能够浏览时,轮播很可能是实现任务的最有效的 UI 模式之一。
在 React Native 社区中有许多软件包用于处理轮播的创建,但根据我的经验,没有一个比 react-native-snap-carousel (github.com/archriss/react-native-snap-carousel
)更稳定或更多功能。该软件包为自定义轮播的外观和行为提供了出色的 API,并支持 Expo 应用程序开发,无需弹出。您可以通过 Carousel 组件的layout
属性轻松更改幻灯片在轮播框架中滑入和滑出时的外观,截至 3.6 版本,您甚至可以创建自定义插值!
虽然您不仅限于使用此软件包显示图像,但我们将构建一个仅显示图像的轮播,以及一个标题,以保持配方简单。我们将使用优秀的免费许可照片网站unsplash.com通过托管在source.unsplash.com的 Unsplash Source 项目获取用于在我们的轮播中显示的随机图像。Unsplash Source 允许您轻松地从 Unsplash 请求随机图像,而无需访问官方 API。您可以访问 Unsplash Source 网站以获取有关其工作原理的更多信息。
准备工作
我们需要为这个配方创建一个新的应用程序。让我们把这个应用叫做“轮播”。
如何做...
- 我们将从打开
App.js
并导入依赖项开始:
import React, { Component } from 'react';
import {
SafeAreaView,
StyleSheet,
Text,
View,
Image,
TouchableOpacity,
Picker,
Dimensions,
} from 'react-native';
import Carousel from 'react-native-snap-carousel';
- 接下来,让我们定义
App
类和初始state
对象。state
有三个属性:一个布尔值,用于指示我们当前是否正在显示轮播图,一个layoutType
属性,用于设置我们轮播图的布局样式,以及一个我们稍后将用于从 Unsplash Source 获取图像的imageSearchTerms
数组。请随意更改imageSearchTerms
数组:
export default class App extends React.Component {
state = {
showCarousel: false,
layoutType: 'default',
imageSearchTerms: [
'Books',
'Code',
'Nature',
'Cats',
]
}
// Defined in following steps
}
- 接下来,让我们定义
render
方法。我们只需检查this.state.showCorousel
的值,然后相应地显示轮播图或控件:
render() {
return (
<SafeAreaView style={styles.container}>
{this.state.showCarousel ?
this.renderCarousel() :
this.renderControls()
}
</SafeAreaView>
);
}
- 接下来,让我们创建
renderControls
函数。这将是用户在首次打开应用程序时看到的布局,包括用于在轮播图中选择布局类型的 React NativePicker
和用于打开轮播图的按钮。Picker
有三个可用选项:默认、tinder 和 stack:
renderControls = () => {
return(
<View style={styles.container}>
<Picker
selectedValue={this.state.layoutType}
style={styles.picker}
onValueChange={this.updateLayoutType}
>
<Picker.Item label="Default" value="default" />
<Picker.Item label="Tinder" value="tinder" />
<Picker.Item label="Stack" value="stack" />
</Picker>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.openButton}
>
<Text style={styles.openButtonText}>Open Carousel</Text>
</TouchableOpacity>
</View>
)
}
- 让我们定义
toggleCarousel
函数。该函数只是将state
上的showCarousel
的值设置为其相反值。通过定义一个切换函数,我们可以使用相同的函数来打开和关闭轮播图:
toggleCarousel = () => {
this.setState({
showCarousel: !this.state.showCarousel
});
}
- 类似地,
updateLayoutType
方法只是更新state
上的layoutType
到从Picker
组件传入的layoutType
值:
updateLayoutType = (layoutType) => {
this.setState({
layoutType
});
}
renderCarousel
函数返回轮播图的标记。它由一个用于关闭轮播图的按钮和Carousel
组件本身组成。该组件接受一个layout
属性,由Picker
设置。它还有一个data
属性,用于接收应该循环播放每个轮播幻灯片的数据,以及一个renderItem
回调函数,用于处理每个单独幻灯片的渲染:
renderCarousel = () => {
return(
<View style={styles.carouselContainer}>
<View style={styles.closeButtonContainer}>
<TouchableOpacity
onPress={this.toggleCarousel}
style={styles.button}
>
<Text style={styles.label}>x</Text>
</TouchableOpacity>
</View>
<Carousel
layout={this.state.layoutType}
data={this.state.imageSearchTerms}
renderItem={this.renderItem}
sliderWidth={350}
itemWidth={350}
>
</Carousel>
</View>
);
}
- 我们仍然需要处理每个幻灯片的渲染的函数。该函数接收一个对象参数,其中包含传递给
data
属性的数组中的下一个项目。我们将返回一个使用item
参数值从 Unsplash Source 获取350x350
大小的随机项目的Image
组件。我们还将添加一个Text
元素来显示正在显示的图像类型:
renderItem = ({item}) => {
return (
<View style={styles.slide}>
<Image
style={styles.image}
source={{ uri: `https://source.unsplash.com/350x350/?
${item}`}}
/>
<Text style={styles.label}>{item}</Text>
</View>
);
}
- 我们需要的最后一件事是一些样式来布局我们的 UI。
container
样式适用于主要包装SafeAreaView
元素,因此我们将justifyContent
设置为'space-evenly'
,以便Picker
和TouchableOpacity
组件填满屏幕。为了在屏幕右上角显示关闭按钮,我们将flexDirection: 'row
和justifyContent: 'flex-end'
应用于包装元素。其余的样式只是尺寸、颜色、填充、边距和字体大小:
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'column',
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'space-evenly',
},
carouselContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#474747'
},
closeButtonContainer: {
width: 350,
flexDirection: 'row',
justifyContent: 'flex-end'
},
slide: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
image: {
width:350,
height: 350,
},
label: {
fontSize: 30,
padding: 40,
color: '#fff',
backgroundColor: '#474747'
},
openButton: {
padding: 10,
backgroundColor: '#000'
},
openButtonText: {
fontSize: 20,
padding: 20,
color: '#fff',
},
closeButton: {
padding: 10
},
picker: {
height: 150,
width: 100,
backgroundColor: '#fff'
}
});
- 我们已经完成了我们的轮播应用程序。它可能不会赢得任何设计奖,但它是一个具有流畅、本地感觉行为的工作轮播应用程序:
它是如何工作的...
在步骤4 中,我们定义了renderControls
函数,该函数在应用程序首次启动时呈现 UI。这是我们第一次使用Picker
组件的示例。它是 React Native 核心库的一部分,并提供下拉类型选择器,用于在许多应用程序中选择选项。selectedValue
属性是与选择器中当前选定项目绑定的值。通过将其设置为this.state.layoutType
,我们将默认选择为“默认”布局,并在选择不同的Picker
项目时保持值同步。选择器中的每个项目都由Picker.Item
组件表示。其label
属性定义了项目的显示文本,value
属性表示项目的字符串值。由于我们将onValueChange
属性与updateLayoutType
函数一起使用,每当选择新项目时都会调用它,从而相应地更新this.state.layoutType
。
在步骤7 中,我们定义了轮播图的 JSX。轮播图的data
和renderItem
属性是必需的,并且一起工作以呈现轮播图中的每个幻灯片。当实例化轮播图时,传递到data
属性的数组将被循环处理,并且renderItem
回调函数将针对区域中的每个项目调用,该项目作为参数传递到renderItem
中。我们还设置了sliderWidth
和itemWidth
属性,这些属性对于水平轮播图是必需的。
在步骤 8中,我们定义了renderItem
函数,该函数对传递到data
中的数组中的每个条目调用。我们将返回的Image
组件的源设置为 Unsplash 源 URL,该 URL 将返回所请求类型的随机图像。
还有更多...
有一些事情我们可以做来改进这个配方。我们可以利用Image.prefetch()
方法在打开轮播图之前下载第一张图片,这样图片就可以立即准备好,或者添加一个输入框,允许用户选择自己的图片搜索词。
react-native-snap-carousel 包为 React Native 应用程序提供了一个很好的构建多媒体轮播图的方式。我们在这里没有时间涵盖的一些功能包括视差图片和自定义分页。对于有冒险精神的开发人员,该包提供了一种创建自定义插值的方式,使您可以创建超出三种内置布局之外的自定义布局。
将推送通知添加到您的应用程序
推送通知是提供应用程序和用户之间持续反馈循环的一种很好的方式,不断提供与用户相关的应用程序特定数据。消息应用程序在有新消息到达时发送通知。提醒应用程序显示通知以提醒用户在特定时间或位置执行任务。播客应用程序可以使用通知通知用户新的一集已经发布。购物应用程序可以使用通知提醒用户查看限时优惠。
推送通知是增加用户互动和留存的一种有效方式。如果您的应用程序使用与时间敏感或基于事件的数据,推送通知可能是一项有价值的资产。在这个配方中,我们将使用 Expo 的推送通知实现,它简化了一些在原生 React Native 项目中所需的设置。如果您的应用程序需要非 Expo 项目,我建议考虑使用 react-native-push-notification 包 github.com/zo0r/react-native-push-notification
。
在这个配方中,我们将制作一个非常简单的消息应用程序,并添加推送通知。我们将请求适当的权限,然后将推送通知令牌注册到我们将构建的 Express 服务器上。我们还将渲染一个TextInput
,让用户输入消息。当用户按下发送按钮时,消息将被发送到我们的服务器,服务器将通过 Expo 的推送通知服务器向所有已在我们的 Express 服务器上注册令牌的设备发送来自应用程序的消息的推送通知。
由于 Expo 内置的推送通知服务,为每个本机设备创建通知的复杂工作被转移到了 Expo 托管的后端。我们在这个教程中构建的 Express 服务器只会将每个推送通知的 JSON 对象传递给 Expo 后端,其余工作都会被处理。Expo 文档中的以下图表(docs.expo.io/versions/latest/guides/push-notifications
)说明了推送通知的生命周期:
图片来源:
docs.expo.io/versions/latest/guides/push-notifications/
虽然使用 Expo 实现推送通知比起其他方式少了一些设置工作,但技术的要求仍然意味着我们需要运行一个服务器来处理注册和发送通知,这意味着这个教程会比大多数教程长一些。让我们开始吧!
准备工作
在这个应用程序中,我们需要做的第一件事是请求设备允许使用推送通知。不幸的是,推送通知权限在模拟器中无法正常工作,因此需要一个真实设备来测试这个应用程序。
我们还需要能够从本地主机之外的地址访问推送通知服务器。在真实环境中,推送通知服务器已经有一个公共 URL,但在开发环境中,最简单的解决方案是创建一个隧道,将开发推送通知服务器暴露给互联网。为此目的,我们将使用 ngrok 工具,因为它是一个成熟、强大且非常易于使用的解决方案。您可以在ngrok.com
了解更多关于该软件的信息。
首先,使用以下命令通过npm
全局安装ngrok
:
npm i -g ngrok
安装完成后,您可以通过使用ngrok
和https
参数在互联网和本地机器上的端口之间创建隧道:
ngrok https [port-to-expose]
我们将在本教程中稍后使用这个命令来暴露开发服务器。
让我们为这个教程创建一个新的应用程序。我们将其命名为push-notifications
。我们将需要三个额外的 npm 包来完成这个教程:express
用于推送通知服务器,esm
用于在服务器上使用 ES6 语法支持,expo-server-sdk
用于处理推送通知。使用yarn
安装它们:
yarn add express esm expo-server-sdk
或者,使用npm
安装它们:
npm install express esm expo-server-sdk --save
如何做...
- 让我们从构建
App
开始。我们将通过向App.js
添加我们需要的依赖项来开始:
import React from 'react';
import {
StyleSheet,
Text,
View,
TextInput,
TouchableOpacity
} from 'react-native';
import { Permissions, Notifications } from 'expo';
- 我们将在服务器上声明两个 API 端点的常量,但是
url
将在教程后面运行服务器时由ngrok
生成,因此我们将在那时更新这些常量的值:
const PUSH_REGISTRATION_ENDPOINT = 'http://generated-ngrok-url/token';
const MESSAGE_ENPOINT = 'http://generated-ngrok-url/message';
- 让我们创建
App
组件并初始化state
对象。我们需要一个notification
属性来保存Notifications
侦听器接收到的通知,我们将在后面的步骤中定义:
export default class App extends React.Component {
state = {
notification: null,
messageText: ''
}
// Defined in following steps
}
- 让我们定义一个方法来处理将推送通知令牌注册到服务器。我们将通过
Permissions
组件上的askAsync
方法向用户请求通知权限。如果获得了权限,就从Notifications
组件的getExpoPushTokenAsync
方法获取设备上的令牌:
registerForPushNotificationsAsync = async () => {
const { status } = await Permissions.askAsync(Permissions.NOTIFICATIONS);
if (status !== 'granted') {
return;
}
let token = await Notifications.getExpoPushTokenAsync();
// Defined in following steps
}
- 一旦我们获得了适当的令牌,我们将将其发送到推送通知服务器进行注册。然后,我们将向
PUSH_REGISTRATION_ENDPOINT
发出POST
请求,发送token
对象和user
对象到请求体中。我已经在用户对象中硬编码了值,但在真实应用中,这将是您为当前用户存储的元数据:
registerForPushNotificationsAsync = async () => {
// Defined in above step
fetch(PUSH_REGISTRATION_ENDPOINT, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
token: {
value: token,
},
user: {
username: 'warly',
name: 'Dan Ward'
},
}),
});
// Defined in next step
}
- 注册令牌后,我们将设置一个事件侦听器来监听应用程序在打开和前台运行时发生的任何通知。在某些情况下,我们需要手动处理来自传入推送通知的信息显示。查看本教程末尾的工作原理...部分,了解为什么需要这样做以及如何利用它。我们将在下一步中定义处理程序:
registerForPushNotificationsAsync = async () => {
// Defined in above steps
this.notificationSubscription =
Notifications.addListener(this.handleNotification);
}
- 每当收到新通知时,
handleNotification
方法将被运行。我们将只是将传递给此回调的新通知存储在state
对象中,以便稍后在render
函数中使用:
handleNotification = (notification) => {
this.setState({ notification });
}
- 我们希望我们的应用程序在启动时请求使用推送通知的权限,并注册推送通知令牌。我们将利用
componentDidMount
生命周期钩子来运行我们的registerForPushNotificationsAsync
方法:
componentDidMount() {
this.registerForPushNotificationsAsync();
}
- UI 将非常简单,以保持教程简单。它由一个用于消息文本的
TextInput
,一个用于发送消息的发送按钮,以及一个用于显示通知的View
组成:
render() {
return (
<View style={styles.container}>
<TextInput
value={this.state.messageText}
onChangeText={this.handleChangeText}
style={styles.textInput}
/>
<TouchableOpacity
style={styles.button}
onPress={this.sendMessage}
>
<Text style={styles.buttonText}>Send</Text>
</TouchableOpacity>
{this.state.notification ?
this.renderNotification()
: null}
</View>
);
}
- 在上一步中定义的
TextInput
组件缺少其onChangeText
属性所需的方法。让我们接下来创建这个方法。它只是将用户输入的文本保存到this.state.messageText
中,以便可以被value
属性和其他地方使用。
handleChangeText = (text) => {
this.setState({ messageText: text });
}
TouchableOpacity
组件的onPress
属性调用sendMessage
方法,在用户按下按钮时发送消息文本。在这个函数中,我们将获取消息文本并将其POST
到我们推送通知服务器上的MESSAGE_ENDPOINT
。服务器将处理后续操作。消息发送后,我们将清除state
中的messageText
属性。
sendMessage = async () => {
fetch(MESSAGE_ENPOINT, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: this.state.messageText,
}),
});
this.setState({ messageText: '' });
}
App
所需的最后一部分是样式。这些样式很简单,现在应该都很熟悉了。
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#474747',
alignItems: 'center',
justifyContent: 'center',
},
textInput: {
height: 50,
width: 300,
borderColor: '#f6f6f6',
borderWidth: 1,
backgroundColor: '#fff',
padding: 10
},
button: {
padding: 10
},
buttonText: {
fontSize: 18,
color: '#fff'
},
label: {
fontSize: 18
}
});
- React Native 应用程序部分完成后,让我们继续进行服务器部分。首先,在项目的根目录中创建一个新的
server
文件夹,并在其中创建一个index.js
文件。让我们首先导入express
来运行服务器,以及expo-server-sdk
来处理注册和发送推送通知。我们将创建一个 Express 服务器应用并将其存储在app
常量中,以及一个 Expo 服务器 SDK 的新实例存储在expo
常量中。我们还将添加一个savedPushTokens
数组来存储在 React Native 应用中注册的任何令牌,以及一个PORT_NUMBER
常量来指定服务器要运行的端口号。
import express from 'express';
import Expo from 'expo-server-sdk';
const app = express();
const expo = new Expo();
let savedPushTokens = [];
const PORT_NUMBER = 3000;
- 我们的服务器需要公开两个端点(一个用于注册令牌,一个用于接受来自 React Native 应用的消息),因此我们将创建两个函数,当命中这些路由时将执行这些函数。首先我们将定义
saveToken
函数。它只是获取一个令牌,检查它是否存储在savedPushTokens
数组中,如果尚未存储,则将其推送到数组中。
const saveToken = (token) => {
if (savedPushTokens.indexOf(token === -1)) {
savedPushTokens.push(token);
}
}
- 我们服务器需要的另一个函数是在接收来自 React Native 应用的消息时发送推送通知的处理程序。我们将遍历所有保存在
savedPushTokens
数组中的令牌,并为每个令牌创建一个消息对象。每个消息对象的标题为收到消息!
,这将以粗体显示在推送通知中,消息文本作为通知的正文。
const handlePushTokens = (message) => {
let notifications = [];
for (let pushToken of savedPushTokens) {
if (!Expo.isExpoPushToken(pushToken)) {
console.error(`Push token ${pushToken} is not a valid Expo push token`);
continue;
}
notifications.push({
to: pushToken,
sound: 'default',
title: 'Message received!',
body: message,
data: { message }
})
}
// Defined in following step
}
- 一旦我们有了消息数组,我们可以将它们发送到 Expo 的服务器,然后 Expo 的服务器将把推送通知发送到所有注册设备。我们将通过 expo 服务器的
chunkPushNotifications
和sendPushNotificationsAsync
方法发送消息数组,并根据情况将成功收据或错误记录到服务器控制台上。关于这个工作原理的更多信息,请参阅本教程末尾的工作原理...部分:
const handlePushTokens = (message) => {
// Defined in previous step
let chunks = expo.chunkPushNotifications(notifications);
(async () => {
for (let chunk of chunks) {
try {
let receipts = await expo.sendPushNotificationsAsync(chunk);
console.log(receipts);
} catch (error) {
console.error(error);
}
}
})();
}
- 现在我们已经定义了处理推送通知和消息的函数,让我们通过创建 API 端点来公开这些函数。如果您对 Express 不熟悉,它是一个在 Node 中运行 Web 服务器的强大且易于使用的框架。您可以通过基本路由文档快速了解基本路由的基础知识:
expressjs.com/en/starter/basic-routing.html
。
我们将使用 JSON 数据,因此第一步将是使用express.json()
调用应用 JSON 解析器中间件:
app.use(express.json());
- 尽管我们实际上不会使用服务器的根路径(
/
),但定义一个是个好习惯。我们将只回复一条消息,表示服务器正在运行:
app.get('/', (req, res) => {
res.send('Push Notification Server Running');
});
- 首先,让我们实现保存推送通知令牌的端点。当向
/token
端点发送POST
请求时,我们将把令牌值传递给saveToken
函数,并返回一个声明已收到令牌的响应:
app.post('/token', (req, res) => {
saveToken(req.body.token.value);
console.log(`Received push token, ${req.body.token.value}`);
res.send(`Received push token, ${req.body.token.value}`);
});
- 同样,
/message
端点将从请求体中获取message
并将其传递给handlePushTokens
函数进行处理。然后,我们将发送一个响应,表示已收到消息:
app.post('/message', (req, res) => {
handlePushTokens(req.body.message);
console.log(`Received message, ${req.body.message}`);
res.send(`Received message, ${req.body.message}`);
});
- 服务器的最后一部分是对服务器实例调用 Express 的
listen
方法,这将启动服务器:
app.listen(PORT_NUMBER, () => {
console.log('Server Online on Port ${PORT_NUMBER}');
});
- 我们需要一种启动服务器的方法,因此我们将在
package.json
文件中添加一个名为 serve 的自定义脚本。打开package.json
文件并更新它,使其具有一个新的serve
脚本的 scripts 对象。添加了这个之后,我们可以通过yarn run serve
命令或npm run serve
命令使用 yarn 运行服务器或使用 npm 运行服务器。package.json
文件应该看起来像这样:
{
"main": "node_modules/expo/AppEntry.js",
"private": true,
"dependencies": {
"esm": "³.0.28",
"expo": "²⁷.0.1",
"expo-server-sdk": "².3.3",
"express": "⁴.16.3",
"react": "16.3.1",
"react-native": "https://github.com/expo/react-native/archive/sdk-27.0.0.tar.gz"
},
"scripts": {
"serve": "node -r esm server/index.js"
}
}
- 我们已经把所有的代码放在了一起,让我们来使用它吧!如前所述,推送通知权限在模拟器上无法正常工作,因此需要一个真实设备来测试推送通知功能。首先,我们将通过运行以下命令来启动我们新创建的服务器:
yarn run serve
npm run serve
应该会看到我们在步骤 21中定义的listen
方法调用中定义的Server Online
消息:
- 接下来,我们需要运行
ngrok
来将我们的服务器暴露到互联网上。打开一个新的终端窗口,并使用以下命令创建一个ngrok
隧道:
ngrok http 3000
您应该在终端中看到ngrok
界面。这显示了ngrok
生成的 URL。在这种情况下,ngrok
正在将我的位于http://localhost:3000
的服务器转发到 URLhttp://ddf558bd.ngrok.io
。让我们复制该 URL:
- 您可以通过在浏览器中访问生成的 URL 来测试服务器是否正在运行并且可以从互联网访问。直接导航到此 URL 的行为与导航到
http://localhost:3000
完全相同,这意味着我们在上一步中定义的GET
端点应该运行。该函数返回Push Notification Server Running
字符串,并应在浏览器中显示:
- 现在我们已经确认服务器正在运行,让我们更新 React Native 应用程序以使用正确的服务器 URL。在步骤 2中,我们添加了常量来保存我们的 API 端点,但是我们还没有正确的 URL。让我们更新这些 URL 以反映
ngrok
生成的隧道 URL:
const PUSH_REGISTRATION_ENDPOINT = 'http://ddf558bd.ngrok.io/token';
const MESSAGE_ENPOINT = 'http://ddf558bd.ngrok.io/message';
- 如前所述,您需要在真实设备上运行此应用程序,以便权限请求能够正确工作。一旦您打开应用程序,设备应该会提示您是否要允许该应用程序发送通知:
- 一旦选择了“允许”,推送通知令牌将被发送到服务器的
/token
端点以进行保存。这也应该在服务器终端中打印出相关的console.log
语句与保存的令牌。在这种情况下,我的 iPhone 的推送令牌是字符串。
ExponentPushToken[g5sIEbOm2yFdzn5VdSSy9n]
:
-
此时,如果您有第二个 Android 或 iOS 设备,请继续在该设备上打开 React Native 应用程序。如果没有,不用担心。还有另一种简单的方法可以测试我们的推送通知功能是否正常工作,而无需使用第二个设备。
-
您可以使用 React Native 应用程序的文本输入向其他注册设备发送消息。如果您有第二个已向服务器注册令牌的设备,它应该会收到与新发送的消息相对应的推送通知。您还应该在服务器上看到两个新的
console.log
实例:一个显示接收到的消息,另一个显示从 Expo 服务器返回的receipts
数组。数组中的每个 receipt 对象都将具有一个status
属性,如果操作成功,则该属性的值为'ok'
:
- 如果您没有第二个设备进行测试,可以使用 Expo 的推送通知工具,托管在
expo.io/dashboard/notifications
。只需从服务器终端复制push token
并将其粘贴到标有 EXPO PUSH TOKEN(来自您的应用程序)的输入中。要模拟从我们的 React Native 应用程序发送的消息,请将 MESSAGE TITLE 设置为Message received!
,将 MESSAGE BODY 设置为您想要发送的消息文本,并选中 Play Sound 复选框。如果愿意,还可以通过提供具有"message"
键和您的消息文本值的 JSON 对象来模拟data
对象,例如{ "message": "This is a test message." }
。然后接收到的消息应该看起来像这个屏幕截图:
它是如何工作的...
我们在这里构建的配方有点牵强,但请求权限、注册令牌、接受应用程序数据以及响应应用程序数据发送推送通知所需的核心概念都在这里。
在步骤 4中,我们定义了registerForPushNotificationsAsync
函数的第一部分。我们首先通过Permissions.askAsync
方法询问用户是否允许我们通过Permissions.NOTIFICATIONS
常量发送通知。然后我们保存了解析后的return
对象的status
属性,如果用户授予权限,则该属性的值将为'granted'
。如果我们没有获得权限,我们将立即return
;否则,我们通过调用getExpoPushTokenAsync
从 Expo 的Notifications
组件中获取令牌。此函数返回一个令牌字符串,格式如下:
ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]
在步骤 5中,我们定义了对服务器注册端点(/token
)的POST
调用。此函数将令牌发送到请求正文中,然后使用在步骤 14中定义的saveToken
函数在服务器上保存。
在步骤 6中,我们创建了一个事件监听器,用于监听任何新的推送通知。这是通过调用Notifications.addListener
并传入一个回调函数来实现的,每次接收到新通知时都会执行该函数。在 iOS 设备上,系统设计为仅在发送推送通知的应用程序未打开并处于前台时才产生推送通知。这意味着如果您尝试在用户当前使用您的应用程序时发送推送通知,他们将永远不会收到。
为了解决这个问题,Expo 建议手动在应用程序内显示推送通知数据。Notifications.addListener
方法就是为了满足这个需求而创建的。当接收到推送通知时,传递给addListener
的回调将被执行,并将新的通知对象作为参数接收。在步骤 7中,我们将此通知保存到state
中,以便相应地重新渲染 UI。在本教程中,我们只在Text
组件中显示了消息文本,但您也可以使用模态框进行更类似通知的呈现。
在步骤 11中,我们创建了sendMessage
函数,该函数将存储在state
中的消息文本发布到服务器的/message
端点。这将执行在步骤 15中定义的handlePushToken
服务器函数。
在步骤 13中,我们开始在服务器上使用 Express 和 Expo 服务器 SDK。通过直接调用express()
创建一个新的服务器,通常按惯例将其命名为app
。我们能够使用new Expo()
创建一个新的 Expo 服务器 SDK 实例,并将其存储在expo
常量中。稍后我们使用 Expo 服务器 SDK 使用expo
发送推送通知,在步骤 17到步骤 20中使用app
定义路由,并在步骤 22中通过调用app.listen()
启动服务器。
在步骤 14中,我们定义了saveToken
函数,当 React Native 应用程序使用/token
端点注册令牌时将执行该函数。此函数将传入的令牌保存到savedPushTokens
数组中,以便稍后在用户发送消息时使用。在真实的应用程序中,这通常是您希望将令牌保存到持久性数据库(如 SQL、MongoDB 或 Firebase 数据库)的地方。
在步骤 15中,我们开始定义handlePushTokens
函数,当 React Native 应用程序使用/message
端点时运行。该函数循环处理savedPushTokens
数组。使用 Expo 服务器 SDK 的isExpoPushToken
方法检查每个令牌的有效性,该方法接受一个令牌并返回true
如果令牌有效。如果无效,我们将在服务器控制台上记录错误。如果有效,我们将在下一步的批处理中将新的通知对象推送到本地notifications
数组中。每个通知对象都需要一个to
属性,其值设置为有效的 Expo 推送令牌。所有其他属性都是可选的。我们设置的可选属性如下:
-
声音:可以默认播放默认通知声音,或者对于无声音为
null
-
标题:推送通知的标题,通常以粗体显示
-
正文:推送通知的正文
-
数据:自定义数据 JSON 对象
在步骤 16中,我们使用 Expo 服务器 SDK 的chunkPushNotifications
实例方法创建了一个优化发送到 Expo 推送通知服务器的数据块数组。然后我们循环遍历这些块,并通过expo.sendPushNotificationsAsync
方法将每个块发送到 Expo 的推送通知服务器。它返回一个解析为每个推送通知的收据数组的 promise。如果过程成功,数组中将有一个{ status: 'ok' }
对象。
这个端点的行为比真实服务器可能要简单,因为大多数消息应用程序处理消息的方式可能更复杂。至少,可能会有一个接收者列表,指定注册设备将接收特定推送通知。逻辑被故意保持简单,以描绘基本流程。
在步骤 18中,我们在服务器上定义了第一个可访问的路由,即根(/
)路径。Express 提供了get
和post
辅助方法,用于轻松地创建GET
和POST
请求的 API 端点。回调函数接收请求对象和响应对象作为参数。所有服务器 URL 都需要响应请求;否则,请求将超时。响应通过响应对象上的send
方法发送。这个路由不处理任何数据,所以我们只返回指示我们的服务器正在运行的字符串。
在步骤 19和步骤 20中,我们为/token
和/message
定义了POST
端点,分别执行saveToken
和handlePushTokens
。我们还在每个端点中添加了console.log
语句,以便在服务器终端上记录令牌和消息,便于开发。
在步骤 21中,我们在 Express 服务器上定义了listen
方法,启动了服务器。第一个参数是要监听请求的端口号,第二个参数是回调函数,通常用于在服务器终端上console.log
一条消息,表示服务器已启动。
在步骤 22中,我们在项目的package.json
文件中添加了一个自定义脚本。可以通过在package.json
文件中添加一个scripts
键,将可以在终端中运行的任何命令设置为自定义 npm 脚本,其键是自定义脚本的名称,值是运行该自定义脚本时应执行的命令。在这个示例中,我们定义了一个名为serve
的自定义脚本,运行node -r esm server/index.js
命令。这个命令使用我们在本示例开始时安装的esm
npm 包在 Node 中运行我们的服务器文件(server/index.js
)。自定义脚本可以使用npm
执行:
npm run [custom-script-name]
也可以使用yarn
执行:
yarn run [custom-script-name]
还有更多...
推送通知可能会很复杂,但幸运的是,Expo 以多种方式简化了这个过程。Expo 的推送通知服务有很好的文档,涵盖了通知定时、其他语言中的 Expo 服务器 SDK 以及如何通过 HTTP/2 实现通知的具体内容。我鼓励你在docs.expo.io/versions/latest/guides/push-notifications
上阅读更多。
实现基于浏览器的身份验证
在第八章的使用 Facebook 登录示例中,我们将介绍使用 Expo 的Facebook
组件创建登录工作流程,以提供用户的基本 Facebook 账户信息给我们的应用程序。Expo 还提供了一个Google
组件,用于获取用户的 Google 账户信息的类似功能。但是,如果我们想要创建一个使用来自不同网站的账户信息的登录工作流程,我们该怎么办呢?在这种情况下,Expo 提供了AuthSession
组件。
AuthSession
是建立在 Expo 的 WebBrowser
组件之上的,我们在第四章 实现复杂用户界面 - 第二部分 中已经使用过。典型的登录流程包括四个步骤:
-
用户启动登录流程
-
网页浏览器打开到登录页面
-
认证提供程序在成功登录时提供重定向
-
React Native 应用程序处理重定向
在这个应用程序中,我们将使用 Spotify API 通过用户登录来获取我们应用程序的 Spotify 账户信息。前往 beta.developer.spotify.com/dashboard/applications
创建一个新的 Spotify 开发者账户(如果你还没有),以及一个新的应用。应用可以取任何你喜欢的名字。创建完应用后,你会在应用信息中看到一个客户端 ID 字符串。在构建 React Native 应用程序时,我们将需要这个 ID。
准备就绪
我们需要一个新的应用程序来完成这个教程。让我们将应用命名为 browser-based-auth
。
重定向 URI 也需要在之前创建的 Spotify 应用中列入白名单。重定向应该是 https://auth.expo.io/@YOUR_EXPO_USERNAME/YOUR_APP_SLUG
的形式。由于我的 Expo 用户名是 warlyware
,并且由于我们正在构建的这个 React Native 应用程序名为 browser-based-auth
,我的重定向 URI 是 https://auth.expo.io/@warlyware/browser-based-auth
。请确保将其添加到 Spotify 应用的设置中的重定向 URI 列表中。
如何做...
- 我们将从打开
App.js
并导入我们将要使用的依赖项开始。
import React, { Component } from 'react';
import { TouchableOpacity, StyleSheet, Text, View } from 'react-native';
import { AuthSession } from 'expo';
import { FontAwesome } from '@expo/vector-icons';
- 让我们也声明
CLIENT_ID
为一个常量,以便以后使用。复制之前创建的 Spotify 应用的客户端 ID,以便我们可以将其保存在CLIENT_ID
常量中:
const CLIENT_ID = Your-Spotify-App-Client-ID;
- 让我们创建
App
类和初始state
。userInfo
属性将保存我们从 Spotify API 收到的用户信息,didError
是一个布尔值,用于跟踪登录过程中是否发生错误:
export default class App extends React.Component {
state = {
userInfo: null,
didError: false
};
// Defined in following steps
}
- 接下来,让我们定义将用户登录到 Spotify 的方法。
AuthSession
组件的getRedirectUrl
方法提供了在登录后返回到 React Native 应用程序所需的重定向 URL,这是我们在本示例的准备就绪部分中保存在 Spotify 应用程序中的相同重定向 URI。 然后,我们将在登录请求中使用重定向 URL,我们将使用AuthSession.startAsync
方法启动登录请求,传入一个选项对象,其中authUrl
属性设置为用于授权用户数据的 Spotify 端点。 有关此 URL 的更多信息,请参阅本示例末尾的它是如何工作...部分:
handleSpotifyLogin = async () => {
let redirectUrl = AuthSession.getRedirectUrl();
let results = await AuthSession.startAsync({
authUrl:
`https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}
&redirect_uri=${encodeURIComponent(redirectUrl)}
&scope=user-read-email&response_type=token`
});
// Defined in next step
};
- 我们将点击 Spotify 端点以进行用户身份验证的结果保存在本地
results
变量中。 如果结果对象上的type
属性返回的不是'success'
,那么就会发生错误,因此我们将相应地更新state
的didError
属性。 否则,我们将使用从授权接收到的访问令牌点击/me
端点以获取用户信息,然后将其保存到this.state.userInfo
中:
handleSpotifyLogin = async () => {
if (results.type !== 'success') {
this.setState({ didError: true });
} else {
const userInfo = await axios.get(`https://api.spotify.com/v1/me`, {
headers: {
"Authorization": `Bearer ${results.params.access_token}`
}
});
this.setState({ userInfo: userInfo.data });
}
};
- 现在
auth
相关的方法已经定义,让我们创建render
函数。 我们将使用FontAwesome
Expo 图标库来显示 Spotify 标志,添加一个按钮允许用户登录,并添加渲染错误或用户信息的方法,具体取决于this.state.didError
的值。 一旦在state
的userInfo
属性上保存了数据,我们还将禁用登录按钮:
render() {
return (
<View style={styles.container}>
<FontAwesome
name="spotify"
color="#2FD566"
size={128}
/>
<TouchableOpacity
style={styles.button}
onPress={this.handleSpotifyLogin}
disabled={this.state.userInfo ? true : false}
>
<Text style={styles.buttonText}>
Login with Spotify
</Text>
</TouchableOpacity>
{this.state.didError ?
this.displayError() :
this.displayResults()
}
</View>
);
}
- 接下来,让我们定义处理错误的 JSX。 模板只是显示一个通用的错误消息,表示用户应该再试一次:
displayError = () => {
return (
<View style={styles.userInfo}>
<Text style={styles.errorText}>
There was an error, please try again.
</Text>
</View>
);
}
displayResults
函数将是一个View
组件,如果state
中保存了userInfo
,则显示用户的图像,用户名和电子邮件地址,否则它将提示用户登录:
displayResults = () => {
{ return this.state.userInfo ? (
<View style={styles.userInfo}>
<Image
style={styles.profileImage}
source={ {'uri': this.state.userInfo.images[0].url} }
/>
<View>
<Text style={styles.userInfoText}>
Username:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.id}
</Text>
<Text style={styles.userInfoText}>
Email:
</Text>
<Text style={styles.userInfoText}>
{this.state.userInfo.email}
</Text>
</View>
</View>
) : (
<View style={styles.userInfo}>
<Text style={styles.userInfoText}>
Login to Spotify to see user data.
</Text>
</View>
)}
}
- 这个示例的样式非常简单。 它使用了列式弹性布局,应用了 Spotify 的黑色和绿色配色方案,并添加了字体大小和边距:
const styles = StyleSheet.create({
container: {
flexDirection: 'column',
backgroundColor: '#000',
flex: 1,
alignItems: 'center',
justifyContent: 'space-evenly',
},
button: {
backgroundColor: '#2FD566',
padding: 20
},
buttonText: {
color: '#000',
fontSize: 20
},
userInfo: {
height: 250,
width: 200,
alignItems: 'center',
},
userInfoText: {
color: '#fff',
fontSize: 18
},
errorText: {
color: '#fff',
fontSize: 18
},
profileImage: {
height: 64,
width: 64,
marginBottom: 32
}
});
- 现在,如果我们查看应用程序,我们应该能够登录到 Spotify,并看到与用于登录的帐户关联的图像,用户名和电子邮件地址:
它是如何工作的...
在步骤 4中,我们创建了处理 Spotify 登录过程的方法。AuthSession.startAsync
方法只需要一个authUrl
,这是由 Spotify 开发者文档提供的。所需的四个部分是Client-ID
,用于处理来自 Spotify 的响应的重定向 URI,指示应用程序请求的用户信息范围的scope
参数,以及response_type
参数为token
。我们只需要用户的基本信息,因此我们请求了user-read-email
的范围类型。有关所有可用范围的信息,请查看beta.developer.spotify.com/documentation/general/guides/scopes/
上的文档。
在步骤 5中,我们完成了 Spotify 登录处理程序。如果登录不成功,我们相应地更新了state
上的didError
。如果成功,我们使用该响应访问 Spotify API 端点以获取用户数据(api.spotify.com/v1/me
)。我们根据 Spotify 的文档,使用Bearer ${results.params.access_token}
定义了GET
请求的Authorization
标头来验证请求。在此请求成功后,我们将返回的用户数据存储在userInfo
state
对象中,这将重新呈现 UI 并显示用户信息。
深入了解 Spotify 的认证过程,您可以在beta.developer.spotify.com/documentation/general/guides/authorization-guide/
找到指南。
另请参阅
-
Expo
MapView
文档:docs.expo.io/versions/latest/sdk/map-view
-
Airbnb 的 React Native Maps 包:
github.com/react-community/react-native-maps
-
Expo 音频文档:
docs.expo.io/versions/latest/sdk/audio
-
React Native Image Prefetch 文档:
facebook.github.io/react-native/docs/image.html#prefetch
-
React Native Snap Carousel 自定义插值文档:
github.com/archriss/react-native-snap-carousel/blob/master/doc/CUSTOM_INTERPOLATIONS.md
-
Expo 推送通知文档:
docs.expo.io/versions/latest/guides/push-notifications
-
Express 基本路由指南:
expressjs.com/en/starter/basic-routing.html
-
esm 软件包:
github.com/standard-things/esm
-
用于 Node 的 Expo 服务器 SDK:
github.com/expo/exponent-server-sdk-node
-
ngrok 软件包:
github.com/inconshreveable/ngrok
第六章:向您的应用程序添加基本动画
在本章中,我们将涵盖以下教程:
-
创建简单动画
-
运行多个动画
-
创建动画通知
-
展开和折叠容器
-
创建带有加载动画的按钮
介绍
为了提供良好的用户体验,我们可能希望添加一些动画来引导用户的注意力,突出特定的操作,或者只是为我们的应用程序增添独特的风格。
正在进行一个倡议,将所有处理从 JavaScript 移至本地端。在撰写本文时(React Native 版本 0.58),我们可以选择使用本地驱动程序在本地世界中运行所有这些计算。不幸的是,这不能用于所有动画,特别是与布局相关的动画,比如 flexbox 属性。在文档中阅读有关使用本地动画时的注意事项的更多信息facebook.github.io/react-native/docs/animations#caveats
。
本章中的所有教程都使用 JavaScript 实现。React Native 团队承诺在将所有处理移至本地端时使用相同的 API,因此我们不需要担心现有 API 的变化。
创建简单动画
在这个教程中,我们将学习动画的基础知识。我们将使用一张图片来创建一个简单的线性移动,从屏幕的右侧移动到左侧。
准备工作
为了完成这个教程,我们需要创建一个空的应用程序。让我们称之为simple-animation
。
我们将使用一个云的 PNG 图像来制作这个教程。您可以在 GitHub 上托管的教程存储库中找到该图像github.com/warlyware/react-native-cookbook/tree/master/chapter-6/simple-animation/assets/images
。将图像放在/assets/images
文件夹中以供应用程序使用。
如何做...
- 让我们从打开
App.js
并导入App
类的依赖项开始。Animated
类将负责创建动画的值。它提供了一些准备好可以进行动画处理的组件,还提供了几种方法和辅助程序来运行平滑的动画。
Easing
类提供了几种辅助方法,用于计算运动(如linear
和quadratic
)和预定义动画(如bounce
、ease
和elastic
)。我们将使用Dimensions
类来获取当前设备尺寸,以便在动画初始化时知道在哪里放置元素:
import React, { Component } from 'react';
import {
Animated,
Easing,
Dimensions,
StyleSheet,
View,
} from 'react-native';
- 我们还将初始化一些我们在应用程序中需要的常量。在这种情况下,我们将获取设备尺寸,设置图像的大小,并
require
我们将要进行动画处理的图像:
const { width, height } = Dimensions.get('window');
const cloudImage = require('./assets/images/cloud.png');
const imageHeight = 200;
const imageWidth = 300;
- 现在,让我们创建
App
组件。我们将使用组件生命周期系统中的两种方法。如果您对这个概念不熟悉,请查看相关的 React 文档(reactjs.cn/react/docs/component-specs.html
)。这个页面还有一个关于生命周期钩子如何工作的非常好的教程:
export default class App extends Component {
componentWillMount() {
// Defined on step 4
}
componentDidMount() {
// Defined on step 7
}
startAnimation () {
// Defined on step 5
}
render() {
// Defined on step 6
}
}
const styles = StyleSheet.create({
// Defined on step 8
});
- 为了创建动画,我们需要定义一个标准值来驱动动画。
Animated.Value
是一个处理每一帧动画值的类。我们需要在组件创建时创建这个类的实例。在这种情况下,我们使用componentWillMount
方法,但我们也可以使用constructor
或者属性的默认值:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
- 一旦我们创建了动画值,我们就可以定义动画。我们还通过将
Animated.timing
的start
方法传递给一个箭头函数来创建一个循环,该箭头函数再次执行startAnimation
函数。现在,当图像达到动画的末尾时,我们将再次开始相同的动画,以创建一个无限循环的动画:
startAnimation() {
this.animatedValue.setValue(width);
Animated.timing(
this.animatedValue,
{
toValue: -imageWidth,
duration: 6000,
easing: Easing.linear,
useNativeDriver: true,
}
).start(() => this.startAnimation());
}
- 我们已经完成了动画,但目前只是计算了每一帧的值,没有对这些值做任何操作。下一步是在屏幕上渲染图像,并设置我们想要动画的样式属性。在这种情况下,我们想要在x轴上移动元素;因此,我们应该更新
left
属性:
render() {
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.image,
{ left: this.animatedValue },
]}
source={cloudImage}
/>
</View>
);
}
- 如果我们刷新模拟器,我们将看到图像在屏幕上,但它还没有被动画处理。为了解决这个问题,我们需要调用
startAnimation
方法。我们将在组件完全渲染后开始动画,使用componentDidMount
生命周期钩子:
componentDidMount() {
this.startAnimation();
}
- 如果我们再次运行应用程序,我们将看到图像在屏幕顶部移动,就像我们想要的那样!作为最后一步,让我们为应用程序添加一些基本样式:
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
image: {
height: imageHeight,
position: 'absolute',
top: height / 3,
width: imageWidth,
},
});
输出如下所示:
工作原理...
在步骤 5中,我们设置了动画数值。第一行每次调用此方法时都会重置初始值。在本例中,初始值将是设备的宽度
,这将把图像移动到屏幕的右侧,这是我们想要开始动画的地方。
然后,我们使用Animated.timing
函数基于时间创建动画,并传入两个参数。对于第一个参数,我们传入了在步骤 4中的componentWillMount
生命周期钩子中创建的animatedValue
。第二个参数是一个包含动画配置的对象。在这种情况下,我们将把结束值设置为图像宽度的负值,这将把图像放在屏幕的左侧。动画在那里完成。
配置完毕后,Animated
类将计算所需的所有帧,以在分配的 6 秒内执行从右向左的线性动画(通过将duration
属性设置为6000
毫秒)。
React Native 还提供了另一个与Animated
配对使用的辅助工具,称为Easing
。在这种情况下,我们使用Easing
辅助类的linear
属性。Easing
提供其他常见的缓动方法,如elastic
和bounce
。查看Easing
类文档,并尝试为easing
属性设置不同的值,看看每个值的效果。您可以在facebook.github.io/react-native/docs/easing.html
找到文档。
动画配置正确后,我们需要运行它。我们通过调用start
方法来实现这一点。此方法接收一个可选的callback
函数参数,当动画完成时将执行该函数。在这种情况下,我们递归运行相同的startAnimation
函数。这将创建一个无限循环,这正是我们想要实现的。
在步骤 6中,我们正在渲染图像。如果要对图像进行动画处理,应始终使用Animate.Image
组件。在内部,此组件将处理动画的值,并将为本机组件上的每个帧设置每个值。这避免了在每个帧上在 JavaScript 层上运行渲染方法,从而实现更流畅的动画。
除了Image
之外,我们还可以对View
、Text
和ScrollView
组件进行动画处理。这四个组件都有内置的支持,但我们也可以创建一个新组件,并通过Animated.createAnimatedComponent()
添加动画支持。这四个组件都能处理样式更改。我们所要做的就是将animatedValue
传递给我们想要动画的属性,这种情况下是left
属性,但我们也可以在每个组件上使用任何可用的样式。
运行多个动画
在这个配方中,我们将学习如何在几个元素中使用相同的动画值。这样,我们可以重复使用相同的值,以及插值,为其余的元素获得不同的值。
这个动画将类似于上一个配方。这次,我们将有两朵云:一朵较小,移动较慢,另一朵较大,移动较快。在屏幕中央,我们将有一架静止的飞机。我们不会给飞机添加任何动画,但移动的云会使它看起来像飞机在移动。
准备就绪
让我们通过创建一个名为multiple-animations
的空应用程序来开始这个配方。
我们将使用三种不同的图像:两个云和一架飞机。您可以从 GitHub 上的配方存储库下载图像,地址为github.com/warlyware/react-native-cookbook/tree/master/chapter-6/multiple-animations/assets/images
。确保将图像放在/assets/images
文件夹中。
如何做...
- 让我们从打开
App.js
并添加我们的导入开始:
import React, { Component } from 'react';
import {
View,
Animated,
Image,
Easing,
Dimensions,
StyleSheet,
} from 'react-native';
- 此外,我们需要定义一些常量,并要求我们将用于动画的图像。请注意,我们将在这个配方中将相同的云图像视为
cloudImage1
和cloudImage2
,但我们将把它们视为单独的实体:
const { width, height } = Dimensions.get('window');
const cloudImage1 = require('./assets/images/cloud.png');
const cloudImage2 = require('./assets/images/cloud.png');
const planeImage = require('./assets/images/plane.gif');
const cloudHeight = 100;
const cloudWidth = 150;
const planeHeight = 60;
const planeWidth = 100;
- 在下一步中,当组件被创建时,我们将创建
animatedValue
实例,然后在组件完全渲染时开始动画。我们正在创建一个在无限循环中运行的动画。初始值将为1
,最终值将为0
。如果您对这段代码不清楚,请确保阅读本章的第一个配方:
export default class App extends Component {
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startAnimation();
}
startAnimation () {
this.animatedValue.setValue(1);
Animated.timing(
this.animatedValue,
{
toValue: 0,
duration: 6000,
easing: Easing.linear,
}
).start(() => this.startAnimation());
}
render() {
// Defined in a later step
}
}
const styles = StyleSheet.create({
// Defined in a later step
});
- 在本示例中,
render
方法将与上一个示例有很大不同。在本示例中,我们将使用相同的animatedValue
来动画两个图像。动画值将返回从1
到0
的值;但是,我们希望将云从右向左移动,因此我们需要为每个元素设置left
值。
为了设置正确的值,我们需要对animatedValue
进行插值。对于较小的云,我们将把初始的left
值设为设备的宽度,但对于较大的云,我们将把初始的left
值设得远离设备的右边缘。这将使移动距离更大,因此移动速度会更快:
render() {
const left1 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth, width],
});
const left2 = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-cloudWidth*5, width + cloudWidth*5],
});
// Defined in a later step
}
- 一旦我们有了正确的
left
值,我们需要定义我们想要动画的元素。在这里,我们将把插值值设置为left
样式属性:
render() {
// Defined in a later step
return (
<View style={styles.background}>
<Animated.Image
style={[
styles.cloud1,
{ left: left1 },
]}
source={cloudImage1}
/>
<Image
style={styles.plane}
source={planeImage}
/>
<Animated.Image
style={[
styles.cloud2,
{ left: left2 },
]}
source={cloudImage2}
/>
</View>
);
}
- 至于最后一步,我们需要定义一些样式,只需设置每朵云的
width
和height
以及为top
分配样式即可。
const styles = StyleSheet.create({
background: {
flex: 1,
backgroundColor: 'cyan',
},
cloud1: {
position: 'absolute',
width: cloudWidth,
height: cloudHeight,
top: height / 3 - cloudWidth / 2,
},
cloud2: {
position: 'absolute',
width: cloudWidth * 1.5,
height: cloudHeight * 1.5,
top: height/2,
},
plane: {
position: 'absolute',
height: planeHeight,
width: planeWidth,
top: height / 2 - planeHeight,
left: width / 2 - planeWidth,
}
});
- 如果我们刷新应用,我们应该能看到动画:
工作原理...
在步骤 4中,我们定义了插值以获取每朵云的left
值。interpolate
方法接收一个具有两个必需配置的对象,inputRange
和outputRange
。
inputRange
配置接收一个值数组。这些值应始终是升序值;您也可以使用负值,只要值是升序的。
outputRange
应该与inputRange
中定义的值的数量匹配。这些是我们需要作为插值结果的值。
对于本示例,inputRange
从0
到1
,这些是我们的animatedValue
的值。在outputRange
中,我们定义了我们需要的移动的限制。
创建动画通知
在本示例中,我们将从头开始创建一个通知组件。在显示通知时,组件将从屏幕顶部滑入。几秒钟后,我们将自动隐藏它,将其滑出。
准备工作
我们将创建一个应用。让我们称之为notification-animation
。
如何做...
- 我们将从
App
组件开始工作。首先,让我们导入所有必需的依赖项:
import React, { Component } from 'react';
import {
Text,
TouchableOpacity,
StyleSheet,
View,
SafeAreaView,
} from 'react-native';
import Notification from './Notification';
- 一旦我们导入了所有依赖项,我们就可以定义
App
类。在这种情况下,我们将使用notify
属性等于false
来初始化state
。我们将使用此属性来显示或隐藏通知。默认情况下,通知不会显示在屏幕上。为了简化事情,我们将在state
中定义message
属性,其中包含我们想要显示的文本:
export default class App extends Component {
state = {
notify: false,
message: 'This is a notification!',
};
toggleNotification = () => {
// Defined on later step
}
render() {
// Defined on later step
}
}
const styles = StyleSheet.create({
// Defined on later step
});
- 在
render
方法内,我们需要仅在notify
属性为true
时显示通知。我们可以通过使用if
语句来实现这一点:
render() {
const notify = this.state.notify
? <Notification
autoHide
message={this.state.message}
onClose={this.toggleNotification}
/>
: null;
// Defined on next step
}
- 在上一步中,我们只定义了对
Notification
组件的引用,但还没有使用它。让我们定义一个return
,其中包含此应用程序所需的所有 JSX。为了保持简单,我们只会定义一个工具栏、一些文本和一个按钮,以在按下时切换通知的状态:
render() {
// Code from previous step
return (
<SafeAreaView>
<Text style={styles.toolbar}>Main toolbar</Text>
<View style={styles.content}>
<Text>
Lorem ipsum dolor sit amet, consectetur adipiscing
elit,
sed do eiusmod tempor incididunt ut labore et
dolore magna.
</Text>
<TouchableOpacity
onPress={this.toggleNotification}
style={styles.btn}
>
<Text style={styles.text}>Show notification</Text>
</TouchableOpacity>
<Text>
Sed ut perspiciatis unde omnis iste natus error sit
accusantium doloremque laudantium.
</Text>
{notify}
</View>
</SafeAreaView>
);
}
- 我们还需要定义一个方法,用于在
state
上切换notify
属性,这非常简单:
toggleNotification = () => {
this.setState({
notify: !this.state.notify,
});
}
- 我们几乎完成了这个类。剩下的只有样式。在这种情况下,我们只会添加基本样式,如
color
、padding
、fontSize
、backgroundColor
和margin
,没有什么特别的:
const styles = StyleSheet.create({
toolbar: {
backgroundColor: '#8e44ad',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
overflow: 'hidden',
},
btn: {
margin: 10,
backgroundColor: '#9b59b6',
borderRadius: 3,
padding: 10,
},
text: {
textAlign: 'center',
color: '#fff',
},
});
- 如果我们尝试运行应用程序,我们会看到一个错误,即无法解析
./Notification
模块。让我们通过定义Notification
组件来解决这个问题。让我们创建一个Notifications
文件夹,其中包含一个index.js
文件。然后,我们可以导入我们的依赖项:
import React, { Componen } from 'react';
import {
Animated,
Easing,
StyleSheet,
Text,
} from 'react-native';
- 一旦我们导入了依赖项,让我们定义新组件的 props 和初始状态。我们将定义一些非常简单的东西,只是一个用于接收要显示的消息的属性,以及两个
callback
函数,允许在通知出现在屏幕上和关闭时运行一些操作。我们还将添加一个属性来设置在自动隐藏通知之前显示通知的毫秒数:
export default class Notification extends Component {
static defaultProps = {
delay: 5000,
onClose: () => {},
onOpen: () => {},
};
state = {
height: -1000,
};
}
- 终于是时候开始处理动画了!我们需要在组件被渲染时立即开始动画。如果以下代码中有什么不清楚的地方,我建议你看一下本章的第一和第二个示例:
componentWillMount() {
this.animatedValue = new Animated.Value();
}
componentDidMount() {
this.startSlideIn();
}
getAnimation(value, autoHide) {
const { delay } = this.props;
return Animated.timing(
this.animatedValue,
{
toValue: value,
duration: 500,
easing: Easing.cubic,
delay: autoHide ? delay : 0,
}
);
}
- 到目前为止,我们已经定义了一个获取动画的方法。对于滑入运动,我们需要计算从
0
到1
的值。动画完成后,我们需要运行onOpen
回调。如果autoHide
属性在调用onOpen
方法时设置为true
,我们将自动运行滑出动画以删除组件:
startSlideIn () {
const { onOpen, autoHide } = this.props;
this.animatedValue.setValue(0);
this.getAnimation(1)
.start(() => {
onOpen();
if (autoHide){
this.startSlideOut();
}
});
}
- 与前面的步骤类似,我们需要一个用于滑出运动的方法。在这里,我们需要计算从
1
到0
的值。我们将autoHide
值作为参数发送到getAnimation
方法。这将自动延迟动画,延迟时间由delay
属性定义(在我们的例子中为 5 秒)。动画完成后,我们需要运行onClose
回调函数,这将从App
类中删除组件:
startSlideOut() {
const { autoHide, onClose } = this.props;
this.animatedValue.setValue(1);
this.getAnimation(0, autoHide)
.start(() => onClose());
}
- 最后,让我们添加
render
方法。在这里,我们将获取props
提供的message
值。我们还需要组件的height
来将组件移动到动画的初始位置;默认情况下是-1000
,但我们将在下一步在运行时设置正确的值。animatedValue
从0
到1
或从1
到0
,取决于通知是打开还是关闭;因此,我们需要对其进行插值以获得实际值。动画将从组件的负高度到0
;这将导致一个漂亮的滑入/滑出动画:
render() {
const { message } = this.props;
const { height } = this.state;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [-height, 0],
});
// Defined on next step
}
}
- 为了尽可能简单,我们将返回一个带有一些文本的
Animated.View
。在这里,我们正在使用插值结果设置top
样式,这意味着我们将对顶部样式进行动画处理。如前所述,我们需要在运行时计算组件的高度。为了实现这一点,我们需要使用视图的onLayout
属性。此函数将在每次布局更新时调用,并将新的组件尺寸作为参数发送:
render() {
// Code from previous step
return (
<Animated.View
onLayout={this.onLayoutChange}
style={[
styles.main,
{ top }
]}
>
<Text style={styles.text}>{message}</Text>
</Animated.View>
);
}
}
onLayoutChange
方法将非常简单。我们只需要获取新的height
并更新state
。此方法接收一个event
。从这个对象中,我们可以获取有用的信息。对于我们的目的,我们将在event
对象的nativeEvent.layout
中访问数据。layout
对象包含屏幕的width
和height
,以及Animated.View
调用此函数时屏幕上的x和y位置:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
this.setState({ height });
}
- 在最后一步,我们将为通知组件添加一些样式。由于我们希望该组件在任何其他内容之上进行动画,我们需要将
position
设置为absolute
,并将left
和right
属性设置为0
。我们还将添加一些颜色和填充:
const styles = StyleSheet.create({
main: {
backgroundColor: 'rgba(0, 0, 0, 0.7)',
padding: 10,
position: 'absolute',
left: 0,
right: 0,
},
text: {
color: '#fff',
},
});
- 最终应用程序应该看起来像以下截图:
工作原理...
在步骤 3中,我们定义了Notification
组件。该组件接收三个参数:一个标志,用于在几秒后自动隐藏组件,我们要显示的消息,以及在通知关闭时将执行的callback
函数。
当onClose
回调被执行时,我们将切换notify
属性以移除Notification
实例并清除内存。
在步骤 4中,我们定义了用于渲染应用程序组件的 JSX。重要的是要在其他组件之后渲染Notification
组件,以便该组件显示在所有其他组件之上。
在步骤 6中,我们定义了组件的state
。defaultProps
对象为每个属性设置了默认值。如果给定属性没有赋值,这些值将被应用。
我们将每个callback
的默认值定义为空函数。这样,我们在尝试执行它们之前不必检查这些 props 是否有值。
对于初始的state
,我们定义了height
属性。实际的height
值将根据message
属性中接收的内容在运行时计算。这意味着我们需要最初将组件远离原始位置进行渲染。由于在计算布局时存在短暂延迟,我们不希望在移动到正确位置之前显示通知。
在步骤 9中,我们创建了动画。getAnimation
方法接收两个参数:要应用的delay
和autoHide
布尔值,用于确定通知是否自动关闭。我们在步骤 10和步骤 11中使用了这个方法。
在步骤 13中,我们为该组件定义了 JSX。onLayout
函数在更新布局时非常有用,可以获取组件的尺寸。例如,如果设备方向发生变化,尺寸将发生变化,这种情况下,我们希望更新动画的初始和最终坐标。
还有更多...
当前的实现效果相当不错,但是我们应该解决一个性能问题。目前,onLayout
方法在每一帧动画上都会被执行,这意味着我们在每一帧上都在更新state
,这导致组件在每一帧上重新渲染!我们应该避免这种情况,只更新一次以获得实际的高度。
为了解决这个问题,我们可以添加一个简单的验证,只有在当前值与初始值不同时才更新状态。这将避免在每一帧上更新state
,我们也不会一遍又一遍地强制渲染:
onLayoutChange = (event) => {
const {layout: { height } } = event.nativeEvent;
if (this.state.height === -1000) {
this.setState({ height });
}
}
虽然这对我们的目的有效,但我们也可以进一步确保在方向改变时height
也会更新。然而,我们会在这里停下,因为这个方法已经相当长了。
展开和折叠容器
在这个方法中,我们将创建一个带有title
和content
的自定义容器元素。当用户按下标题时,内容将折叠或展开。这个方法将允许我们探索LayoutAnimation
API。
做好准备
让我们从创建一个新的应用程序开始。我们将其称为collapsable-containers
。
一旦我们创建了应用程序,让我们还创建一个Panel
文件夹,里面有一个index.js
文件,用于存放我们的Panel
组件。
如何做...
- 让我们首先专注于
Panel
组件。首先,我们需要导入我们将在这个类中使用的所有依赖项:
import React, { Component } from 'react';
import {
View,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
} from 'react-native';
- 一旦我们有了依赖项,让我们声明
defaultProps
来初始化这个组件。在这个方法中,我们只需要将expanded
属性初始化为false
:
export default class Panel extends Component {
static defaultProps = {
expanded: false
};
}
const styles = StyleSheet.create({
// Defined on later step
});
- 我们将使用
state
对象上的height
属性来展开或折叠容器。这个组件第一次被创建时,我们需要检查expanded
属性,以设置正确的初始height
:
state = {
height: this.props.expanded ? null : 0,
};
- 让我们为这个组件渲染所需的 JSX 元素。我们需要从
state
中获取height
的值,并将其设置为内容的样式视图。当按下title
元素时,我们将执行toggle
方法(稍后定义)来改变state
的height
值:
render() {
const { children, style, title } = this.props;
const { height } = this.state;
return (
<View style={[styles.main, style]}>
<TouchableOpacity onPress={this.toggle}>
<Text style={styles.title}>
{title}
</Text>
</TouchableOpacity>
<View style={{ height }}>
{children}
</View>
</View>
);
}
- 如前所述,当按下
title
元素时,toggle
方法将被执行。在这里,我们将在state
上切换height
并在下一个渲染周期更新样式时调用我们想要使用的动画:
toggle = () => {
LayoutAnimation.spring();
this.setState({
height: this.state.height === null ? 0 : null,
})
}
- 为了完成这个组件,让我们添加一些简单的样式。我们需要将
overflow
设置为hidden
,否则在组件折叠时内容将被显示出来。
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
borderRadius: 3,
overflow: 'hidden',
paddingLeft: 30,
paddingRight: 30,
},
title: {
fontWeight: 'bold',
paddingTop: 15,
paddingBottom: 15,
}
- 一旦我们定义了
Panel
组件,让我们在App
类中使用它。首先,我们需要在App.js
中要求所有的依赖项:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Panel from './Panel';
- 在上一步中,我们导入了
Panel
组件。我们将在 JSX 中声明这个类的三个实例:
export default class App extends Component {
render() {
return (
<SafeAreaView style={[styles.main]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Panel
title={'Container 1'}
style={styles.panel}
>
<Text style={styles.panelText}>
Temporibus autem quibusdam et aut officiis
debitis aut rerum necessitatibus saepe
eveniet ut et voluptates repudiandae sint et
molestiae non recusandae.
</Text>
</Panel>
<Panel
title={'Container 2'}
style={styles.panel}
>
<Text style={styles.panelText}>
Et harum quidem rerum facilis est et expedita
distinctio. Nam libero tempore,
cum soluta nobis est eligendi optio cumque.
</Text>
</Panel>
<Panel
expanded
title={'Container 3'}
style={styles.panel}
>
<Text style={styles.panelText}>
Nullam lobortis eu lorem ut vulputate.
</Text>
<Text style={styles.panelText}>
Donec id elementum orci. Donec fringilla lobortis
ipsum, vitae commodo urna.
</Text>
</Panel>
</View>
</SafeAreaView>
);
}
}
- 在这个示例中,我们在 React Native 中使用了
LayoutAnimation
API。在当前版本的 React Native 中,这个 API 在 Android 上默认是禁用的。在App
组件挂载之前,我们将使用Platform
助手和UIManager
在 Android 设备上启用这个功能:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
- 最后,让我们为工具栏和主容器添加一些样式。我们只需要一些你现在可能已经习惯的简单样式:
padding
,margin
和color
。
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
},
panel: {
marginBottom: 10,
},
panelText: {
paddingBottom: 15,
}
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 3中,我们设置了内容的初始height
。如果expanded
属性设置为true
,那么我们应该显示内容。通过将height
值设置为null
,布局系统将根据内容计算height
;否则,我们需要将值设置为0
,这将在组件折叠时隐藏内容。
在步骤 4中,我们为Panel
组件定义了所有 JSX。这一步中有一些值得介绍的概念。首先,children
属性是从props
对象中传入的,当这个组件在App
类中使用时,它将包含在<Panel>
和</Panel>
之间定义的任何元素。这非常有帮助,因为通过使用这个属性,我们允许这个组件接收任何其他组件作为子组件。
在同一步骤中,我们还从state
对象中获取height
并将其设置为应用于可折叠内容的View
的style
。这将更新height
,导致组件相应地展开或折叠。我们还声明了onPress
回调,当按下title
元素时,它会切换state
上的height
。
在步骤 7中,我们定义了toggle
方法,它可以切换height
值。在这里,我们使用了LayoutAnimation
类。通过调用spring
方法,布局系统将在下一次渲染时对布局发生的每一次变化进行动画处理。在这种情况下,我们只改变了height
,但我们也可以改变任何其他属性,比如opacity
,position
或color
。
LayoutAnimation
类包含一些预定义的动画。在这个示例中,我们使用了spring
,但我们也可以使用linear
或easeInEaseOut
,或者使用configureNext
方法创建自己的动画。
如果我们移除LayoutAnimation
,我们将看不到动画;组件将通过从0
到总高度跳跃来展开和折叠。但通过添加那一行代码,我们可以轻松地添加一个漂亮、平滑的动画。如果您需要更多对动画的控制,您可能会想使用动画 API。
在步骤 9中,我们在Platform
助手上检查了 OS 属性,它返回了'android'
或'ios'
字符串,取决于应用程序运行在哪个设备上。如果应用程序在 Andriod 上运行,我们使用UIManager
助手的setLayoutAnimationEnabledExperimental
方法来启用LayoutAnimation
API。
另请参阅
-
LayoutAnimation
API 文档在facebook.github.io/react-native/docs/layoutanimation.html
-
在
codeburst.io/a-quick-intro-to-reacts-props-children-cb3d2fce4891
快速介绍 React 的props.children
。
创建带有加载动画的按钮
在这个示例中,我们将继续使用LayoutAnimation
类。在这里,我们将创建一个按钮,当用户按下按钮时,我们将显示一个加载指示器并动画化样式。
准备工作
要开始,我们需要创建一个空的应用程序。让我们称之为button-loading-animation
。
让我们还创建一个Button
文件夹,里面有一个index.js
文件,用于我们的Button
组件。
如何做...
- 让我们从
Button/index.js
文件开始。首先,我们将导入这个组件所需的所有依赖项:
import React, { Component } from 'react';
import {
ActivityIndicator,
LayoutAnimation,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native';
- 对于这个组件,我们将只使用四个 props:一个
label
,一个loading
布尔值,用于切换显示加载指示器或按钮内的标签,一个在按钮被按下时执行的回调函数,以及自定义样式。在这里,我们将init
默认的loading
为false
,并将handleButtonPress
设置为空函数:
export default class Button extends Component {
static defaultProps = {
loading: false,
onPress: () => {},
};
// Defined on later steps
}
- 我们将尽可能简化这个组件的
render
方法。我们将根据loading
属性的值来渲染标签和活动指示器:
render() {
const { loading, style } = this.props;
return (
<TouchableOpacity
style={[
styles.main,
style,
loading ? styles.loading : null,
]}
activeOpacity={0.6}
onPress={this.handleButtonPress}
>
<View>
{this.renderLabel()}
{this.renderActivityIndicator()}
</View>
</TouchableOpacity>
);
}
- 为了渲染
label
,我们需要检查loading
属性是否为false
。如果是,那么我们只返回一个带有从props
接收到的label
的Text
元素:
renderLabel() {
const { label, loading } = this.props;
if(!loading) {
return (
<Text style={styles.label}>{label}</Text>
);
}
}
- 同样,
renderActivityIndicator
指示器应该只在loading
属性的值为true
时应用。如果是这样,我们将返回ActivityIndicator
组件。我们将使用ActivityIndicator
的 props 来定义一个小的size
和白色的color
(#fff
):
renderActivityIndicator() {
if (this.props.loading) {
return (
<ActivityIndicator size="small" color="#fff" />
);
}
}
- 我们的类中还缺少一个方法:
handleButtonPress
。当按钮被按下时,我们需要通知这个组件的父组件,这可以通过调用通过props
传递给这个组件的onPress
回调来实现。我们还将使用LayoutAnimation
在下一次渲染时排队一个动画:
handleButtonPress = () => {
const { loading, onPress } = this.props;
LayoutAnimation.easeInEaseOut();
onPress(!loading);
}
- 为了完成这个组件,我们需要添加一些样式。我们将定义一些颜色,圆角,对齐,填充等。对于显示加载指示器时将应用的
loading
样式,我们将更新填充以创建一个围绕加载指示器的圆形:
const styles = StyleSheet.create({
main: {
backgroundColor: '#e67e22',
borderRadius: 20,
padding: 10,
paddingLeft: 50,
paddingRight: 50,
},
label: {
color: '#fff',
fontWeight: 'bold',
textAlign: 'center',
backgroundColor: 'transparent',
},
loading: {
padding: 10,
paddingLeft: 10,
paddingRight: 10,
},
});
- 我们已经完成了
Button
组件。现在,让我们来处理App
类。让我们首先导入所有的依赖项:
import React, { Component } from 'react';
import {
Text,
StyleSheet,
View,
SafeAreaView,
Platform,
UIManager
} from 'react-native';
import Button from './Button';
App
类相对简单。我们只需要在state
对象上定义一个loading
属性,它将切换Button
的动画。我们还将渲染一个toolbar
和一个Button
:
export default class App extends Component {
state = {
loading: false,
};
// Defined on next step
handleButtonPress = (loading) => {
this.setState({ loading });
}
render() {
const { loading } = this.state;
return (
<SafeAreaView style={[styles.main, android]}>
<Text style={styles.toolbar}>Animated containers</Text>
<View style={styles.content}>
<Button
label="Login"
loading={loading}
onPress={this.handleButtonPress}
/>
</View>
</SafeAreaView>
);
}
}
- 与上一个示例一样,我们需要在 Android 设备上手动启用
LayoutAnimation
API:
componentWillMount() {
if (Platform.OS === 'android') {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
}
- 最后,我们将添加一些
styles
,只是一些颜色,填充和居中对齐按钮在屏幕上:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#f39c12',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
backgroundColor: '#ecf0f1',
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 3中,我们为Button
组件添加了render
方法。在这里,我们接收了loading
属性,并根据该值将相应的样式应用于TouchableOpacity
按钮元素。我们还使用了两种方法:一种用于渲染标签,另一种用于渲染活动指示器。
在步骤 6中,我们执行了onPress
回调。默认情况下,我们声明了一个空函数,因此我们不必检查值是否存在。
这个按钮的父组件应该负责在调用onPress
回调时更新loading
属性。从这个组件中,我们只负责在按下此按钮时通知父组件。
LayoutAnimation.eadeInEaseOut
方法只是将动画排队到下一个渲染阶段,这意味着动画不会立即执行。我们负责更改我们想要动画的样式。如果我们不改变任何样式,那么我们就看不到任何动画。
Button
组件不知道loading
属性是如何更新的。这可能是因为获取请求、超时或任何其他操作。父组件负责更新loading
属性。无论发生任何变化,我们都会将新样式应用于按钮,并进行平滑的动画。
在步骤 9中,我们定义了App
类的内容。在这里,我们使用了我们的Button
组件。当按下按钮时,loading
属性的state
将被更新,这将导致每次按下按钮时动画运行。
结论
在本章中,我们已经介绍了如何为您的 React Native 应用程序添加动画的基础知识。这些示例旨在提供有用的实际代码解决方案,并建立如何使用基本构建块,以便您更好地创建适合您的应用程序的动画。希望到目前为止,您应该已经开始熟悉Animated
和LayoutAnimation
动画助手。在第七章中,为您的应用程序添加高级动画,我们将结合我们在这里学到的东西来构建更复杂和有趣的应用程序 UI 动画。
第七章:为您的应用程序添加高级动画
在本章中,我们将涵盖以下配方:
-
从列表组件中删除项目
-
创建 Facebook 反应小部件
-
在全屏显示图像
介绍
在上一章中,我们介绍了在 React Native 中使用两个主要动画助手Animated
和LayoutAnimation
的基础知识。在本章中,我们将通过构建更复杂的配方来进一步了解这些概念,展示常见的本地 UX 模式。
从列表组件中删除项目
在这个配方中,我们将学习如何在ListView
中创建带有动画侧向滑动的列表项。如果用户将项目滑动超过阈值,项目将被移除。这是许多具有可编辑列表的移动应用程序中的常见模式。我们还将看到如何使用PanResponder
来处理拖动事件。
准备就绪
我们需要创建一个空的应用程序。对于这个配方,我们将其命名为removing-list-items
。
我们还需要创建一个新的ContactList
文件夹,并在其中创建两个文件:index.js
和ContactItem.js
。
如何做...
- 让我们从导入主
App
类的依赖项开始,如下所示:
import React from 'react';
import {
Text,
StyleSheet,
SafeAreaView,
} from 'react-native';
import ContactList from './ContactList';
- 这个组件将很简单。我们只需要渲染一个
toolbar
和我们在上一步中导入的ContactList
组件,如下所示:
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Contacts</Text>
<ContactList style={styles.content} />
</SafeAreaView>
);
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#2c3e50',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
padding: 10,
flex: 1,
},
});
export default App;
- 这就是我们开始实际工作的全部内容。让我们打开
ContactList/index.js
文件,并导入所有依赖项,如下所示:
import React, { Component } from 'react';
import {
ListView,
ScrollView,
} from 'react-native';
import ContactItem from './ContactItem';
- 然后我们需要定义一些数据。在真实的应用程序中,我们会从 API 中获取数据,但为了保持简单并且只关注拖动功能,让我们在这个相同的文件中定义数据:
const data = [
{ id: 1, name: 'Jon Snow' },
{ id: 2, name: 'Luke Skywalker' },
{ id: 3, name: 'Bilbo Baggins' },
{ id: 4, name: 'Bob Labla' },
{ id: 5, name: 'Mr. Magoo' },
];
- 这个组件的
state
只包含两个属性:列表的数据和一个布尔值,在拖动开始或结束时将更新。如果您不熟悉ListView
的工作原理,请查看第二章中的显示项目列表配方,创建一个简单的 React Native 应用程序。让我们定义数据如下:
export default class ContactList extends Component {
ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
state = {
dataSource: this.ds.cloneWithRows(data),
swiping: false,
};
// Defined in later steps
}
render
方法只需要显示列表。在renderScrollComponent
属性中,我们将仅在用户不在列表上滑动项目时启用滚动。如果用户在滑动,我们希望禁用垂直滚动,如下所示:
render() {
const { dataSource, swiping } = this.state;
return (
<ListView
key={data}
enableEmptySections
dataSource={dataSource}
renderScrollComponent={
(props) => <ScrollView {...props} scrollEnabled={!swiping}/>
}
renderRow={this.renderItem}
/>
);
}
renderItem
方法将返回列表中的每个项目。在这里,我们需要将联系信息作为属性发送,以及三个回调函数:
renderItem = (contact) => (
<ContactItem
contact={contact}
onRemove={this.handleRemoveContact}
onDragEnd={this.handleToggleSwipe}
onDragStart={this.handleToggleSwipe}
/>
);
- 我们需要切换
state
对象上的 swiping 属性的值,这将切换列表上的垂直滚动是否被锁定:
handleToggleSwipe = () => {
this.setState({ swiping: !this.state.swiping });
}
- 在移除项目时,我们需要找到给定
contact
的index
,然后从原始列表中将其移除。之后,我们需要更新state
上的dataSource
,以使用生成的数据重新渲染列表:
handleRemoveContact = (contact) => {
const index = data.findIndex(
(item) => item.id === contact.id
);
data.splice(index, 1);
this.setState({
dataSource: this.ds.cloneWithRows(data),
});
}
- 列表已经完成,现在让我们专注于列表项。让我们打开
ContactList/ContactItem.js
文件,并导入我们需要的依赖项:
import React, { Component } from 'react';
import {
Animated,
Easing,
PanResponder,
StyleSheet,
Text,
TouchableHighlight,
View,
} from 'react-native';
- 我们需要为这个组件定义
defaultProps
。defaultProps
对象将需要为从父级ListView
元素传递给它的四个 props 中的每一个都提供一个空函数。当项目被按下时,onPress
函数将被执行,当联系人被移除时,onRemove
函数将被执行,而两个拖动函数将监听拖动事件。在state
上,我们只需要定义一个动画值来保存拖动的 x 和 y 坐标,如下所示:
export default class ContactItem extends Component {
static defaultProps = {
onPress: () => {},
onRemove: () => {},
onDragEnd: () => {},
onDragStart: () => {},
};
state = {
pan: new Animated.ValueXY(),
};
- 当组件被创建时,我们需要配置
PanResponder
。我们将在componentWillMount
生命周期钩子中进行这个操作。PanResponder
负责处理手势。它提供了一个简单的 API 来捕获用户手指生成的事件,如下所示:
componentWillMount() {
this.panResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: this.handleShouldDrag,
onPanResponderMove: Animated.event(
[null, { dx: this.state.pan.x }]
),
onPanResponderRelease: this.handleReleaseItem,
onPanResponderTerminate: this.handleReleaseItem,
});
}
- 现在让我们定义实际的函数,这些函数将在前一步中定义的每个回调中执行。我们可以从
handleShouldDrag
方法开始,如下所示:
handleShouldDrag = (e, gesture) => {
const { dx } = gesture;
return Math.abs(dx) > 2;
}
handleReleaseItem
有点复杂。我们将把这个方法分成两步。首先,我们需要弄清楚当前项目是否需要被移除。为了做到这一点,我们需要设置一个阈值。如果用户将元素滑动超出我们的阈值,我们将移除该项目,如下所示:
handleReleaseItem = (e, gesture) => {
const { onRemove, contact,onDragEnd } = this.props;
const move = this.rowWidth - Math.abs(gesture.dx);
let remove = false;
let config = { // Animation to origin position
toValue: { x: 0, y: 0 },
duration: 500,
};
if (move < this.threshold) {
remove = true;
if (gesture.dx > 0) {
config = { // Animation to the right
toValue: { x: this.rowWidth, y: 0 },
duration: 100,
};
} else {
config = { // Animation to the left
toValue: { x: -this.rowWidth, y: 0 },
duration: 100,
};
}
}
// Remainder in next step
}
- 一旦我们对动画进行了配置,我们就准备好移动项目了!首先,我们将执行
onDragEnd
回调,如果项目应该被移除,我们将运行onRemove
函数,如下所示:
handleReleaseItem = (e, gesture) => {
// Code from previous step
onDragEnd();
Animated.spring(
this.state.pan,
config,
).start(() => {
if (remove) {
onRemove(contact);
}
});
}
- 拖动系统已经完全就绪。现在我们需要定义
render
方法。我们只需要在TouchableHighlight
元素内显示联系人姓名,包裹在Animated.View
中,如下所示:
render() {
const { contact, onPress } = this.props;
return (
<View style={styles.row} onLayout={this.setThreshold}>
<Animated.View
style={[styles.pan, this.state.pan.getLayout()]}
{...this.panResponder.panHandlers}
>
<TouchableHighlight
style={styles.info}
onPress={() => onPress(contact)}
underlayColor="#ecf0f1"
>
<Text>{contact.name}</Text>
</TouchableHighlight>
</Animated.View>
</View>
);
}
- 我们需要在这个类上再添加一个方法,这个方法是通过
View
元素的onLayout
属性在布局改变时触发的。setThreshold
将获取row
的当前width
并设置threshold
。在这种情况下,我们将其设置为屏幕宽度的三分之一。这些值是必需的,以决定是否移除该项,如下所示:
setThreshold = (event) => {
const { layout: { width } } = event.nativeEvent;
this.threshold = width / 3;
this.rowWidth = width;
}
- 最后,我们将为行添加一些样式,如下所示:
const styles = StyleSheet.create({
row: {
backgroundColor: '#ecf0f1',
borderBottomWidth: 1,
borderColor: '#ecf0f1',
flexDirection: 'row',
},
pan: {
flex: 1,
},
info: {
backgroundColor: '#fff',
paddingBottom: 20,
paddingLeft: 10,
paddingTop: 20,
},
});
- 最终的应用程序应该看起来像这个屏幕截图:
它是如何工作的...
在步骤 5中,我们在state
上定义了swiping
属性。这个属性只是一个布尔值,当拖动开始时设置为true
,当完成时设置为false
。我们需要这个信息来锁定列表在拖动项目时的垂直滚动。
在步骤 7中,我们定义了列表中每行的内容。onDragStart
属性接收handleToggleSwipe
方法,当拖动开始时将执行该方法。当拖动完成时,我们也将执行相同的方法。
在同一步骤中,我们还将handleRemoveContact
方法发送给每个项目。顾名思义,当用户将其滑出时,我们将从列表中移除当前项目。
在步骤 11中,我们为项目组件定义了defaultProps
和state
。在过去的示例中,我们一直使用单个值来创建动画,但是在这种情况下,我们需要处理x和y坐标,所以我们需要一个Animated.ValueXY
的实例。在内部,这个类处理两个Animated.Value
实例,因此 API 几乎与我们之前看到的那些相同。
在步骤 12中,创建了PanResponder
。React Native 中的手势系统,就像浏览器中的事件系统一样,在触摸事件时处理手势分为两个阶段:捕获和冒泡。在我们的情况下,我们需要使用捕获阶段来确定当前事件是按压项目还是尝试拖动它。onMoveShouldSetPanResponderCapture
将捕获事件。然后,我们需要通过返回true
或false
来决定是否拖动该项。
onPanResponderMove
属性将在每一帧从动画中获取值,这些值将被应用于state
中的pan
对象。我们需要使用Animated.event
来访问每一帧的动画值。在这种情况下,我们只需要x
值。稍后,我们将使用这个值来运行不同的动画,将元素返回到其原始位置或将其从屏幕上移除。
当用户释放物品时,onPanResponderRelease
函数将被执行。如果由于任何其他原因,拖动被中断,将执行onPanResponderTerminate
。
在步骤 13中,我们需要检查当前事件是简单的按压还是拖动。我们可以通过检查x轴上的增量来做到这一点。如果触摸事件移动了超过两个像素,那么用户正在尝试拖动物品,否则,他们正在尝试按下按钮。我们将差异评估为绝对数,因为移动可能是从左到右或从右到左,我们希望适应这两种移动。
在步骤 14中,我们需要获取物品相对于设备宽度移动的距离。如果这个距离低于我们在setThreshold
中定义的阈值,那么我们需要移除这些物品。我们为每个动画定义了config
对象,否则将返回物品到原始位置。但是,如果我们需要移除物品,我们会检查方向并相应地设置配置。
在步骤 16中,我们定义了 JSX。我们在Animated.View
上设置我们想要动画的样式。在这种情况下,它是left
属性,但是我们可以从我们在state.pan
中存储的Animated.ValueXY
实例中调用getLayout
方法,而不是手动创建对象,该方法返回具有其现有值的 top 和 left 属性。
在同一步骤中,我们还通过展开this.panResponder.panHandlers
来为Animated.View
设置事件处理程序,使用展开运算符将我们在前面步骤中定义的拖动配置绑定到Animated.View
。
我们还定义了对props
中的onPress
回调的调用,传入当前的contact
信息。
另请参阅
您可以在以下网址找到PanResponder
API 文档:
facebook.github.io/react-native/docs/panresponder.html
创建一个 Facebook 反应小部件
在这个食谱中,我们将创建一个模拟 Facebook 反应小部件的组件。我们将有一个喜欢按钮图像,当按下时,将显示五个图标。图标行将使用交错的滑入动画,同时从0
增加到1
的不透明度。
准备工作
让我们创建一个名为facebook-widget
的空应用程序。
我们需要一些图片来显示一个假时间线。一些你的猫的照片就可以了,或者你可以使用 GitHub 上相应存储库中包含的猫的图片(github.com/warlyware/react-native-cookbook/tree/master/chapter-7/facebook-widget
)。我们还需要五个图标来显示五种反应,比如,生气、笑、心、惊讶,这些也可以在相应的存储库中找到。
首先,我们将在空应用程序中创建两个 JavaScript 文件:Reactions/index.js
和Reactions/Icon.js
。我们需要将猫的图片复制到应用程序根目录下的images/
文件夹中,反应图标应放置在Reactions/images
中。
如何做...
- 我们将在
App
类上创建一个假的 Facebook 时间线。让我们首先导入依赖项,如下所示:
import React from 'react';
import {
Dimensions,
Image,
Text,
ScrollView,
StyleSheet,
SafeAreaView,
} from 'react-native';
import Reactions from './Reactions';
- 我们需要导入一些图片来在我们的时间线中渲染。这一步中的 JSX 非常简单:只是一个
toolbar
,一个带有两个Image
和两个Reaction
组件的ScrollView
,如下所示:
const image1 = require('./images/01.jpg');
const image2 = require('./images/02.jpg');
const { width } = Dimensions.get('window');
const App = () => (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Reactions</Text>
<ScrollView style={styles.content}>
<Image source={image1} style={styles.image} resizeMode="cover" />
<Reactions />
<Image source={image2} style={styles.image} resizeMode="cover" />
<Reactions />
</ScrollView>
</SafeAreaView>
);
export default App;
- 我们需要为这个组件添加一些基本的样式,如下所示:
const styles = StyleSheet.create({
main: {
flex: 1,
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
flex: 1,
},
image: {
width,
height: 300,
},
});
- 我们准备开始工作在这个食谱的
Reactions
组件。让我们首先导入依赖项,如下所示。我们将在后续步骤中构建导入的Icon
组件:
import React, { Component } from 'react';
import {
Image,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
import Icon from './Icon';
- 让我们定义
defaultProps
和初始state
。我们还需要要求like
图标图片以在屏幕上显示它,如下所示:
const image = require('./images/like.png');
export default class Reactions extends Component {
static defaultProps = {
icons: [
'like', 'heart', 'angry', 'laughing', 'surprised',
],
};
state = {
show: false,
selected: '',
};
// Defined at later steps
}
- 让我们定义两种方法:一种是将
state
的选定值设置为选定的reaction
,另一种是切换state
的show
值以相应地显示或隐藏反应行,如下所示:
onSelectReaction = (reaction) => {
this.setState({
selected: reaction,
});
this.toggleReactions();
}
toggleReactions = () => {
this.setState({
show: !this.state.show,
});
};
- 我们将为此组件定义
render
方法。我们将显示一张图片,当按下时,将调用我们之前定义的toggleReactions
方法,如下所示:
render() {
const { style } = this.props;
const { selected } = this.state;
return (
<View style={[style, styles.container]}>
<TouchableOpacity onPress={this.toggleReactions}>
<Image source={image} style={styles.icon} />
</TouchableOpacity>
<Text>{selected}</Text>
{this.renderReactions()}
</View>
);
}
- 在这一步中,您会注意到我们正在调用
renderReactions
方法。接下来,我们将渲染用户按下主反应按钮时要显示的所有图标,如下所示:
renderReactions() {
const { icons } = this.props;
if (this.state.show) {
return (
<View style={styles.reactions}>
{ icons.map((name, index) => (
<Icon
key={index}
name={name}
delay={index * 100}
index={index}
onPress={this.onSelectReaction}
/>
))
}
</View>
);
}
}
- 我们需要为这个组件设置
styles
。我们将为反应图标图像设置大小并定义一些填充。reactions
容器的高度将为0
,因为图标将浮动,我们不希望添加任何额外的空间:
const styles = StyleSheet.create({
container: {
padding: 10,
},
icon: {
width: 30,
height: 30,
},
reactions: {
flexDirection: 'row',
height: 0,
},
});
Icon
组件目前缺失,所以如果我们尝试在这一点上运行我们的应用程序,它将失败。让我们通过打开Reactions/Icon.js
文件并添加组件的导入来构建这个组件,如下所示:
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Image,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
- 让我们定义我们将要使用的图标。我们将使用一个对象来存储图标,这样我们可以通过键名轻松检索到每个图像,如下所示:
const icons = {
angry: require('./images/angry.png'),
heart: require('./images/heart.png'),
laughing: require('./images/laughing.png'),
like: require('./images/like.png'),
surprised: require('./images/surprised.png'),
};
- 现在我们应该为这个组件定义
defaultProps
。我们不需要定义初始状态:
export default class Icon extends Component {
static defaultProps = {
delay: 0,
onPress: () => {},
};
}
- 图标应该通过动画出现在屏幕上,所以当组件挂载时,我们需要创建并运行动画,如下所示:
componentWillMount() {
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
const { delay } = this.props;
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 200,
easing: Easing.elastic(1),
delay,
}
).start();
}
- 当图标被按下时,我们需要执行
onPress
回调来通知父组件已选择了一个反应。我们将反应的名称作为参数发送,如下所示:
onPressIcon = () => {
const { onPress, name } = this.props;
onPress(name);
}
- 拼图的最后一块是
render
方法,我们将在这个组件中定义 JSX,如下所示:
render() {
const { name, index, onPress } = this.props;
const left = index * 50;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [10, -95],
});
const opacity = this.animatedValue;
return (
<Animated.View
style={[
styles.icon,
{ top, left, opacity },
]}
>
<TouchableOpacity onPress={this.onPressIcon}>
<Image source={icons[name]} style={styles.image} />
</TouchableOpacity>
</Animated.View>
);
}
- 作为最后一步,我们将为每个
icon
添加样式。我们需要图标浮动,所以我们将position
设置为absolute
,width
和height
设置为40
像素。在这个改变之后,我们应该能够运行我们的应用程序:
icon: {
position: 'absolute',
},
image: {
width: 40,
height: 40,
},
});
- 最终的应用程序应该看起来像这个屏幕截图:
它是如何工作的...
在 步骤 2 中,我们在时间线中定义了 Reactions
组件。现在,我们不专注于处理数据,而是专注于显示用户界面。因此,我们不会通过 Reactions
属性发送任何回调来获取所选值。
在 步骤 5 中,我们定义了 defaultProps
和初始 state
。
我们的状态中有两个属性:
-
show
属性是一个布尔值。我们用它来在用户按下主按钮时切换反应图标。当为false
时,我们隐藏反应,当为true
时,我们运行动画来显示每个图标。 -
selected
包含当前的选择。每当选择新的反应时,我们将更新这个属性。
在 步骤 8 中,我们渲染图标。在这里,我们需要将图标的名称发送到每个创建的实例。我们还为每个图标发送了 100 毫秒的 delay
,这将创建一个漂亮的交错动画。onPress
属性接收了 步骤 6 中定义的 onSelectReaction
方法,该方法在 state
上设置了所选的反应。
在步骤 13中,我们创建了动画。首先,我们使用Animated.Value
助手定义了animatedValue
变量,正如在之前的配方中提到的那样,这是负责在动画中每一帧中保存值的类。组件一旦挂载,我们就运行动画。动画的进度从0
到1
,持续时间为 200 毫秒,使用弹性缓动函数,并根据接收到的delay
属性延迟动画。
在步骤 15中,我们为Icon
组件定义了 JSX。在这里,我们对top
和opacity
属性进行动画处理。对于top
属性,我们需要从animatedValue
中插值出值,以便图标从其原始位置向上移动 95 像素。opacity
属性所需的值从0
到1
,由于我们不需要插值任何内容来完成这一点,因此我们可以直接使用animatedValue
。
left
值是根据index
计算的:我们只是将图标向前一个图标的左侧移动 50 像素,这样可以避免将图标全部渲染在同一个位置。
在全屏显示图像
在这个配方中,我们将创建一个图像时间轴。当用户按下任何图像时,它将在黑色背景下全屏显示图像。
我们将为背景使用不透明度动画,并将图像从其原始位置滑入。
准备工作
让我们创建一个名为photo-viewer
的空白应用程序。
此外,我们还将创建PostContainer/index.js
来显示时间轴中的每个图像,以及PhotoViewer/index.js
来在全屏显示所选图像。
您可以使用此处配方存储库中托管在 GitHub 上的图像(github.com/warlyware/react-native-cookbook/tree/master/chapter-7/photo-viewer
)中包含的图像,也可以使用自己的一些照片。将它们放在项目根目录中的images
文件夹中。
如何做...
- 我们将在
App
类中显示一个带有图像的时间轴。让我们导入所有依赖项,包括我们稍后将构建的另外两个组件,如下所示:
import React, { Component } from 'react';
import {
Dimensions,
Image,
Text,
ScrollView,
StyleSheet,
SafeAreaView,
} from 'react-native';
import PostContainer from './PostContainer';
import PhotoViewer from './PhotoViewer';
- 在这一步中,我们将定义要渲染的数据。这只是一个包含
title
和image
的对象数组。
const image1 = require('./images/01.jpg');
const image2 = require('./images/02.jpg');
const image3 = require('./images/03.jpg');
const image4 = require('./images/04.jpg');
const timeline = [
{ title: 'Enjoying the fireworks', image: image1 },
{ title: 'Climbing the Mount Fuji', image: image2 },
{ title: 'Check my last picture', image: image3 },
{ title: 'Sakuras are beautiful!', image: image4 },
];
- 现在我们需要声明此组件的初始
state
。当按下任何图像时,我们将更新selected
和position
属性,如下所示:
export default class App extends Component {
state = {
selected: null,
position: null,
};
// Defined in following steps
}
- 为了更新
state
,我们将声明两个方法:一个用于设置被按下的图像的值,另一个用于在查看器关闭时删除这些值:
showImage = (selected, position) => {
this.setState({
selected,
position,
});
}
closeViewer = () => {
this.setState({
selected: null,
position: null,
});
}
- 现在我们准备开始处理
render
方法。在这里,我们需要在ScrollView
中渲染每个图像,以便列表可以滚动,如下所示:
render() {
return (
<SafeAreaView style={styles.main}>
<Text style={styles.toolbar}>Timeline</Text>
<ScrollView style={styles.content}>
{
timeline.map((post, index) =>
<PostContainer key={index} post={post}
onPress={this.showImage} />
)
}
</ScrollView>
{this.renderViewer()}
</SafeAreaView>
);
}
- 在上一步中,我们调用了
renderViewer
方法。在这里,我们只会在状态中有一个帖子selected
时显示查看器组件。我们还会发送初始位置以开始动画和一个关闭查看器的回调,如下所示:
renderViewer() {
const { selected, position } = this.state;
if (selected) {
return (
<PhotoViewer
post={selected}
position={position}
onClose={this.closeViewer}
/>
);
}
}
- 这个组件的样式非常简单,只有一些颜色和填充,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#ecf0f1',
flex: 1,
},
toolbar: {
backgroundColor: '#2c3e50',
color: '#fff',
fontSize: 22,
padding: 20,
textAlign: 'center',
},
content: {
flex: 1,
},
});
- 时间轴已经完成,但是如果我们尝试运行我们的应用程序,它将失败。让我们开始处理
PostContainer
组件。我们将首先导入依赖项,如下所示:
import React, { Component } from 'react';
import {
Dimensions,
Image,
Text,
TouchableOpacity,
StyleSheet,
View,
} from 'react-native';
- 我们只需要两个
props
来定义这个组件。post
属性将接收图像数据,title
和image
,onPress
属性是一个回调,当图像被按下时我们将执行它,如下所示:
const { width } = Dimensions.get('window');
export default class PostContainer extends Component {
static defaultProps = {
onPress: ()=> {},
};
// Defined on following steps
}
- 这个组件将在
ScrollView
中。这意味着当用户开始滚动内容时,它的位置将会改变。当按下图像时,我们需要获取屏幕上的当前位置并将这些信息发送给父组件,如下所示:
onPressImage = (event) => {
const { onPress, post } = this.props;
this.refs.main.measure((fx, fy, width, height, pageX, pageY) => {
onPress(post, {
width,
height,
pageX,
pageY,
});
});
}
- 现在是时候为这个组件定义 JSX 了。为了保持简单,我们只会渲染
image
和title
:
render() {
const { post: { image, title } } = this.props;
return (
<View style={styles.main} ref="main">
<TouchableOpacity
onPress={this.onPressImage}
activeOpacity={0.9}
>
<Image
source={image}
style={styles.image}
resizeMode="cover"
/>
</TouchableOpacity>
<Text style={styles.title}>{title}</Text>
</View>
);
}
- 和往常一样,我们需要为这个组件定义一些样式。我们将添加一些颜色和填充,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#fff',
marginBottom: 30,
paddingBottom: 10,
},
content: {
flex: 1,
},
image: {
width,
height: 300,
},
title: {
margin: 10,
color: '#ccc',
}
});
- 如果现在运行应用程序,我们应该能够看到时间轴,但是如果我们按下任何图像,将会抛出错误。我们需要定义查看器,所以让我们打开
PhotoViewer/index.js
文件并导入依赖项:
import React, { Component } from 'react';
import {
Animated,
Dimensions,
Easing,
Text,
TouchableOpacity,
StyleSheet,
} from 'react-native';
- 让我们为这个组件定义
props
。为了将图像居中显示在屏幕上,我们需要知道当前设备的height
:
const { width, height } = Dimensions.get('window');
export default class PhotoViewer extends Component {
static defaultProps = {
onClose: () => {},
};
// Defined on following steps
}
- 当显示这个组件时,我们希望运行两个动画,因此我们需要在组件挂载后初始化并运行动画。动画很简单:它只是在
400
毫秒内从0
到1
进行一些缓动,如下所示:
componentWillMount() {
this.animatedValue = new Animated.Value(0);
}
componentDidMount() {
Animated.timing(
this.animatedValue,
{
toValue: 1,
duration: 400,
easing: Easing.in,
}
).start();
}
- 当用户按下关闭按钮时,我们需要执行
onClose
回调来通知父组件需要移除这个组件,如下所示:
onPressBtn = () => {
this.props.onClose();
}
- 我们将把
render
方法分为两步。首先,我们需要插入动画的值,如下所示:
render() {
const { post: { image, title }, position } = this.props;
const top = this.animatedValue.interpolate({
inputRange: [0, 1],
outputRange: [position.pageY, height/2 - position.height/2],
});
const opacity = this.animatedValue;
// Defined on next step
}
- 我们只需要定义三个元素:
Animated.View
来动画显示背景,Animated.Image
来显示图像,以及一个关闭按钮。我们将opacity
样式设置为主视图,这将使图像背景从透明变为黑色。图像将同时滑入,产生一个很好的效果:
// Defined on previous step
render() {
return (
<Animated.View
style={[
styles.main,
{ opacity },
]}
>
<Animated.Image
source={image}
style={[
styles.image,
{ top, opacity }
]}
/>
<TouchableOpacity style={styles.closeBtn}
onPress={this.onPressBtn}
>
<Text style={styles.closeBtnText}>X</Text>
</TouchableOpacity>
</Animated.View>
);
}
- 我们几乎完成了!这个食谱中的最后一步是定义样式。我们需要将主容器的位置设置为绝对位置,以便图像位于其他所有内容的顶部。我们还将关闭按钮移动到屏幕的右上角,如下所示:
const styles = StyleSheet.create({
main: {
backgroundColor: '#000',
bottom: 0,
left: 0,
position: 'absolute',
right: 0,
top: 0,
},
image: {
width,
height: 300,
},
closeBtn: {
position: 'absolute',
top: 50,
right: 20,
},
closeBtnText: {
fontSize: 20,
color: '#fff',
fontWeight: 'bold',
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
在步骤 4中,我们在state
中定义了两个属性:selected
和position
。selected
属性保存了按下图像的图像数据,可以是步骤 3中定义的timeline
对象中的任何一个。position
属性将保存屏幕上的当前y坐标,稍后用于将图像从其原始位置动画到屏幕中心。
在步骤 5中,我们对timeline
数组进行map
操作,以渲染每个post
。我们为每个 post 使用PostContainer
元素,发送post
信息,并使用onPress
回调来设置按下的图像。
在步骤 10中,我们需要图像的当前位置。为了实现这一点,我们使用所需信息的组件的measure
方法。该方法接收一个回调函数,并检索,除其他属性外,width
、height
和屏幕上的当前位置。
我们正在使用引用来访问在下一步的 JSX 中声明的组件。
在步骤 11中,我们声明了组件的 JSX。在主包装容器中,我们设置了ref
属性,用于获取图像的当前位置。每当我们想要在当前类的任何方法中访问组件时,我们都使用引用。我们可以通过简单地设置ref
属性并为任何组件分配一个名称来创建引用。
在步骤 18中,我们插值动画值以获得每一帧的正确顶部值。插值的输出将从图像的当前位置开始,并向屏幕中间进展。这样,根据值是负数还是正数,动画将从底部向顶部运行,或者反之。
我们不需要插值 opacity
,因为当前的动画值已经从 0
到 1
。
另请参阅
Refs 和 DOM 的深入解释可以在以下链接找到:
reactjs.org/docs/refs-and-the-dom.html
。
第八章:处理应用逻辑和数据
在本章中,我们将涵盖以下内容:
-
本地存储和检索数据
-
从远程 API 检索数据
-
向远程 API 发送数据
-
与 WebSockets 建立实时通信
-
将持久数据库功能与 Realm 集成
-
在网络连接丢失时掩盖应用程序
-
将本地持久化数据与远程 API 同步
介绍
开发任何应用程序最重要的一个方面是处理数据。这些数据可能来自用户本地,可能由公开 API 的远程服务器提供,或者,与大多数业务应用程序一样,可能是两者的组合。您可能想知道处理数据的最佳策略是什么,或者如何甚至完成简单的任务,比如发出 HTTP 请求。幸运的是,React Native 通过提供易于处理来自各种不同来源的数据的机制,使您的生活变得更加简单。
开源社区已经进一步提供了一些可以与 React Native 一起使用的优秀模块。在本章中,我们将讨论如何处理各个方面的数据,以及如何将其整合到我们的 React Native 应用程序中。
本地存储和检索数据
在开发移动应用程序时,我们需要考虑需要克服的网络挑战。一个设计良好的应用程序应该允许用户在没有互联网连接时继续使用应用程序。这需要应用程序在没有互联网连接时在设备上本地保存数据,并在网络再次可用时将数据与服务器同步。
另一个需要克服的挑战是网络连接,可能会很慢或有限。为了提高我们应用的性能,我们应该将关键数据保存在本地设备上,以避免对服务器 API 造成压力。
在这个示例中,我们将学习一种从设备本地保存和检索数据的基本有效策略。我们将创建一个简单的应用程序,其中包含一个文本输入和两个按钮,一个用于保存字段内容,另一个用于加载现有内容。我们将使用AsyncStorage
类来实现我们的目标。
准备工作
我们需要创建一个名为local-data-storage
的空应用程序。
如何做...
- 我们将从
App
组件开始。让我们首先导入所有的依赖项:
import React, { Component } from 'react';
import {
Alert,
AsyncStorage,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
- 现在,让我们创建
App
类。我们将创建一个key
常量,以便我们可以设置要用于保存内容的键的名称。在state
中,我们将有两个属性:一个用于保存来自文本输入组件的值,另一个用于加载和显示当前存储的值:
const key = '@MyApp:key';
export default class App extends Component {
state = {
text: '',
storedValue: '',
};
//Defined in later steps
}
- 当组件挂载时,如果存在,我们希望加载已存储的值。一旦应用程序加载,我们将显示内容,因此我们需要在
componentWillMount
生命周期方法中读取本地值:
componentWillMount() {
this.onLoad();
}
onLoad
函数从本地存储加载当前内容。就像浏览器中的localStorage
一样,只需使用保存数据时定义的键即可:
onLoad = async () => {
try {
const storedValue = await AsyncStorage.getItem(key);
this.setState({ storedValue });
} catch (error) {
Alert.alert('Error', 'There was an error while loading the
data');
}
}
- 保存数据也很简单。我们将声明一个键,通过
AsyncStorage
的setItem
方法保存我们想要与该键关联的任何数据:
onSave = async () => {
const { text } = this.state;
try {
await AsyncStorage.setItem(key, text);
Alert.alert('Saved', 'Successfully saved on device');
} catch (error) {
Alert.alert('Error', 'There was an error while saving the
data');
}
}
- 接下来,我们需要一个函数,将输入文本中的值保存到
state
中。当输入的值发生变化时,我们将获取新的值并保存到state
中:
onChange = (text) => {
this.setState({ text });
}
- 我们的 UI 将很简单:只有一个
Text
元素来呈现保存的内容,一个TextInput
组件允许用户输入新值,以及两个按钮。一个按钮将调用onLoad
函数来加载当前保存的值,另一个将保存文本输入的值:
render() {
const { storedValue, text } = this.state;
return (
<View style={styles.container}>
<Text style={styles.preview}>{storedValue}</Text>
<View>
<TextInput
style={styles.input}
onChangeText={this.onChange}
value={text}
placeholder="Type something here..."
/>
<TouchableOpacity onPress={this.onSave} style=
{styles.button}>
<Text>Save locally</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.onLoad} style=
{styles.button}>
<Text>Load data</Text>
</TouchableOpacity>
</View>
</View>
);
}
- 最后,让我们添加一些样式。这将是简单的颜色、填充、边距和布局,如第二章中所述,创建一个简单的 React Native 应用:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
preview: {
backgroundColor: '#bdc3c7',
width: 300,
height: 80,
padding: 10,
borderRadius: 5,
color: '#333',
marginBottom: 50,
},
input: {
backgroundColor: '#ecf0f1',
borderRadius: 3,
width: 300,
height: 40,
padding: 5,
},
button: {
backgroundColor: '#f39c12',
padding: 10,
borderRadius: 3,
marginTop: 10,
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
AsyncStorage
类允许我们轻松地在本地设备上保存数据。在 iOS 上,这是通过在文本文件上使用字典来实现的。在 Android 上,它将使用 RocksDB 或 SQLite,具体取决于可用的内容。
不建议使用此方法保存敏感信息,因为数据未加密。
在步骤 4中,我们加载了当前保存的数据。AsyncStorage
API 包含一个getItem
方法。该方法接收我们要检索的键作为参数。我们在这里使用await
/async
语法,因为这个调用是异步的。获取值后,我们只需将其设置为state
;这样,我们就能在视图上呈现数据。
在步骤 7中,我们保存了state
中的文本。使用setItem
方法,我们可以设置一个新的key
和任何我们想要的值。这个调用是异步的,因此我们使用了await
/async
语法。
另请参阅
关于 JavaScript 中async
/await
的工作原理的一篇很棒的文章,可在ponyfoo.com/articles/understanding-javascript-async-await
上找到。
从远程 API 检索数据
在之前的章节中,我们使用了来自 JSON 文件或直接在源代码中定义的数据。虽然这对我们之前的示例很有用,但在现实世界的应用程序中很少有帮助。
在这个示例中,我们将学习如何从 API 请求数据。我们将从 API 中进行GET
请求,以获得 JSON 响应。然而,现在,我们只会在文本元素中显示 JSON。我们将使用 Fake Online REST API for Testing and Prototyping,托管在jsonplaceholder.typicode.com
,由优秀的开发测试 API 软件 JSON Server(github.com/typicode/json-server
)提供支持。
我们将保持这个应用程序简单,以便我们可以专注于数据管理。我们将有一个文本组件,用于显示来自 API 的响应,并添加一个按钮,按下时请求数据。
准备工作
我们需要创建一个空的应用。让我们把这个命名为remote-api
。
如何做...
- 让我们从
App.js
文件中导入我们的依赖项开始:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View
} from 'react-native';
- 我们将在
state
上定义一个results
属性。这个属性将保存来自 API 的响应。一旦我们得到响应,我们需要更新视图:
export default class App extends Component {
state = {
results: '',
};
// Defined later
}
const styles = StyleSheet.create({
// Defined later
});
- 当按钮被按下时,我们将发送请求。接下来,让我们创建一个处理该请求的方法:
onLoad = async () => {
this.setState({ results: 'Loading, please wait...' });
const response = await fetch('http://jsonplaceholder.typicode.com/users', {
method: 'GET',
});
const results = await response.text();
this.setState({ results });
}
- 在
render
方法中,我们将显示来自state
的响应。我们将使用TextInput
来显示 API 数据。通过属性,我们将声明编辑为禁用,并支持多行功能。按钮将调用我们在上一步中创建的onLoad
函数:
render() {
const { results } = this.state;
return (
<View style={styles.container}>
<View>
<TextInput
style={styles.preview}
value={results}
placeholder="Results..."
editable={false}
multiline
/>
<TouchableOpacity onPress={this.onLoad} style=
{styles.btn}>
<Text>Load data</Text>
</TouchableOpacity>
</View>
</View>
);
}
- 最后,我们将添加一些样式。同样,这只是布局、颜色、边距和填充:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
preview: {
backgroundColor: '#bdc3c7',
width: 300,
height: 400,
padding: 10,
borderRadius: 5,
color: '#333',
marginBottom: 50,
},
btn: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 3,
marginTop: 10,
},
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
在步骤 4中,我们向 API 发送了请求。我们使用fetch
方法发出请求。第一个参数是一个包含端点 URL 的字符串,而第二个参数是一个配置对象。对于这个请求,我们需要定义的唯一选项是request
方法为GET
,但我们也可以使用这个对象来定义头部、cookie、参数和许多其他内容。
我们还使用async
/await
语法来等待响应,并最终将其设置在state
上。如果您愿意,当然也可以使用承诺来实现这个目的。
还要注意,我们在这里使用箭头函数来正确处理作用域。当将此方法分配给onPress
回调时,这将自动设置正确的作用域。
向远程 API 发送数据
在上一个教程中,我们介绍了如何使用fetch
从 API 获取数据。在这个教程中,我们将学习如何向同一个 API 发送数据。这个应用程序将模拟创建一个论坛帖子,帖子的请求将具有title
、body
和user
参数。
如何做到这一点...准备好
在进行本教程之前,我们需要创建一个名为remote-api-post
的新空应用程序。
在这个教程中,我们还将使用非常流行的axios
包来处理我们的 API 请求。您可以通过终端使用yarn
安装它:
yarn add axios
或者,您也可以使用npm
:
npm install axios --save
首先,我们需要从state
中获取值。我们还可以在这里运行一些验证,以确保title
和body
不为空。在POST
请求中,我们需要定义请求的内容类型,这种情况下将是 JSON。我们将userId
属性硬编码为1
。在真实的应用程序中,我们可能会从之前的 API 请求中获取这个值。请求完成后,我们获取 JSON 响应,如果成功,将触发我们之前定义的onLoad
方法:
- 首先,我们需要打开
App.js
文件并导入我们将要使用的依赖项:
import React, { Component } from 'react';
import axios from 'axios';
import {
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
SafeAreaView,
} from 'react-native';
- 我们将定义
App
类,其中包含一个具有三个属性的state
对象。title
和body
属性将用于发出请求,results
将保存 API 的响应:
const endpoint = 'http://jsonplaceholder.typicode.com/posts';
export default class App extends Component {
state = {
results: '',
title: '',
body: '',
};
const styles = StyleSheet.create({
// Defined later
});
}
- 保存新帖子后,我们将从 API 请求所有帖子。我们将定义一个
onLoad
方法来获取新数据。这段代码与上一个教程中的onLoad
方法完全相同,但这次,我们将使用axios
包来创建请求:
onLoad = async () => {
this.setState({ results: 'Loading, please wait...' });
const response = await axios.get(endpoint);
const results = JSON.stringify(response);
this.setState({ results });
}
- 让我们开始保存新数据。
onSave = async () => {
const { title, body } = this.state;
try {
const response = await axios.post(endpoint, {
headers: {
'Content-Type': 'application/json;charset=UTF-8',
},
params: {
userId: 1,
title,
body
}
});
const results = JSON.stringify(response);
Alert.alert('Success', 'Post successfully saved');
this.onLoad();
} catch (error) {
Alert.alert('Error', `There was an error while saving the
post: ${error}`);
}
}
- 保存功能已经完成。接下来,我们需要方法来保存
title
和body
到state
。这些方法将在用户在输入文本中输入时执行,跟踪state
对象上的值:
onTitleChange = (title) => this.setState({ title });
onPostChange = (body) => this.setState({ body });
- 我们已经拥有了功能所需的一切,所以让我们添加 UI。
render
方法将显示一个工具栏、两个输入文本和一个保存按钮,用于调用我们在步骤 4中定义的onSave
方法:
render() {
const { results, title, body } = this.state;
return (
<SafeAreaView style={styles.container}>
<Text style={styles.toolbar}>Add a new post</Text>
<ScrollView style={styles.content}>
<TextInput
style={styles.input}
onChangeText={this.onTitleChange}
value={title}
placeholder="Title"
/>
<TextInput
style={styles.input}
onChangeText={this.onPostChange}
value={body}
placeholder="Post body..."
/>
<TouchableOpacity onPress={this.onSave} style=
{styles.button}>
<Text>Save</Text>
</TouchableOpacity>
<TextInput
style={styles.preview}
value={results}
placeholder="Results..."
editable={false}
multiline
/>
</ScrollView>
</SafeAreaView>
);
}
- 最后,让我们添加样式来定义布局、颜色、填充和边距:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
toolbar: {
backgroundColor: '#3498db',
color: '#fff',
textAlign: 'center',
padding: 25,
fontSize: 20,
},
content: {
flex: 1,
padding: 10,
},
preview: {
backgroundColor: '#bdc3c7',
flex: 1,
height: 500,
},
input: {
backgroundColor: '#ecf0f1',
borderRadius: 3,
height: 40,
padding: 5,
marginBottom: 10,
flex: 1,
},
button: {
backgroundColor: '#3498db',
padding: 10,
borderRadius: 3,
marginBottom: 30,
},
});
- 最终的应用程序应该类似于以下截图:
工作原理...
在步骤 2中,我们在state
上定义了三个属性。results
属性将包含来自服务器 API 的响应,我们稍后将用它来在 UI 中显示值。
我们使用title
和body
属性来保存输入文本组件中的值,以便用户可以创建新的帖子。然后,这些值将在按下保存按钮时发送到 API。
在步骤 6中,我们在 UI 上声明了元素。我们使用了两个输入来输入数据和一个保存按钮,当按下时调用onSave
方法。最后,我们使用输入文本来显示结果。
使用 WebSockets 建立实时通信
在本教程中,我们将在 React Native 应用程序中集成 WebSockets。我们将使用 WebSockets 应用程序的Hello World,也就是一个简单的聊天应用程序。这个应用程序将允许用户发送和接收消息。
准备工作
为了在 React Native 上支持 WebSockets,我们需要运行一个服务器来处理所有连接的客户端。服务器应该能够在收到任何连接的客户端的消息时广播消息。
我们将从一个新的空白 React Native 应用程序开始。我们将其命名为web-sockets
。在项目的根目录下,让我们添加一个带有index.js
文件的server
文件夹。如果您还没有它,您将需要 Node 来运行服务器。您可以从nodejs.org/
获取 Node.js,或者使用 Node 版本管理器(github.com/creationix/nvm
)。
我们将使用优秀的 WebSocket 包ws
。您可以通过终端使用yarn
添加该包:
yarn add ws
或者,您可以使用npm
:
npm install --save ws
安装完包后,将以下代码添加到/server/index.js
文件中。一旦此服务器运行,它将通过server.on('connection')
监听传入的连接,并通过socket.on('message')
监听传入的消息。有关ws
的更多信息,请查看github.com/websockets/ws
上的文档:
const port = 3001;
const WebSocketServer = require('ws').Server;
const server = new WebSocketServer({ port });
server.on('connection', (socket) => {
socket.on('message', (message) => {
console.log('received: %s', message);
server.clients.forEach(client => {
if (client !== socket) {
client.send(message);
}
});
});
});
console.log(`Web Socket Server running on port ${port}`);
一旦服务器代码就位,您可以通过在项目根目录的终端中运行以下命令来启动服务器:
node server/index.js
让服务器保持运行,这样一旦我们构建了 React Native 应用程序,我们就可以使用服务器在客户端之间进行通信。
如何做到这一点...
- 首先,让我们创建
App.js
文件并导入我们将使用的所有依赖项:
import React, { Component } from 'react';
import {
Dimensions,
ScrollView,
StyleSheet,
Text,
TextInput,
SafeAreaView,
View,
Platform
} from 'react-native';
- 在
state
对象上,我们将声明一个history
属性。该属性将是一个数组,用于保存用户之间来回发送的所有消息:
export default class App extends Component {
state = {
history: [],
};
// Defined in later steps
}
const styles = StyleSheet.create({
// Defined in later steps
});
- 现在,我们需要通过连接到服务器并设置用于接收消息、错误以及连接打开或关闭时的回调函数来将 WebSockets 集成到我们的应用中。我们将在组件创建后执行此操作,使用
componentWillMount
生命周期钩子:
componentWillMount() {
const localhost = Platform.OS === 'android' ? '10.0.3.2' :
'localhost';
this.ws = new WebSocket(`ws://${localhost}:3001`);
this.ws.onopen = this.onOpenConnection;
this.ws.onmessage = this.onMessageReceived;
this.ws.onerror = this.onError;
this.ws.onclose = this.onCloseConnection;
}
- 让我们定义打开/关闭连接和处理接收到的错误的回调。我们只是要记录这些操作,但这是我们可以在连接关闭时显示警报消息,或者在服务器抛出错误时显示错误消息的地方:
onOpenConnection = () => {
console.log('Open!');
}
onError = (event) => {
console.log('onerror', event.message);
}
onCloseConnection = (event) => {
console.log('onclose', event.code, event.reason);
}
- 当从服务器接收到新消息时,我们需要将其添加到
state
上的history
属性中,以便在消息到达时能够立即呈现新内容:
onMessageReceived = (event) => {
this.setState({
history: [
...this.state.history,
{ isSentByMe: false, messageText: event.data },
],
});
}
- 现在,让我们发送消息。我们需要定义一个方法,当用户在键盘上按下Return键时将执行该方法。此时我们需要做两件事:将新消息添加到
history
中,然后通过 socket 发送消息:
onSendMessage = () => {
const { text } = this.state;
this.setState({
text: '',
history: [
...this.state.history,
{ isSentByMe: true, messageText: text },
],
});
this.ws.send(text);
}
- 在上一步中,我们从
state
中获取了text
属性。每当用户在输入框中输入内容时,我们需要跟踪该值,因此我们需要一个用于监听按键并将值保存到state
的函数:
onChangeText = (text) => {
this.setState({ text });
}
- 我们已经就位了所有功能,现在让我们来处理 UI。在
render
方法中,我们将添加一个工具栏,一个滚动视图来呈现history
中的所有消息,以及一个文本输入框,允许用户发送新消息:
render() {
const { history, text } = this.state;
return (
<SafeAreaView style={[styles.container, android]}>
<Text style={styles.toolbar}>Simple Chat</Text>
<ScrollView style={styles.content}>
{ history.map(this.renderMessage) }
</ScrollView>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={text}
onChangeText={this.onChangeText}
onSubmitEditing={this.onSendMessage}
/>
</View>
</SafeAreaView>
);
}
- 为了渲染来自
history
的消息,我们将遍历history
数组,并通过renderMessage
方法渲染每条消息。我们需要检查当前消息是否属于此设备上的用户,以便我们可以应用适当的样式:
renderMessage(item, index){
const sender = item.isSentByMe ? styles.me : styles.friend;
return (
<View style={[styles.msg, sender]} key={index}>
<Text>{item.msg}</Text>
</View>
);
}
- 最后,让我们来处理样式!让我们为工具栏、
history
组件和文本输入添加样式。我们需要将history
容器设置为灵活,因为我们希望它占用所有可用的垂直空间:
const styles = StyleSheet.create({
container: {
backgroundColor: '#ecf0f1',
flex: 1,
},
toolbar: {
backgroundColor: '#34495e',
color: '#fff',
fontSize: 20,
padding: 25,
textAlign: 'center',
},
content: {
flex: 1,
},
inputContainer: {
backgroundColor: '#bdc3c7',
padding: 5,
},
input: {
height: 40,
backgroundColor: '#fff',
},
// Defined in next step
});
- 现在,让我们来处理每条消息的样式。我们将为所有消息创建一个名为
msg
的公共样式对象,然后为来自设备上用户的消息创建样式,最后为来自其他人的消息创建样式,相应地更改颜色和对齐方式:
msg: {
margin: 5,
padding: 10,
borderRadius: 10,
},
me: {
alignSelf: 'flex-start',
backgroundColor: '#1abc9c',
marginRight: 100,
},
friend: {
alignSelf: 'flex-end',
backgroundColor: '#fff',
marginLeft: 100,
}
- 最终的应用程序应该类似于以下屏幕截图:
它是如何工作的...
在步骤 2中,我们声明了state
对象,其中包含一个用于跟踪消息的history
数组。history
属性将保存表示客户端之间交换的所有消息的对象。每个对象都将有两个属性:包含消息文本的字符串,以及用于确定发送者的布尔标志。我们可以在这里添加更多数据,例如用户的名称、头像图像的 URL,或者我们可能需要的其他任何内容。
在步骤 3中,我们连接到 WebSocket 服务器提供的套接字,并设置了处理套接字事件的回调。我们指定了服务器地址以及端口。
在步骤 5中,我们定义了当从服务器接收到新消息时要执行的回调。在这里,每次接收到新消息时,我们都会向state
的history
数组添加一个新对象。每个消息对象都有isSentByMe
的属性
messageText
。
在步骤 6中,我们将消息发送到服务器。我们需要将消息添加到历史记录中,因为服务器将向所有其他客户端广播消息,但不包括消息的作者。为了跟踪这条消息,我们需要手动将其添加到历史记录中。
将持久数据库功能与 Realm 集成
随着您的应用程序变得更加复杂,您可能会达到需要在设备上存储数据的地步。这可能是业务数据,比如用户列表,以避免不得不进行昂贵的网络连接到远程 API。也许您根本没有 API,您的应用程序作为一个自给自足的实体运行。无论情况如何,您可能会受益于利用数据库来存储您的数据。对于 React Native 应用程序有多种选择。第一个选择是 AsyncStorage
,我们在本章的 存储和检索本地数据 教程中介绍过。您还可以考虑 SQLite,或者您可以编写一个适配器来连接到特定于操作系统的数据提供程序,比如 Core Data。
另一个极好的选择是使用移动数据库,比如 Realm。Realm 是一个非常快速、线程安全、事务性、基于对象的数据库。它主要设计用于移动设备,具有直观的 JavaScript API。它支持其他功能,如加密、复杂查询、UI 绑定等。您可以在 realm.io/products/realm-mobile-database/
上阅读有关它的所有信息。
在本教程中,我们将介绍在 React Native 中使用 Realm。我们将创建一个简单的数据库,并执行基本操作,如插入、更新和删除记录。然后我们将在 UI 中显示这些记录。
准备工作
让我们创建一个名为 realm-db
的新的空白 React Native 应用程序。
安装 Realm 需要运行以下命令:
react-native link
因此,我们将致力于一个从 Expo 弹出的应用程序。这意味着您可以使用以下命令创建此应用程序:
react-native init
或者,您可以使用以下命令创建一个新的 Expo 应用程序:
expo init
然后,您可以通过以下命令弹出使用 Expo 创建的应用程序:
expo eject
创建 React Native 应用程序后,请务必通过 cd
在新应用程序中使用 ios
目录安装 CocoaPods 依赖项,并运行以下命令:
pod install
有关 CocoaPods 工作原理的详细解释以及弹出(或纯粹的 React Native)应用程序与 Expo React Native 应用程序的区别,请参阅第十章 应用程序工作流程和第三方插件。
在将数据发送到远程 API的示例中,我们使用了axios
包来处理我们的 AJAX 调用。在这个示例中,我们将使用原生 JavaScript 的 fetch
方法进行 AJAX 调用。两种方法都同样有效,对两种方法都有所了解,希望能让您决定您更喜欢哪种方法来进行您的项目。
一旦您处理了创建一个弹出式应用程序,就可以使用 yarn
安装 Realm:
yarn add realm
或者,您可以使用 npm
:
npm install --save realm
安装了包之后,您可以使用以下代码链接本机包:
react-native link realm
如何做...
- 首先,让我们打开
App.js
并导入我们将要使用的依赖项:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity
} from 'react-native';
import Realm from 'realm';
- 接下来,我们需要在
componentWillMount
方法中实例化我们的 Realm 数据库。我们将使用realm
类变量保留对它的引用:
export default class App extends Component {
realm;
componentWillMount() {
const realm = this.realm = new Realm({
schema: [
{
name: 'User',
properties: {
firstName: 'string',
lastName: 'string',
email: 'string'
}
}
]
});
}
// Defined in later steps.
}
- 为了创建
User
条目,我们将使用randomuser.me提供的随机用户生成器 API。让我们创建一个带有getRandomUser
函数的方法。这将fetch
这些数据:
getRandomUser() {
return fetch('https://randomuser.me/api/')
.then(response => response.json());
}
- 我们还需要一个在我们的应用程序中创建用户的方法。
createUser
方法将使用我们之前定义的函数来获取一个随机用户,然后使用realm.write
方法和realm.create
方法将其保存到我们的 realm 数据库中:
createUser = () => {
const realm = this.realm;
this.getRandomUser().then((response) => {
const user = response.results[0];
const userName = user.name;
realm.write(() => {
realm.create('User', {
firstName: userName.first,
lastName: userName.last,
email: user.email
});
this.setState({users:realm.objects('User')});
});
});
}
- 由于我们正在与数据库交互,我们还应该添加一个用于更新数据库中的
User
的函数。updateUser
将简单地获取集合中的第一条记录并更改其信息:
updateUser = () => {
const realm = this.realm;
const users = realm.objects('User');
realm.write(() => {
if(users.length) {
let firstUser = users.slice(0,1)[0];
firstUser.firstName = 'Bob';
firstUser.lastName = 'Cookbook';
firstUser.email = 'react.native@cookbook.com';
this.setState(users);
}
});
}
- 最后,让我们添加一种删除用户的方法。我们将添加一个
deleteUsers
方法来删除所有用户。这是通过调用带有执行realm.deleteAll
的回调函数的realm.write
实现的:
deleteUsers = () => {
const realm = this.realm;
realm.write(() => {
realm.deleteAll();
this.setState({users:realm.objects('User')});
});
}
- 让我们构建我们的用户界面。我们将呈现一个
User
对象列表和一个按钮,用于我们的create
、update
和delete
方法:
render() {
const realm = this.realm;
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to Realm DB Test!
</Text>
<View style={styles.buttonContainer}>
<TouchableOpacity style={styles.button}
onPress={this.createUser}>
<Text style={styles.buttontext}>Add User</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}
onPress={this.updateUser}>
<Text>Update First User</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.button}
onPress={this.deleteUsers}>
<Text>Remove All Users</Text>
</TouchableOpacity>
</View>
<View style={styles.container}>
<Text style={styles.welcome}>Users:</Text>
{this.state.users.map((user, idx) => {
return <Text key={idx}>{user.firstName} {user.lastName}
{user.email}</Text>;
})}
</View>
</View>
);
}
- 一旦我们在任何平台上运行应用程序,我们与数据库交互的三个按钮应该显示在我们的 Realm 数据库中保存的实时数据上:
它是如何工作的...
Realm 数据库是用 C++构建的,其核心被称为Realm 对象存储。有一些产品为每个主要平台(Java、Objective-C、Swift、Xamarin 和 React Native)封装了这个对象存储。React Native 的实现是 Realm 的 JavaScript 适配器。从 React Native 方面来看,我们不需要担心实现细节。相反,我们得到了一个用于持久化和检索数据的清晰 API。步骤 4到步骤 6演示了使用一些基本的 Realm 方法。如果您想了解 API 的更多用法,请查看此文档,网址为realm.io/docs/react-native/latest/api/
。
在网络连接丢失时对应用进行屏蔽
互联网连接并不总是可用,特别是当人们在城市中移动、乘坐火车或在山上徒步时。良好的用户体验将在用户失去与互联网的连接时通知用户。
在这个示例中,我们将创建一个应用,当网络连接丢失时会显示一条消息。
准备工作
我们需要创建一个空的应用。让我们把它命名为network-loss
。
如何做...
- 让我们从
App.js
中导入必要的依赖项开始:
import React, { Component } from 'react';
import {
SafeAreaView,
NetInfo,
StyleSheet,
Text,
View,
Platform
} from 'react-native';
- 接下来,我们将定义
App
类和一个用于存储连接状态的state
对象。如果连接成功,online
布尔值将为true
,如果连接失败,offline
布尔值将为true
:
export default class App extends Component {
state = {
online: null,
offline: null,
};
// Defined in later steps
}
- 组件创建后,我们需要获取初始网络状态。我们将使用
NetInfo
类的getConnectionInfo
方法来获取当前状态,并且我们还将设置一个回调,当状态改变时将执行该回调:
componentWillMount() {
NetInfo.getConnectionInfo().then((connectionInfo) => {
this.onConnectivityChange(connectionInfo);
});
NetInfo.addEventListener('connectionChange',
this.onConnectivityChange);
}
- 当组件即将被销毁时,我们需要通过
componentWillUnmount
生命周期来移除监听器:
componentWillUnmount() {
NetInfo.removeEventListener('connectionChange',
this.onConnectivityChange);
}
- 让我们添加一个在网络状态改变时执行的回调。它只是检查当前的网络类型是否为
none
,并相应地设置state
:
onConnectivityChange = connectionInfo => {
this.setState({
online: connectionInfo.type !== 'none',
offline: connectionInfo.type === 'none',
});
}
- 现在,我们知道网络是开启还是关闭,但我们仍然需要一个用于显示信息的用户界面。让我们渲染一个带有一些虚拟文本的工具栏作为内容:
render() {
return (
<SafeAreaView style={styles.container}>
<Text style={styles.toolbar}>My Awesome App</Text>
<Text style={styles.text}>Lorem...</Text>
<Text style={styles.text}>Lorem ipsum...</Text>
{this.renderMask()}
</SafeAreaView>
);
}
- 正如您从上一步中看到的,有一个
renderMask
函数。当网络离线时,此函数将返回一个模态,如果在线则什么都不返回:
renderMask() {
if (this.state.offline) {
return (
<View style={styles.mask}>
<View style={styles.msg}>
<Text style={styles.alert}>Seems like you do not have
network connection anymore.</Text>
<Text style={styles.alert}>You can still continue
using the app, with limited content.</Text>
</View>
</View>
);
}
}
- 最后,让我们为我们的应用添加样式。我们将从工具栏和内容开始:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5FCFF',
},
toolbar: {
backgroundColor: '#3498db',
padding: 15,
fontSize: 20,
color: '#fff',
textAlign: 'center',
},
text: {
padding: 10,
},
// Defined in next step
}
- 对于断开连接的消息,我们将在所有内容的顶部呈现一个黑色蒙版,并在屏幕中央放置一个带有文本的容器。对于
mask
,我们需要将位置设置为absolute
,然后将top
、bottom
、right
和left
设置为0
。我们还将为 mask 的背景颜色添加不透明度,并将内容调整和对齐到中心:
const styles = StyleSheet.create({
// Defined in previous step
mask: {
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
bottom: 0,
justifyContent: 'center',
left: 0,
position: 'absolute',
top: 0,
right: 0,
},
msg: {
backgroundColor: '#ecf0f1',
borderRadius: 10,
height: 200,
justifyContent: 'center',
padding: 10,
width: 300,
},
alert: {
fontSize: 20,
textAlign: 'center',
margin: 5,
}
});
- 要在模拟器中看到蒙版显示,模拟设备必须断开与互联网的连接。对于 iOS 模拟器,只需断开 Mac 的 Wi-Fi 或拔掉以太网即可断开模拟器与互联网的连接。在 Android 模拟器上,您可以通过工具栏禁用手机的 Wi-Fi 连接:
- 一旦设备与互联网断开连接,蒙版应该相应地显示:
工作原理...
在步骤 2中,我们创建了初始的state
对象,其中包含两个属性:online
在网络连接可用时为true
,offline
在网络不可用时为true
。
在步骤 3中,我们检索了初始的网络状态并设置了一个监听器来检查状态的变化。NetInfo
返回的网络类型可以是wifi
、cellular
、unknown
或none
。Android 还有额外的选项,如bluetooth
、ethernet
和WiMAX
(用于 WiMAX 连接)。您可以阅读文档以查看所有可用的值:facebook.github.io/react-native/docs/netinfo.html
。
在步骤 5中,我们定义了每当网络状态发生变化时执行的方法,并相应地设置了state
的值online
和offline
。更新状态会重新渲染 DOM,如果没有连接,则会显示蒙版。
将本地持久化数据与远程 API 同步
在使用移动应用程序时,网络连接通常是理所当然的。但是当您的应用程序需要调用 API 并且用户刚刚失去连接时会发生什么?幸运的是,对于我们来说,React Native 有一个模块可以对网络连接状态做出反应。我们可以以一种支持连接丢失的方式设计我们的应用程序,通过在网络连接恢复后自动同步我们的数据来实现。
这个步骤将展示使用NetInfo
模块来控制我们的应用是否会进行 API 调用的简单实现。如果失去连接,我们将保留挂起请求的引用,并在网络访问恢复时完成它。我们将再次使用jsonplaceholder.typicode.com
来向实时服务器发出POST
请求。
准备工作
对于这个步骤,我们将使用一个名为syncing-data
的空的 React Native 应用程序。
如何做...
- 我们将从在
App.js
中导入我们的依赖项开始这个步骤:
import React from 'react';
import {
StyleSheet,
Text,
View,
NetInfo,
TouchableOpacity
} from 'react-native';
- 我们需要添加
pendingSync
类变量,用于在没有可用网络连接时存储挂起的请求。我们还将创建state
对象,其中包含用于跟踪应用程序是否连接(isConnected
)、同步状态(syncStatus
)以及发出POST
请求后服务器的响应(serverResponse
)的属性:
export default class App extends React.Component {
pendingSync;
state = {
isConnected: null,
syncStatus: null,
serverResponse: null
}
// Defined in later steps
}
- 在
componentWillMount
生命周期钩子中,我们将通过NetInfo.isConnected.fetch
方法获取网络连接的状态,并使用响应设置状态的isConnected
属性。我们还将添加一个connectionChange
事件的事件侦听器,以跟踪连接的变化:
componentWillMount() {
NetInfo.isConnected.fetch().then(isConnected => {
this.setState({isConnected});
});
NetInfo.isConnected.addEventListener('connectionChange',
this.onConnectionChange);
}
- 接下来,让我们实现在前一步中定义的事件侦听器将执行的回调。在这个方法中,我们将更新
state
的isConnected
属性。然后,如果定义了pendingSync
类变量,这意味着我们有一个缓存的POST
请求,因此我们将提交该请求并相应地更新状态:
onConnectionChange = (isConnected) => {
this.setState({isConnected});
if (this.pendingSync) {
this.setState({syncStatus : 'Syncing'});
this.submitData(this.pendingSync).then(() => {
this.setState({syncStatus : 'Sync Complete'});
});
}
}
- 接下来,我们需要实现一个函数,当有活动网络连接时,将实际进行 API 调用:
submitData(requestBody) {
return fetch('http://jsonplaceholder.typicode.com/posts', {
method : 'POST',
body : JSON.stringify(requestBody)
}).then((response) => {
return response.text();
}).then((responseText) => {
this.setState({
serverResponse : responseText
});
});
}
- 在我们可以开始处理用户界面之前,我们需要做的最后一件事是为“提交数据”按钮添加一个处理
onPress
事件的函数,我们将要渲染这个按钮。如果没有网络连接,这将立即执行调用,否则将保存在this.pendingSync
中:
onSubmitPress = () => {
const requestBody = {
title: 'foo',
body: 'bar',
userId: 1
};
if (this.state.isConnected) {
this.submitData(requestBody);
} else {
this.pendingSync = requestBody;
this.setState({syncStatus : 'Pending'});
}
}
- 现在,我们可以构建我们的用户界面,它将渲染“提交数据”按钮,并显示当前连接状态、同步状态以及来自 API 的最新响应:
render() {
const {
isConnected,
syncStatus,
serverResponse
} = this.state;
return (
<View style={styles.container}>
<TouchableOpacity onPress={this.onSubmitPress}>
<View style={styles.button}>
<Text style={styles.buttonText}>Submit Data</Text>
</View>
</TouchableOpacity>
<Text style={styles.status}>
Connection Status: {isConnected ? 'Connected' :
'Disconnected'}
</Text>
<Text style={styles.status}>
Sync Status: {syncStatus}
</Text>
<Text style={styles.status}>
Server Response: {serverResponse}
</Text>
</View>
);
}
- 您可以像在上一个步骤的步骤 10中描述的那样在模拟器中禁用网络连接:
它是如何工作的...
这个步骤利用了NetInfo
模块来控制何时进行 AJAX 请求。
在步骤 6中,我们定义了当单击“提交数据”按钮时执行的方法。如果没有连接,我们将请求主体保存到pendingSync
类变量中。
在步骤 3中,我们定义了componentWillMount
生命周期钩子。在这里,两个NetInfo
方法调用检索当前的网络连接状态,并附加一个事件监听器到变化事件。
在步骤 4中,我们定义了当网络连接发生变化时将执行的函数,该函数适当地通知状态的isConnected
布尔属性。如果设备已连接,我们还会检查是否有挂起的 API 调用,并在存在时完成请求。
这个教程也可以扩展为支持挂起调用的队列系统,这将允许多个 AJAX 请求延迟,直到重新建立互联网连接。
使用 Facebook 登录
Facebook 是全球最大的社交媒体平台,拥有超过 10 亿用户。这意味着您的用户很可能拥有 Facebook 账户。您的应用程序可以注册并链接到他们的账户,从而允许您使用他们的 Facebook 凭据作为应用程序的登录。根据请求的权限,这还将允许您访问用户信息、图片,甚至让您能够访问共享内容。您可以从 Facebook 文档中了解更多关于可用权限选项的信息,网址为developers.facebook.com/docs/facebook-login/permissions#reference-public_profile
。
在这个教程中,我们将介绍通过应用程序登录 Facebook 以获取会话令牌的基本方法。然后,我们将使用该令牌访问 Facebook 的 Graph API 提供的基本/me
端点,这将给我们用户的姓名和 ID。要与 Facebook Graph API 进行更复杂的交互,您可以查看文档,该文档可以在developers.facebook.com/docs/graph-api/using-graph-api
找到。
为了使这个示例简单,我们将构建一个使用Expo.Facebook.logInWithReadPermissionsAsync
方法来完成 Facebook 登录的 Expo 应用程序,这也将允许我们绕过其他必要的设置。如果您希望在不使用 Expo 的情况下与 Facebook 交互,您可能希望使用 React Native Facebook SDK,这需要更多的步骤。您可以在github.com/facebook/react-native-fbsdk
找到 SDK。
准备工作
对于这个示例,我们将创建一个名为facebook-login
的新应用程序。您需要有一个活跃的 Facebook 账户来测试其功能。
这个示例还需要一个 Facebook 开发者账户。如果您还没有,请前往developers.facebook.com
注册。一旦您登录,您可以使用仪表板创建一个新的应用程序。一旦创建完成,请记下应用程序 ID,因为我们将在示例中需要它。
如何做...
- 让我们从打开
App.js
文件并添加我们的导入开始:
import React from 'react';
import {
StyleSheet,
Text,
View,
TouchableOpacity,
Alert
} from 'react-native';
import Expo from 'expo';
- 接下来,我们将声明
App
类并添加state
对象。state
将跟踪用户是否使用loggedIn
布尔值登录,并将在一个名为facebookUserInfo
的对象中保存从 Facebook 检索到的用户数据:
export default class App extends React.Component {
state = {
loggedIn: false,
facebookUserInfo: {}
}
// Defined in later steps
}
- 接下来,让我们定义我们的类的
logIn
方法。这将是在按下登录按钮时调用的方法。这个方法使用Facebook
方法的logInWithReadPermissionsAsync
Expo 辅助类来提示用户显示 Facebook 登录屏幕。在下面的代码中,用你的应用 ID 替换第一个参数,标记为APP_ID
:
logIn = async () => {
const { type, token } = await
Facebook.logInWithReadPermissionsAsync(APP_ID, {
permissions: ['public_profile'],
});
// Defined in next step
}
- 在
logIn
方法的后半部分,如果请求成功,我们将使用从登录中收到的令牌调用 Facebook Graph API 来请求已登录用户的信息。一旦响应解析,我们就相应地设置状态:
logIn = async () => {
//Defined in step above
if (type === 'success') {
const response = await fetch(`https://graph.facebook.com/me?
access_token=${token}`);
const facebookUserInfo = await response.json();
this.setState({
facebookUserInfo,
loggedIn: true
});
}
}
- 我们还需要一个简单的
render
函数。我们将显示一个登录按钮用于登录,以及一些Text
元素,一旦登录成功完成,将显示用户信息:
render() {
return (
<View style={styles.container}>
<Text style={styles.headerText}>Login via Facebook</Text>
<TouchableOpacity
onPress={this.logIn}
style={styles.button}
>
<Text style={styles.buttonText}>Login</Text>
</TouchableOpacity>
{this.renderFacebookUserInfo()}
</View>
);
}
- 如您在前面的
render
函数中所看到的,我们正在调用this.renderFacebookUserInfo
来渲染用户信息。这个方法简单地检查用户是否通过this.state.loggedIn
登录。如果是,我们将显示用户的信息。如果不是,我们将返回null
来显示空白:
renderFacebookUserInfo = () => {
return this.state.loggedIn ? (
<View style={styles.facebookUserInfo}>
<Text style={styles.facebookUserInfoLabel}>Name:</Text>
<Text style={styles.facebookUserInfoText}>
{this.state.facebookUserInfo.name}</Text>
<Text style={styles.facebookUserInfoLabel}>User ID:</Text>
<Text style={styles.facebookUserInfoText}>
{this.state.facebookUserInfo.id}</Text>
</View>
) : null;
}
- 最后,我们将添加样式以完成布局,设置填充、边距、颜色和字体大小:
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
button: {
marginTop: 30,
padding: 10,
backgroundColor: '#3B5998'
},
buttonText: {
color: '#fff',
fontSize: 30
},
headerText: {
fontSize: 30
},
facebookUserInfo: {
paddingTop: 30
},
facebookUserInfoText: {
fontSize: 24
},
facebookUserInfoLabel: {
fontSize: 20,
marginTop: 10,
color: '#474747'
}
});
- 现在,如果我们运行应用程序,我们将看到我们的登录按钮,当按下登录按钮时会出现登录模态,以及用户的信息,一旦用户成功登录,将显示在屏幕上:
它是如何工作的...
在我们的 React Native 应用中与 Facebook 互动要比以往更容易,通过 Expo 的Facebook
辅助库。
在步骤 5中,我们创建了logIn
函数,它使用Facebook.logInWithReadPermissionsAsync
来向 Facebook 发出登录请求。它接受两个参数:一个appID
和一个选项对象。在我们的情况下,我们只设置了权限选项。权限选项接受一个字符串数组,用于请求每种类型的权限,但是对于我们的目的,我们只使用最基本的权限,'public_profile'
。
在步骤 6中,我们完成了logIn
函数。在成功登录后,它使用从logInWithReadPermissionsAsync
返回的数据提供的令牌,向 Facebook 的 Graph API 端点/me
发出调用。用户的信息和登录状态将保存到状态中,这将触发重新渲染并在屏幕上显示用户的数据。
这个示例故意只调用了一个简单的 API 端点。您可以使用此端点返回的数据来填充应用程序中的用户数据。或者,您可以使用从登录中收到的相同令牌来执行图形 API 提供的任何操作。要查看通过 API 可以获得哪些数据,您可以在developers.facebook.com/docs/graph-api/reference
上查看参考文档。
第九章:实施 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
如何做...
-
作为这个教程的一部分,我们将构建应用程序将使用的文件夹结构。让我们添加一个
components
文件夹,里面有一个Album
文件夹,用来保存相册组件。我们还需要一个redux
文件夹来保存所有 Redux 代码。 -
在
redux
文件夹中,让我们添加一个index.js
文件进行 Redux 初始化。我们还需要一个photos
目录,里面有一个actions.js
文件和一个reducer.js
文件。 -
目前,
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;
它是如何工作的...
在入门中,我们安装了redux
和react-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 项目:
我们还需要创建一个redux/photos
文件夹。在这个文件夹中,我们将创建以下内容:
-
actions.js
文件,其中将包含应用程序可以执行的所有操作。我们将在下一个教程中更多地讨论操作。 -
reducer.js
文件,其中将包含管理 Redux 存储中数据的所有代码。我们将在以后的教程中更深入地探讨这个主题。
定义操作
一个 action 是发送数据到 store 的信息载荷。使用这些 actions 是组件请求或发送数据到 Redux store 的唯一方式,Redux store 作为整个应用程序的全局状态对象。一个 action 只是一个普通的 JavaScript 对象。我们将定义返回这些 actions 的函数。返回 action 的函数称为 action creator。
在这个教程中,我们将创建加载图库初始图片的 actions。在这个教程中,我们将添加硬编码的数据,但以后,我们将从 API 请求这些数据,以创建更真实的场景。
准备工作
让我们继续在上一个教程中的代码上工作。确保按照这些步骤安装 Redux 并构建我们将在此项目中使用的文件夹结构。
如何做...
- 我们需要为每个 action 定义类型。打开
redux/photos/actions.js
文件。action 类型被定义为常量,以便稍后在 actions 和 reducers 中引用,如下所示:
export const FETCH_PHOTOS = 'FETCH_PHOTOS';
- 现在让我们创建我们的第一个 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
}
]
}
}
}
- 我们将需要为每个我们希望应用程序能够执行的 action 创建一个 action creator,并且我们希望这个应用程序能够添加和删除图片。首先,让我们添加
addBookmark
action creator,如下所示:
export const ADD_PHOTO = 'ADD_PHOTO';
export const addPhoto = (photo) => {
return {
type: ADD_PHOTO,
payload: photo
};
}
- 同样,我们还需要另一个用于删除照片的 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 属性并不是固有特殊的。我们可以将其命名为params
或data
,行为仍然相同。
还有更多...
目前,我们已经定义了动作创建者,它们是简单的返回动作的函数。为了使用它们,我们需要使用 Redux store
提供的dispatch
方法。我们将在后面的配方中了解更多关于 store 的内容。
定义 reducers
到目前为止,我们已经为我们的应用创建了一些动作。正如前面讨论的,动作定义了应该发生的事情,但我们还没有为执行动作创建任何内容。这就是 reducers 的作用。Reducers 是定义动作如何影响 Redux store
中的数据的函数。在 reducer 中访问store
中的数据。
Reducers 接收两个参数:state
和action
。state
参数表示应用的全局状态,action
参数是 reducer 使用的动作对象。Reducers 返回一个新的state
参数,反映了与给定action
参数相关的更改。在这个配方中,我们将介绍一个用于通过在前一个配方中定义的动作来获取照片的 reducer。
准备工作
这个配方依赖于前一个配方定义动作。确保从本章的开头开始,以避免任何问题或混淆。
如何做...
- 让我们从打开
photos/reducer.js
文件开始,并导入我们在前一个配方中定义的所有动作类型,如下所示:
import {
FETCH_PHOTOS,
ADD_PHOTO,
REMOVE_PHOTO
} from './actions';
- 我们将为这个 reducer 中的状态定义一个初始状态对象。它有一个
photos
属性,初始化为一个空数组,用于当前加载的照片,如下所示:
const initialState = () => return {
photos: []
};
- 现在我们可以定义
reducer
函数。它将接收两个参数,当前状态和已经被分发的动作,如下所示:
export default (state = initialState, action) => {
// Defined in next steps
}
React Native 组件也可以有一个state
对象,但这是一个完全独立于 Redux 使用的state
。在这个上下文中,state
指的是存储在 Redux store
中的全局状态。
- 状态是不可变的,所以在 reducer 函数内部,我们需要返回当前动作的新状态,而不是操纵状态,如下所示:
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PHOTOS:
return {
...state,
photos: [...action.payload],
};
// Defined in next steps
}
- 为了将新的书签添加到数组中,我们只需要获取操作的有效负载并将其包含在新数组中。我们可以使用展开运算符在
state
上展开当前的照片数组,然后将action.payload
添加到新数组中,如下所示:
case ADD_PHOTO:
return {
...state,
photos: [...state.photos, action.payload],
};
- 如果我们想从数组中删除一个项目,我们可以使用 filter 方法,如下所示:
case REMOVE_PHOTO:
return {
...state,
photos: state.photos.filter(photo => {
return photo.id !== action.payload.id
})
};
- 最后一步是将我们拥有的所有 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
函数的初始状态。目前,我们只需要一个空数组来存储我们的照片,但我们可以向状态添加其他属性,例如isLoading
和didError
的布尔属性来跟踪加载和错误状态。这些可以反过来用于在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 在应用程序中更常见的用法。
如何做...
- 让我们打开
redux/index.js
文件,并从redux
中导入createStore
函数,如下所示:
import { combineReducers, createStore } from 'redux';
- 创建 store 非常简单;我们只需要调用函数
在步骤 1中导入并将 reducers 作为第一个参数发送,如下所示:
const store = createStore(reducers);
export default store;
- 就是这样!我们已经设置好了 store,现在让我们分发一些 actions。这个食谱中的下一步将从最终项目中删除,因为它们是用来测试我们的设置。让我们首先导入我们想要分发的 action creators:
import {
loadPhotos,
addPhotos,
removePhotos,
} from './photos/actions';
- 在分发任何 actions 之前,让我们订阅 store,这将允许我们监听 store 中发生的任何更改。对于我们当前的目的,我们只需要
console.log
store.getState()
的结果,如下所示:
const unsubscribe = store.subscribe(() => {
console.log(store.getState());
});
- 让我们分发一些 actions,并在开发者控制台中查看结果状态:
store.dispatch(loadPhotos());
- 为了添加一个新的书签,我们需要使用照片对象作为参数来分派
addBookmark
操作创建者:
store.dispatch(addPhoto({
"albumId": 2,
"title": "dolore esse a in eos sed",
"url": `http://placehold.it/600/`,
"thumbnailUrl": `http://placehold.it/150/`
}));
- 要删除一个项目,我们将要删除的照片的
id
传递给操作创建者,因为这是减速器用来查找应该被删除的项目的内容:
store.dispatch(removePhoto({ id: 1 }));
- 执行完所有这些操作后,我们可以通过运行我们在步骤 4中订阅 store 时创建的取消订阅函数来停止监听 store 上的更改,如下所示:
unsubscribe();
- 我们需要将
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
,如果请求失败,则将错误作为payload
的FETCH_PHOTOS_REJECTED
类型的动作。
如何做到...
- 让我们首先将新的中间件添加到我们的 Redux 存储中。在
redux/index.js
文件中,让我们添加 Redux 方法applyMiddleware
。我们还将添加我们刚刚安装的新中间件,如下所示:
import { combineReducers, createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
- 在我们之前定义的
createStore
调用中,我们可以将applyMiddleware
作为第二个参数传递。applyMiddleware
接受一个参数,即我们要使用的中间件promiseMiddleware
:
const store = createStore(reducers, applyMiddleware(promiseMiddleware()));
与其他一些流行的 Redux 中间件解决方案(如redux-thunk
)不同,promiseMiddleware
在传递给applyMiddleware
时必须被调用。它是一个返回中间件的函数。
- 我们现在将在我们的动作中进行真正的 API 请求,因此我们需要将
axios
导入到redux/photos/actions
中。我们还将添加 API 的基本 URL。我们使用的是前几章中使用的相同的虚拟数据 API,托管在jsonplaceholder.typicode.com
,如下所示:
import axios from 'axios';
const API_URL='http://jsonplaceholder.typicode.com';
- 接下来,我们将更新我们的动作创建者。我们将首先更新我们处理 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';
- 与其为这个动作返回虚拟数据作为
payload
,我们将返回一个GET
请求。由于这是一个Promise
,它将触发我们的新中间件。另外,请注意动作的类型是FETCH_PHOTOS
。这将导致中间件自动创建FETCH_PHOTOS_PENDING
,FETCH_PHOTOS_FULFILLED
,如果成功则带有解析数据的payload
,以及FETCH_PHOTOS_REJECTED
,带有发生的错误的payload
,如下所示:
export const fetchPhotos = () => {
return {
type: FETCH_PHOTOS,
payload: axios.get(`${API_URL}/photos?_page=1&_limit=20`)
}
}
- 就像
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';
- action creator 本身将不再只返回传入的照片作为
payload
,而是将通过 API 传递一个POST
请求的 promise 来添加图片,如下所示:
export const addPhoto = (photo) => {
return {
type: ADD_PHOTO,
payload: axios.post(`${API_URL}/photos`, photo)
};
}
- 我们可以按照相同的模式将
REMOVE_PHOTO
动作转换为使用 API 进行删除照片的 AJAX 请求。像ADD_PHOTO
和FETCH_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 }
};
}
- 我们还需要重新审视我们的 reducers 以调整预期的 payload。在
redux/reducers.js
中,我们将首先导入我们将使用的所有动作类型,并更新initialState
。由于在下一个步骤中将会显而易见的原因,让我们将state
对象上的照片数组重命名为loadedPhotos
,如下所示:
import {
FETCH_PHOTOS_FULFILLED,
ADD_PHOTO_FULFILLED,
REMOVE_PHOTO_FULFILLED,
} from './actions';
const initialState = {
loadedPhotos: []
};
- 在 reducer 本身中,更新每个 case 以采用基本动作的
FULFILLED
变体:FETCH_PHOTOS
变为FETCH_PHOTOS_FULFILLED
,ADD_PHOTOS
变为ADD_PHOTOS_FULFILLED
,REMOVE_PHOTOS
变为REMOVE_PHOTOS_FULFILLED
。我们还将更新所有对state
的photos
数组的引用,将其从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中,我们应用了在入门部分安装的中间件。如前所述,这个中间件将允许我们为自动创建PENDING
、FULFILLED
和REJECTED
请求状态的单个动作创建 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
如何做...
- 让我们从将 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';
Provider
的工作是将我们的 Redux 存储器连接到 React Native 应用程序,以便应用程序的组件可以与存储器通信。Provider
应该用于包装整个应用程序,由于此应用程序位于Album
组件中,我们将Album
组件与Provider
组件一起包装。Provider
接受一个store
属性,我们将传入我们的 Redux 存储器。应用程序和存储器已连接:
const App = () => (
<Provider store={store}>
<Album />
</Provider>
);
export default App;
- 让我们转向
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';
- 让我们创建
Album
类,但是,我们不会直接将Album
作为default
导出,而是使用connect
将Album
连接到存储。请注意,connect
使用了两组括号,并且组件被传递到了第二组括号中,如下所示:
class Album extends Component {
}
export default connect()(Album);
- 在调用
connect
时,第一组括号接受两个函数参数:mapStateToProps
和mapDispatchToProps
。我们将首先定义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);
- 同样,
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);
- 现在我们已经将 Redux 存储连接到了我们的组件,让我们创建组件本身。我们可以利用
componentDidMount
生命周期钩子来获取我们的照片,如下所示:
class Album extends Component {
componentDidMount() {
this.props.fetchPhotos();
}
// Defined on later steps
}
- 我们还需要一个添加照片的方法。在这里,我们将使用
randomcolor
包(按照惯例导入为randomColor
)来使用placehold.it服务创建一张图片。生成的颜色字符串带有一个哈希前缀的十六进制值,而图片服务的请求不需要这个前缀,所以我们可以简单地使用replace
调用来移除它。要添加照片,我们只需调用映射到props
的addPhoto
函数,传入新的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);
}
- 我们还需要一个
removePhoto
函数。这个函数所需要做的就是调用已经映射到props
的removePhoto
函数,并传入要删除的照片的 ID,如下所示:
removePhoto = (id) => {
this.props.removePhoto(id);
}
- 应用程序的模板将需要一个
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>
);
}
- 最后,我们将添加样式,以便应用程序具有布局,如下所示。这里没有我们之前没有多次涵盖过的内容:
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'
}
});
- 应用程序已完成!单击“添加照片”按钮将在图像列表的开头添加一个新照片,并按下图像将删除它。请注意,由于我们使用的是虚拟数据 API,因此
POST
和DELETE
请求将返回给定操作的适当响应。但是,实际上并没有向数据库添加或删除任何数据。这意味着如果应用程序被刷新,图像列表将重置,并且如果您尝试使用“添加照片”按钮添加的任何照片,您可以期望出现错误。随时将此应用程序连接到真实的 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,而 mapDispatchToProps
将 action 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,如果不成功,将派发带有错误作为 payload
的 FETCH_PHOTOS_REJECTED
action。在成功时,处理 FETCH_PHOTOS_FULFILLED
的 reducer 中的情况将执行,loadedPhotos
将在存储中更新,进而 this.props.photos
也将被更新。更新组件的 props 将触发重新渲染,并且新数据将显示在屏幕上。
在 步骤 8 和 步骤 9 中,我们遵循相同的模式来定义 addPhoto
和 removePhoto
,它们调用同名的 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
如何做...
- 让我们从
redux/index.js
中添加我们需要的依赖项。我们在这里从redux-persist
导入的storage
方法将使用 React Native 的AsyncStorage
方法在会话之间存储 Redux 数据,如下所示:
import { persistStore, persistReducer } from 'redux-persist'
import storage from 'redux-persist/lib/storage';
- 我们将使用一个简单的
config
对象来配置我们的redux-persist
实例。config
需要一个key
属性来存储数据与AsyncStore
的键,并且需要一个 storage 属性,该属性接受storage
实例,如下所示:
const persistConfig = {
key: 'root',
storage
}
- 我们将使用我们在步骤 1中导入的
persistReducer
方法。这个方法将我们在步骤 2中创建的config
对象作为第一个参数,将我们的 reducers 作为第二个参数:
const reducers = combineReducers({
photos,
});
const persistedReducer = persistReducer(persistConfig, reducers);
- 现在让我们更新我们的存储以使用新的
persistedReducer
方法。还要注意,我们不再将store
作为默认导出,因为我们需要从这个文件中导出两个内容:
export const store = createStore(persistedReducer, applyMiddleware(promiseMiddleware()));
- 我们从这个文件中需要的第二个导出是
persistor
。persistor
将在会话之间持久化 Redux 存储。我们可以通过调用persistStore
方法并传入store
来创建persistor
,如下所示:
export const persistor = persistStore(store);
- 现在我们从
redux/index.js
中得到了store
和persistor
作为导出,我们准备在App.js
中应用它们。我们将从中导入它们,并从redux-persist
中导入PersistGate
组件。PersistGate
将确保我们缓存的 Redux 存储在任何组件加载之前加载:
import { PersistGate } from 'redux-persist/integration/react'
import { store, persistor } from './redux';
- 让我们更新
App
组件以使用PersistGate
。该组件接受两个属性:导入的persistor
属性和一个loading
属性。我们将向loading
属性传递null
,但如果我们有一个加载指示器组件,我们可以将其传递进去,PersistGate
会在数据恢复时显示这个加载指示器,如下所示:
const App = () => (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<Album />
</PersistGate>
</Provider>
);
- 为了测试我们的 Redux 存储的持久性,让我们调整
Album
组件中的componentDidMount
方法。我们将延迟调用fetchPhotos
两秒钟,这样我们就可以在从 API 再次获取数据之前看到保存的数据,如下所示:
componentDidMount() {
setTimeout(() => {
this.props.fetchPhotos();
}, 2000);
}
根据您要持久化的数据类型,这种功能可以应用于许多情况,包括持久化用户数据和应用状态,甚至在应用关闭后。它还可以用于改善应用的离线体验,如果无法立即进行 API 请求,则缓存 API 请求,并为用户提供填充数据的视图。
工作原理...
在步骤 2中,我们创建了用于配置redux-persist
的配置对象。该对象只需要具有key
和store
属性,但也支持其他许多属性。您可以通过此处托管的类型定义查看此配置接受的所有选项: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 提供的大量有用功能,例如BarcodeScanner
、MapView
、ImagePicker
等等。
使用 React Native CLI 通过以下命令初始化应用程序:
react-native init
这提供了灵活性,但开发的难度也相应增加。
react-native init
据说这是一个纯 React Native 应用程序,因为没有任何原生代码对开发人员隐藏。
作为一个经验法则,如果使用第三方包的设置需要运行以下命令,则需要一个纯 React Native 应用程序:
react-native link
那么,当您在使用 Expo 构建应用程序时,却发现一个对应用程序要求至关重要的包不受 Expo 开发工作流支持时,该怎么办?幸运的是,Expo 有一种方法可以将 Expo 项目转换为纯 React Native 应用程序,就好像是用以下命令创建的一样:
expo eject
当项目被弹出时,所有的原生代码都被解压到ios
和android
文件夹中,App.js
文件被拆分为App.js
和index.js
,暴露出挂载根 React Native 组件的代码。
但是,如果您的 Expo 应用依赖于 Expo SDK 提供的功能呢?毕竟,使用 Expo 开发的价值很大程度上来自于 Expo 提供的出色功能,包括AuthSession
、Permissions
、WebBrowser
等。
这就是 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。
您可能还记得第一章中设置您的环境,其中一些步骤已经详细介绍了。无需重新安装已在那里描述的任何列在此处的内容。
- 首先,我们将安装所有与纯 React Native 应用程序一起工作所需的依赖项,从 macOS 的 Homebrew(
brew.sh/
)软件包管理器开始。如项目主页上所述,Homebrew 可以通过以下命令轻松从终端安装:
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
- 安装 Homebrew 后,可以使用它来安装 React Native 开发所需的依赖项:Node.js 和
nodemon
。如果您是 JavaScript 开发人员,您可能已经安装了 Node.js。您可以通过以下命令检查它是否已安装:
node -v
这个命令将列出已安装的 Node.js 的版本。请注意,您需要 Node.js 的 8 版本或更高版本来进行 React Native 开发。如果 Node.js 尚未安装,您可以通过以下命令使用 Homebrew 安装它:
brew install node
- 我们还需要
nodemon
包,React Native 在幕后使用它来启用开发过程中的实时重新加载等功能。通过以下命令使用 Homebrew 安装nodemon
:
brew install watchman
- 当然,我们还需要 React Native CLI 来运行引导 React Native 应用程序的命令。可以通过以下命令全局安装它:
npm install -g react-native-cli
- 安装了 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 菜单中选择“首选项”,打开位置面板,并从命令行工具下拉菜单中安装最新版本:
- 很遗憾,为了支持 Android React Native 开发设置 Android Studio 并不是一件轻而易举的事情,需要一些非常具体的步骤来安装它。由于这个过程特别复杂,并且有可能在你阅读本章时已经发生了变化,我建议参考官方文档,获取安装所有 Android 开发依赖的深入和最新的说明。这些说明托管在以下 URL:
facebook.github.io/react-native/docs/getting-started.html#java-development-kit
- 现在所有的依赖都已经安装好了,我们可以通过命令行运行我们的纯 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 制作的应用程序中,这个文件会被隐藏起来,就像ios
和android
文件夹一样,如下所示:
import { AppRegistry } from 'react-native';
import App from './App';
AppRegistry.registerComponent('NativeBase', () => App);
这只是在运行时为您的 React Native 应用程序提供引导过程。AppRegistry
从react-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 就像使用npm
或yarn
安装所需的依赖项一样简单。首先,我们可以在命令行上使用 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
如何做到这一点...
- 我们将首先在
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';
- 接下来,让我们声明
App
类并定义一个起始的state
对象。我们将添加一个 FAB 部分,以展示 NativeBase 如何让您轻松地向应用程序添加弹出菜单按钮。我们将使用fabActive
布尔值来跟踪是否应该显示此菜单。稍后在render
方法中,我们还将使用loading
布尔值,如下所示:
export default class App extends Component {
state = {
loading: true
fabActive: false
}
// Defined on following steps
}
- 您可能还记得在本示例的准备工作部分中,如果您正在使用 Expo 开发应用程序,NativeBase 建议通过
Expo.Font.loadAsync
函数加载 NativeBase 使用的字体。在componentWillMount
方法中,我们将初始化并等待require
字体的加载,然后将state
上的loading
属性设置为false
。loading
属性将在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
}
- 由于这个应用程序主要是 UI,我们准备开始构建
render
函数。为了确保在使用字体之前加载它们,如果state
的loading
属性为 true,我们将返回 App 占位符 Expo 组件AppLoading
,否则我们将渲染 App UI。AppLoading
将指示应用程序继续显示应用程序的启动画面,直到组件被移除。
如果您选择使用纯 React Native 项目开始此示例,您将无法访问 Expo 组件。在这种情况下,您可以简单地返回一个空的View
而不是AppLoading
。
- 我们将从
Container
组件开始,以及Header
、Body
和Title
辅助组件。这将作为页面的容器,显示页面顶部带有标题“Header Title”的标题!
render() {
if (this.state.loading) {
return <AppLoading />;
} else {
return (
<Container>
<Header>
<Body>
<Title>Header Title!</Title>
</Body>
</Header>
</Container>
);
}
}
此时,应用程序应该类似于以下屏幕截图:
- 在以下代码中,
Header
将具有来自 NativeBase 的一些其他 UI 元素。Spinner
组件允许轻松显示带有传递的所需颜色的加载旋转器。与原始的TouchableOpacity
组件相比,Button
组件提供了更多的内置可定制性。在这里,我们使用block
属性将按钮扩展到其容器,并在每个按钮上使用info
和success
属性来应用它们各自的默认蓝色和绿色背景颜色:
<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>
- 前面的渲染函数还引用了我们尚未定义的
renderFab
方法。这利用了Icon
和Fab
组件。 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>
);
}
- 让我们用一些样式来完善这个配方,以便在
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
}
});
- 回顾已完成的应用程序,现在有一个漂亮的跨平台 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 简史:我们是如何到达这里以及我们将去哪里》的文章,托管在:
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
如何做...
- 让我们首先在
App.js
中导入我们需要的所有依赖项,如下所示:
import React from 'react';
import glamorous from 'glamorous-native';
- 我们的应用程序将需要一个包含
View
元素,以容纳应用程序中显示的所有其他组件。我们将使用glamorous
来为此元素传递样式对象,而不是像我们在所有先前的示例中所做的那样,通过传递给StyleSheet
组件的对象来对此元素进行样式化。我们将使用view
方法,它返回一个样式化的View
组件,我们将其存储在一个名为Container
的const
中,以便以后使用,如下所示:
const Container = glamorous.view({
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
});
- 同样地,我们将使用
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'
});
- 我们还将使用
glamorous.touchableHighlight
方法制作一个可重用的Button
组件。这种方法展示了glamorous
组件也可以用不同类型的多个样式声明来创建。在这种情况下,传递给touchableHighlight
的第二个参数是一个函数,根据元素上定义的props
来更新backgroundColor
样式,如下所示:
const Button = glamorous.touchableHighlight(
{ padding: 10 },
props => ({backgroundColor: props.warning ? 'red' : 'blue'})
);
- 我们还可以创建内联样式的组件,这要归功于
glamorous
提供的特殊版本的 React Native 组件。我们将使用一个Image
组件,但是不是从react-native
中导入,而是从导入的glamorous
包中使用Image
组件,如下所示:
const { Image } = glamorous;
- 现在,我们准备声明
App
组件。App
只需要一个render
函数来渲染我们所有新样式化的组件,如下所示:
export default class App extends React.Component {
render() {
// Defined in following steps.
}
}
- 让我们开始构建
render
函数,通过添加在步骤 2中创建的Container
组件。代码可读性的改进已经显而易见。Container
被明确定义,并且不需要其他属性或属性来声明样式,如下所示:
render() {
return (
<Container>
// Defined on following steps
</Container>
);
}
- 让我们添加从导入的
glamorous
库中提取的Image
组件,这是在步骤 5中完成的。请注意,我们能够直接在组件上声明样式属性,如height
、width
和borderRadius
,而不是像普通的Image
组件那样:
<Container>
<Image
height={250}
width={250}
borderRadius={20}
source={{ uri: 'http://placehold.it/250/3B5998' }}
/>
// Defined on following steps
</Container>
- 现在,我们将添加在步骤 3中创建的
Headline
和Subheading
组件。就像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>
- 最后,我们将添加在步骤 4中创建的
Button
组件,以及在步骤 3中创建的ButtonText
组件。两个按钮都有一个onPress
方法,就像任何TouchableOpacity
或TouchableHighlight
组件一样,但第二个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>
- 所有我们的
glamorous
组件都已添加到render
方法中。如果你运行这个应用程序,你应该会看到一个完全样式化的用户界面。
它是如何工作的...
在步骤 2和步骤 3中,我们使用相应的glamorous
方法创建了带有样式的View
和Text
组件,并传入了一个包含应该应用于该特定组件的所有样式的对象。
在步骤 4中,我们通过应用与前几步创建View
和Text
组件相同的方法,创建了一个可重用的Button
样式组件。然而,这个组件中声明样式的方式是不同的,并展示了glamorous-native
在处理样式时的多功能性。您可以将任意数量的样式集合作为参数传递给glamorous
组件构造函数,它们都将被应用。这包括动态样式,通常采用在组件上定义的 props 来应用不同的样式。在步骤 10中,我们使用了我们的Button
元素。如果存在warning
属性,就像在render
中的第一个Button
上一样,backgroundColor
将是red
。否则,它将是blue
。这为在多种类型的组件上应用简单和可重用的主题提供了一个非常好的系统。
在步骤 5中,我们从glamorous
库中提取了Image
组件,以替代 React Native 的Image
组件。这个特殊版本的组件与其 React Native 对应组件的行为相同,同时还能够直接对元素本身应用样式。在步骤 8中,我们使用了该组件,我们能够应用height
、width
和borderRadius
样式,而无需使用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
如何做到这一点...
- 我们将首先在项目的根目录的
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';
- 在这个配方中,我们将设置应用程序循环显示
react-native-spinkit
提供的所有加载旋转器类型。为此,让我们创建一个包含每种可能的旋转器类型的字符串数组。由于最后四种类型在 Android 中不完全受支持,它们在 Android 上都将显示为相同的Plane
旋转器,如下所示:
const types = [
'Bounce',
'Wave',
'WanderingCubes',
'Pulse',
'ChasingDots',
'ThreeBounce',
'Circle',
'9CubeGrid',
'FadingCircleAlt',
'FadingCircle',
'CircleFlip',
'WordPress',
'Arc',
'ArcAlt'
];
- 现在,我们可以开始构建
App
组件。我们将需要一个具有四个属性的state
对象:一个isVisible
属性来跟踪是否应该显示旋转器,一个用于保存当前旋转器类型的type
属性,一个用于保持在types
数组中的位置的typeIndex
,以及一个颜色。我们将通过简单调用randomColor()
来将颜色初始化为随机十六进制代码,如下所示:
export default class App extends Component {
state = {
isVisible: true,
typeIndex: 0,
type: types[0],
color: randomColor()
}
}
- 我们将需要一个函数来改变
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]
});
}
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>
);
}
- 最后,让我们为中心内容添加一些样式,并通过
text
类增加Text
元素的字体大小,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
text: {
paddingTop: 40,
fontSize: 25
}
});
- 完成这个示例后,我们应该看到一个在按下时改变的加载器。感谢
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
。
我们还需要使用npm
将react-native-side-menu
安装到我们的项目中。
npm install react-native-side-menu --save
或者,使用yarn
:
yarn add react-native-side-menu
如何做...
- 让我们从在项目根目录的
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';
- 接下来,让我们定义
App
类和初始的state
。在这个应用程序中,state
只需要两个属性:一个isOpen
布尔值,用于跟踪侧边菜单何时应该打开,以及一个selectedBackgroundColor
属性,其值是表示当前选定的背景颜色的字符串,如下所示:
export default class App extends React.Component {
state = {
isOpen: false,
selectedBackgroundColor: 'green'
}
// Defined in following steps
}
- 我们的应用程序将需要一个方法来改变
state
上的selectedBackgroundColor
属性。这个方法以一个color
字符串作为参数,并将该颜色设置为selectedBackgroundColor
。它还会将state.isOpen
设置为false
,以便在从菜单中选择颜色时关闭侧边菜单,如下所示:
changeBackgroundColor = color => {
this.setState({
isOpen: false,
selectedBackgroundColor: color,
});
}
- 我们准备好定义
App
的render
方法。首先,让我们设置Menu
组件,以便在下一步中可以被SideMenu
使用。我们还没有创建Menu
组件,但我们将使用onColorSelected
属性来传递changeBackgroundColor
方法,如下所示:
render() {
const menu = <Menu onColorSelected={this.changeBackgroundColor}
/>;
// Defined in next step
}
- 渲染的 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>
);
}
- 作为这个组件的最后一步,让我们添加基本样式来居中布局,并应用颜色和字体大小,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
backgroundColor: 'black',
padding: 20,
borderRadius: 10
},
buttonText: {
color: 'white',
fontSize: 25
}
});
- 是时候创建
Menu
组件了。让我们在component
文件夹中创建一个Menu.js
文件。我们将从组件导入开始。就像我们在之前的示例中所做的那样,我们还将使用Dimensions
将应用程序窗口的尺寸存储在一个变量中,以便应用样式,如下所示:
import React from 'react';
import {
Dimensions,
StyleSheet,
View,
Text,
TouchableOpacity
} from 'react-native';
const window = Dimensions.get('window');
Menu
组件只需要是一个展示性组件,因为它没有状态或生命周期钩子的需求。该组件将接收onColorSelected
作为属性,我们将在下一步中使用它,如下所示:
const Menu = ({ onColorSelected }) => {
return (
// Defined on next step
);
}
export default Menu;
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>
- 让我们为
Menu
组件添加一些样式,应用颜色和字体大小。请注意,我们还在步骤 7中定义的window
变量来设置组件的height
和width
,使其等于屏幕的大小,如下所示:
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'
}
});
- 我们的应用程序已经完成!当按下“打开菜单”按钮时,一个平滑动画的侧边菜单将从左侧滑出,显示一个供用户选择的颜色列表。当从列表中选择颜色时,应用程序的背景颜色会更改,并且菜单会滑动回关闭状态:
工作原理...
在步骤 4中,我们为主App
组件创建了render
函数。我们将Menu
组件存储在menu
变量中,以便可以清晰地将其传递给SideMenu
的menu
属性,就像我们在步骤 5中所做的那样。我们通过onColorSelected
属性将changeBackgroundColor
类方法传递给我们的Menu
组件,以便我们可以使用它来正确更新App
组件中的state
。
然后,我们将Menu
组件作为menu
属性传递给SideMenu
,将这两个组件连接在一起。第二个属性是isOpen
,它决定侧边菜单是否应该打开。第三个属性onChange
接受一个回调函数,每次菜单打开或关闭时都会执行。onChange
回调提供了一个isOpen
参数,我们用它来更新state
中isOpen
的值,以便保持同步。
包含的View
元素具有一个style
属性,设置为一个数组,其中包含步骤 6中定义的container
样式和一个具有backgroundColor
键设置为state
中的selectedBackgroundColor
的对象。这将导致View
组件的背景颜色在更新时更改为此值。
在步骤 8和步骤 9中,我们构建了Menu
组件的render
方法。每个TouchableOpacity
按钮都连接到onColorSelected
,传入与按下按钮相关联的颜色。这反过来在父App
类中运行changeBackgroundColor
,在设置state.isOpen
为false
时更新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
如何做...
- 让我们从在项目的根目录中打开
App.js
文件并添加导入开始,如下所示:
import React from 'react';
import Modal from 'react-native-modalbox';
import {
Text,
StyleSheet,
View,
TouchableOpacity
} from 'react-native';
- 接下来,我们将定义和导出
App
组件,以及初始的state
对象,如下所示。对于这个应用程序,我们只需要一个isOpen
布尔值来跟踪我们的模态框是否应该打开或关闭:
export default class App extends Component {
state = {
isOpen: false
};
// Defined on following steps
}
- 让我们跳到下一个构建
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>
);
}
- 现在,我们准备定义
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>
)
}
- 让我们接下来添加
openModal1
方法。这个方法是在步骤 3中我们在render
方法中添加的第一个TouchableOpacity
组件上通过onPress
调用的。通过将modal1
字符串传递给我们在步骤 4中定义的Modal
组件上的ref
属性,我们能够将模态框访问为this.refs.modal1
。在这个引用上调用open
方法将打开模态框。关于这一点,我们将在本教程末尾的它是如何工作的...部分详细介绍。添加openModal1
方法如下:
openModal1 = () => {
this.refs.modal1.open();
}
- 我们在步骤 4中定义的
Modal
还有onClosed
和onOpened
属性,它们分别接受一个在模态框关闭或打开时执行的回调函数。让我们接下来为这些属性定义回调函数。在本教程中,我们将只是简单地使用console.log
作为概念验证,如下所示:
onClose = () => {
console.log('modal is closed');
}
onOpen = () => {
console.log('modal is open');
}
- 我们准备好定义第二个模态框了。这个
Modal
组件的ref
属性将设置为字符串modal2
,我们将添加两个其他可选的属性,这些属性在另一个模态框上没有使用。第一个是position
,可以设置为top
、bottom
或center
(默认)。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>
)
}
- 由于我们使用
state
布尔值isOpen
来操纵模态框的状态,openModal2
方法将演示另一种打开和关闭模态框的方法。通过将state
上的isOpen
设置为true
,第二个模态框将打开,如下所示:
openModal2 = () => {
this.setState({ isOpen: true });
}
- 您可能还注意到,在步骤 7中定义的第二个模态框有一个不同的
onClosed
回调。如果用户按下 OK 按钮,state
上的isOpen
值将成功更新为false
,但如果他们通过触摸背景来关闭模态框,它将不会。添加onCloseModal2
方法可以确保state
的isOpen
值无论用户如何关闭模态框都能正确保持同步,如下所示:
onCloseModal2 = () => {
this.setState({ isOpen: false });
}
- 这个教程的最后一步是应用样式。我们将有一个用于共享模态框样式的
modal
类,用于每个模态框独特样式的modal1
和modal2
类,以及用于将颜色、填充和边距应用于按钮和文本的类,如下所示:
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'
}
});
- 这个教程已经完成,我们现在有一个应用程序,其中有两个基本的模态框,通过按钮按下显示,并在同一个组件中和谐共存:
工作原理...
在步骤 4中,我们定义了第一个Modal
组件。我们定义了onClosed
和onOpened
属性,将onClose
和onOpen
类方法传递给这些属性。每当打开这个Modal
组件时,this.onOpen
都会触发,当Modal
关闭时,this.onClose
会执行。虽然在这个示例中我们没有对这些方法做任何激动人心的事情,但这些钩子可以作为记录与模态框相关的用户操作的绝佳机会。或者,如果模态框包含一个表单,onOpen
可以用来预先填充一些表单输入数据,而onClose
可以将表单数据保存到state
对象中,以便在关闭模态框时使用。
在步骤 5中,我们定义了第一个TouchableOpacity
按钮组件在按下时执行的方法:openModal1
。在这个方法中,我们利用了Modal
组件的引用。引用是 React 本身的一个核心特性,它为组件实例提供了一个存储在组件渲染方法中创建的 DOM 节点和/或 React 元素的位置。就像 React(和 React Native)组件有状态和属性(在类组件中为this.state
和this.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中,我们为保持state
的isOpen
值同步定义了一个单独的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
如何做到...
-
我们将从在 Xcode 中打开 iOS 项目开始。项目文件的文件扩展名为
.xcodeproj
,位于项目根目录的ios/
目录中。在我们的情况下,文件将被称为NativeModuleApp.xcodeproj
。 -
我们需要通过选择并右键单击与项目名称匹配的组/文件夹来创建一个新文件,然后点击 New File...,如下所示:
-
我们将制作一个 Cocoa 类,所以选择 Cocoa Class 并点击 Next。
-
我们将使用
HelloManager
作为类名,并将子类设置为 NSObject,语言设置为 Objective-C,如下所示:
-
点击 Next 后,我们将被提示选择新类的目录。我们希望将其保存到
NativeModuleApp
目录中。 -
创建这个新的 Cocoa 类已经向项目中添加了两个新文件:一个头文件(
HelloManager.h
)和一个实现文件(HelloManager.m
)。 -
在头文件(
HelloManager.h
)中,您应该看到一些生成的代码来实现新的HelloManager
协议。我们还需要导入 React 的RCTBridgeModule
库。文件最终应该看起来像这样:
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface HelloManager : NSObject <RCTBridgeModule>
@end
- 实现文件(
HelloManager.m
)包含了我们模块的功能。为了让我们的 React Native 应用能够从 JavaScript 层访问这个模块,我们需要在 React Bridge 中注册它。这是通过在@implementation
标签后添加RCT_EXPORT_MODULE()
来完成的。还要注意,头文件也应该已经被导入到这个文件中:
#import "HelloManager.h"
@implementation HelloManager
RCT_EXPORT_MODULE();
@end
- 我们需要添加我们将要导出到 React Native 应用的函数。我们将创建一个
greetUser
方法,它将接受两个参数,name
和isAdmin
。这些参数将用于使用字符串连接创建问候消息,然后通过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
- 我们准备切换到 JavaScript 层,这将有一个 UI,将调用我们刚刚创建的原生
HelloManager greetUser
方法,然后显示其输出。幸运的是,React Native 桥为我们完成了所有繁重的工作,并留下了一个简单易用的 JavaScript 对象,模仿了NativeModules
API。在这个例子中,我们将使用TextInput
和Switch
来为原生模块方法提供name
和isAdmin
值。让我们从App.js
中开始导入:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
NativeModules,
TextInput,
Switch
} from 'react-native';
import Button from 'react-native-button';
- 我们可以使用我们导入的
NativeModules
组件来从原生层获取我们创建的HelloManager
协议:
const HelloManager = NativeModules.HelloManager;
- 让我们创建
App
组件并定义初始的state
对象。我们将添加一个greetingMessage
属性来保存从原生模块接收到的消息,userName
来存储输入的用户名,以及一个isAdmin
布尔值来表示用户是否是管理员:
export default class App extends Component {
state = {
greetingMessage: null,
userName: null,
isAdmin: false
}
// Defined on following steps
}
- 我们准备开始构建
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>
);
}
- 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>
);
}
- 随着 UI 渲染必要的组件,我们准备将
Button
的onPress
处理程序连接到本地层的调用。这个函数将displayResults
类方法作为第三个参数传递,这是本地greetUser
函数要使用的回调。我们将在下一步中定义displayResults
:
greetUser = () => {
HelloManager.greetUser(
this.state.userName,
this.state.isAdmin,
this.displayResults
);
}
displayResults
需要做两件事:使用与组件关联的refs
来blur
TextInput
,并将greetingMessage
设置为从本地模块返回的results
:
displayResults = (results) => {
this.refs.userName.blur();
this.setState({ greetingMessage: results });
}
- 最后一步是向布局添加样式并设计应用程序的样式:
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'
}
});
- 我们现在有一个可以直接与本地 iOS 层通信的工作 React Native 应用程序:
工作原理...
我们在这个教程中构建的应用程序将成为本章后续许多教程的基础。这也是 Facebook 用来实现许多捆绑的 React Native API 的方法。
在未来,有几个重要的概念需要牢记。我们想要在 JavaScript 层中使用的任何本地模块类都必须扩展RCTBridgeModule
,因为它包含了将我们的类注册到 React Native 桥上的功能。我们使用RCT_EXPORT_MODULE
方法调用注册我们的类,一旦模块被注册,就会注册模块上的方法。注册模块以及其相应的方法和属性是允许我们从 JavaScript 层与本地层进行接口的。
当按下按钮时,将执行greetUser
方法。这个函数反过来调用HelloManager.greetUser
,传递state
中的userName
和isAdmin
属性以及displayResults
函数作为回调。displayResults
设置state
上的新greetingMessage
,导致 UI 刷新并显示消息。
另请参阅
-
本文解释了 React Native 应用程序如何启动:
levelup.gitconnected.com/wait-what-happens-when-my-react-native-application-starts-an-in-depth-look-inside-react-native-5f306ef3250f
-
深入了解 React Native 事件的实际工作原理:
levelup.gitconnected.com/react-native-events-in-gory-details-what-happens-on-the-way-to-listeners-2cee6c55940c
渲染自定义 iOS 视图组件
在我们的 React Native 应用程序中,在本地层执行代码时利用设备的处理能力非常重要,同样重要的是利用其渲染能力来显示本地 UI 组件。 React Native 可以在应用程序中呈现任何UIView
的 UI 组件实现。 这些组件可以是列表、表单字段、表格、图形等等。
对于这个教程,我们将创建一个名为NativeUIComponent
的 React Native 应用程序。
在这个教程中,我们将采用原生的UIButton
并将其公开为 React Native 视图组件。 您将能够设置按钮标签并附加一个处理程序以在按下按钮时执行。
如何做...
-
让我们从在 Xcode 中打开 iOS 项目开始。 项目文件位于项目的
ios/
目录中,应该被称为NativeUIComponent.xcodeproj
。 -
选择并右键单击与项目名称匹配的组,并单击“新建文件...”:
-
我们将创建一个 Cocoa 类,所以选择
Cocoa Class
并单击下一步
。 -
我们将创建一个按钮,所以让我们将类命名为
Button
,将Subclass of
设置为UIView
,将Language
设置为Objective-C
:
-
点击“下一步”后,我们将被提示选择新类的目录。 我们要将其保存到
NativeUIComponent
目录以创建该类。 -
我们还需要一个
ButtonViewManager
类。 您可以将步骤 2 到 5 重复使用ButtonViewManager
作为类名和RCTViewManager
作为子类。 -
首先,我们将实现我们的
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
- 让我们在实现文件(
Button.m
)上工作。 我们将首先创建我们的UIButton
实例和将保存按钮标签的字符串的引用:
#import "Button.h"
#import "React/UIView+React.h"
@implementation Button {
UIButton *_button;
NSString *_buttonText;
}
// Defined in following steps
- 桥梁将寻找
buttonText
属性的 setter。 这是我们将设置UIButton
实例标题字段的地方:
-(void) setButtonText:(NSString *)buttonText {
NSLog(@"Set text %@", buttonText);
_buttonText = buttonText;
if(_button) {
[_button setTitle:
buttonText forState:UIControlStateNormal];
[_button sizeToFit];
}
}
- 我们的
Button
将从 React Native 应用程序接受一个onTap
事件处理程序。我们需要通过动作选择器将其连接到我们的UIButton
实例:
- (IBAction)onButtonTap:(id)sender {
self.onTap(@{});
}
- 我们需要实例化
UIButton
并将其放置在 ReactSubview
中。我们将称这个方法为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];
}
}
- 让我们在
ButtonViewManager.h
头文件中导入 ReactRCTViewManager
:
#import "React/RCTViewManager.h"
@interface ButtonViewManager : RCTViewManager
@end
- 现在我们需要实现我们的
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
- 我们准备切换到 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';
Button
组件将通过requireNativeComponent
React Native 助手获取我们之前创建的原生Button
模块。调用以字符串作为组件在 React Native 层中的名称作为第一个参数,并且第二个参数将Button
组件在文件中,有效地将两者连接在一起:
export default class Button extends Component {
render() {
return <ButtonView {...this.properties} />;
}
}
const ButtonView = requireNativeComponent('ButtonView', Button);
- 我们准备在项目的根目录下的
App.js
文件中构建主要的App
组件。我们将从导入开始,其中将包括我们在最后两个步骤中创建的Button
组件:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View
} from 'react-native';
import Button from './components/Button';
- 让我们定义
App
组件和初始的state
对象。count
属性将跟踪Button
组件被按下的次数:
export default class App extends Component {
state = {
count: 0
}
// Defined on following steps
}
- 我们准备好定义
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>
);
}
- 您可能还记得我们创建的
Button
组件具有一个onTap
属性,它接受一个回调函数。在这种情况下,我们将使用此函数来增加state
上的计数器:
handleButtonTap = () => {
this.setState({
count: this.state.count + 1
});
}
- 让我们用一些基本的样式结束这个教程:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
button: {
height: 40,
width: 80
}
});
- 应用程序完成了!当按下按钮时,将执行传递给
onTap
的函数,将计数器增加一:
工作原理...
在这个配方中,我们暴露了一个基本的原生 UI 组件。这是创建 React Native 内置的所有 UI 组件(例如Slider
、Picker
和ListView
)的方法。
创建 UI 组件最重要的要求是,你的ViewManager
要扩展RCTViewManager
并返回一个UIView
的实例。在我们的情况下,我们用 React 特定的UIView
扩展来包装UIButton
,这样可以提高我们布局和样式组件的能力。
下一个重要因素是发送属性和对组件事件做出反应。在第 13 步中,我们使用了 React Native 提供的RCT_EXPORT_VIEW_PROPERTY
方法来注册来自 JavaScript 层的buttonText
和onTap
视图属性,这些属性将传递给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
如何做...
-
我们将首先在 Android Studio 中打开新项目的 Android 代码。从 Android Studio 的欢迎屏幕,你可以选择打开现有的 Android Studio 项目,然后选择项目文件夹内的
android
目录。 -
项目加载完成后,让我们在 Android Studio 左侧打开项目资源管理器(即目录树),并展开包结构以找到 Java 源文件,它应该位于
app/java/com.nativemoduleapp
中。该文件夹应该已经有两个.java
文件,MainActivity
和MainApplication
:
-
右键单击 com.nativemoduleapp 包,选择 New | Java Class,并命名类为
HelloManager
。还要确保将 Kind 字段设置为 Class: -
我们还需要在同一个目录中创建一个
HelloPackage
类。您可以重复步骤 2 和 3 来创建这个类,只需应用新名称并保持 Kind 字段设置为 Class。 -
让我们从实现我们的
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;
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";
}
}
- 现在我们已经设置好了
HelloManager
本机模块,是时候向其中添加greetUser
方法了,该方法将期望作为参数name
、isAdmin
和将执行以将消息发送到 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);
}
}
- 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;
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;
}
}
- 在将本机模块注册到 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);
}
}
- 我们在这个配方的 Java 部分已经完成了。 我们需要构建我们的 UI,这将调用本机的
HelloManager greetUser
方法并显示其输出。 在这个例子中,我们将使用TextInput
和Switch
来提供本机模块方法的name
和isAdmin
值。 这与我们在暴露自定义 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';
- 我们需要引用存储在导入的
NativeModules
组件上的HelloManager
对象:
const { HelloManager } = NativeModules;
- 让我们创建
App
类和初始state
:
export default class App extends Component {
state = {
userName: null,
greetingMessage: null,
isAdmin: false
}
}
- 我们准备定义组件的
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>
);
}
- 随着 UI 渲染必要的组件,我们现在需要将
Button
的onPress
处理程序连接起来,通过HelloManager.greetUser
进行本机调用:
updateGreetingMessage = (result) => {
this.setState({
greetingMessage: result
});
}
greetUser = () => {
this.refs.userName.blur();
HelloManager.greetUser(
this.state.userName,
this.state.isAdmin,
this.updateGreetingMessage
);
}
- 我们将添加样式来布局和设计应用程序。 再次强调,这些样式与本章开头的暴露自定义 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'
}
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
这个配方涵盖了我们将在未来的配方中添加本机 Android 模块的大部分基础知识。 所有本机模块类都需要扩展ReactContextBaseJavaModule
,实现构造函数,并定义getName
方法。 所有应该暴露给 React Native 层的方法都需要有@ReactMethod
注解。 创建 React Native Android 本机模块的开销比 iOS 更大,因为您还必须将模块包装在实现ReactPackage
的类中(在这个配方中,那就是HelloPackage
模块),并将包注册到 React Native 项目中。 这是在步骤 7 和 8 中完成的。
在配方的 JavaScript 部分,当用户按下Button
组件时,将执行greetUser
函数。 这反过来又调用HelloManager.greetUser
,并传递state
中的userName
和isAdmin
属性以及updateGreetingMessage
方法作为回调。 updateGreetingMessage
在state
上设置新的greetingMessage
,导致 UI 刷新并显示消息。
渲染自定义 Android 视图组件
迄今为止,React Native 之所以如此受欢迎的一个原因是它能够渲染真正的本机 UI 组件。在 Android 上使用本机 UI 组件,我们不仅能够利用 GPU 渲染能力,还能获得本机组件的本机外观和感觉,包括本机字体、颜色和动画。在 Android 上,Web 和混合应用程序使用 CSS polyfills 来模拟本机动画,但在 React Native 中,我们可以得到真正的东西。
我们需要一个新的纯 React Native 应用程序来完成这个示例。让我们将其命名为NativeUIComponent
。在这个示例中,我们将采用本机Button
并将其公开为 React Native 视图组件。
如何做...
-
让我们从在 Android Studio 中打开 Android 项目开始。在 Android Studio 欢迎屏幕上,选择打开现有的 Android Studio 项目,并打开项目的
android
目录。 -
打开项目资源管理器,并展开包结构,直到您可以看到 Java 源文件(例如,
app/java/com.nativeuicomponent
):
-
右键单击包,然后选择 New | Java Class。使用
ButtonViewManager
作为类名,并将 Kind 字段设置为 Class。 -
使用相同的方法也创建一个
ButtonPackage
类。 -
让我们开始实现我们的
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
组件。
- 让我们从
getName
方法开始类定义,该方法返回我们为组件分配的字符串名称,在本例中为ButtonView
:
public class ButtonViewManager extends SimpleViewManager<Button> implements View.OnClickListener{
@Override
public String getName() {
return "ButtonView";
}
// Defined on following steps.
}
createViewInstance
方法是必需的,用于定义 React 应该如何初始化模块:
@Override
protected Button createViewInstance(ThemedReactContext reactContext) {
Button button = new Button(reactContext);
button.setOnClickListener(this);
return button;
}
setButtonText
将从 React Native 元素的属性中使用,以设置按钮上的文本:
@ReactProp(name = "buttonText")
public void setButtonText(Button button, String buttonText) {
button.setText(buttonText);
}
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);
}
- 就像在上一个示例中一样,我们需要将
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();
}
}
- Java 层的最后一步是将
ButtonPackage
添加到MainApplication
。MainApplication.java
中已经有相当多的样板代码,我们只需要更改getPackages
方法:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new ButtonPackage()
);
}
- 切换到 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}
/>
);
}
}
- 我们可以使用
requireNativeComponent
助手将原生按钮创建为 React Native 组件,它接受三个参数:字符串ButtonView
来定义组件名称,上一步中定义的Button
组件,以及选项对象。有关此对象的更多信息,请参阅本教程末尾的它是如何工作的...部分:
const ButtonView = requireNativeComponent(
'ButtonView',
Button, {
nativeOnly: {
onChange: true
}
}
);
- 我们准备好定义
App
类。让我们从依赖项开始,包括先前创建的Button
组件:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View
} from 'react-native';
import Button from './components/Button';
- 本教程中的
App
组件本质上与本章前面的渲染自定义 iOS 视图组件教程相同。当按下Button
组件时,自定义的onTap
属性被触发,将1
添加到state
的count
属性中。
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>
);
}
}
- 让我们为布局添加一些样式,调整应用的 UI 大小:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
button: {
height: 40,
width: 150
}
});
- 最终的应用程序应该类似于以下截图:
它是如何工作的...
当定义原生视图时,就像我们在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
选项调用它们。
第十二章:添加本机功能-第二部分
在本章中,我们将涵盖以下食谱:
-
对应用程序状态变化做出反应
-
复制和粘贴内容
-
通过触摸 ID 或指纹传感器进行身份验证
-
在多任务处理时隐藏应用程序内容
-
在 iOS 上进行后台处理
-
在 Android 上进行后台处理
-
在 iOS 上播放音频文件
-
在 Android 上播放音频文件
介绍
在本章中,我们将继续介绍更多的食谱,涉及编写与本机 iOS 和 Android 代码交互的 React Native 应用程序的不同方面。我们将涵盖利用内置和社区创建的模块的示例应用程序。这些食谱涵盖了一系列主题,从渲染基本按钮到创建不阻塞主应用程序线程的多线程进程。
对应用程序状态变化做出反应
普通移动设备用户通常会经常使用几个应用程序。理想情况下,除了其他社交媒体应用程序、游戏、媒体播放器等,用户还将使用您的 React Native 应用程序。任何特定的用户可能会在每个应用程序中花费很短的时间,因为他们在多任务处理。如果我们想要在用户离开我们的应用程序并重新进入时做出反应怎么办?我们可以利用这个机会与服务器同步数据,或者告诉用户我们很高兴看到他们回来,或者礼貌地要求在应用商店上对应用程序进行评分。
这个食谱将涵盖应用程序状态变化的基础知识,也就是说,对应用程序处于前台(活动)、后台或非活动状态时做出反应。
对于这个食谱,让我们创建一个名为AppStateApp
的新的纯 React Native 应用程序。
如何做...
- 幸运的是,React Native 提供了对应用程序状态变化的支持,通过
AppState
模块监听。让我们通过向App.js
文件添加依赖项来开始构建应用程序,如下所示:
import React, { Component } from 'react';
import {
AppState,
StyleSheet,
Text,
View
} from 'react-native';
- 在这个食谱中,我们将跟踪先前的状态,以查看用户来自何处。如果这是他们第一次进入应用程序,我们将欢迎他们,如果他们返回,我们将改为欢迎他们。为此,我们需要保留对先前和当前应用程序状态的引用。我们将使用实例变量
previousAppState
和currentAppStates
来代替使用状态,只是为了避免潜在的命名混淆。我们将使用state
来保存向用户的状态消息,如下所示:
export default class App extends Component {
previousAppState = null;
currentAppState = 'active';
state = {
statusMessage: 'Welcome!'
}
// Defined on following steps
}
- 当组件挂载时,我们将使用
AppState
组件添加一个change
事件的监听器。每当应用程序的状态发生变化(例如,当应用程序被置于后台时),将触发change
事件,然后我们将触发下一步中定义的handleAppStateChange
处理程序,如下所示:
componentWillMount() {
AppState.addEventListener('change', this.handleAppStateChange);
}
handleAppStateChange
方法将接收appState
作为参数,我们可以期望它是三个字符串中的一个:如果应用程序从内存中卸载,则为inactive
,如果应用程序在内存中并处于后台,则为background
,如果应用程序在前台,则为active
。我们将使用switch
语句相应地更新state
上的statusMessage
:
handleAppStateChange = (appState) => {
let statusMessage;
this.previousAppState = this.currentAppState;
this.currentAppState = appState;
switch(appState) {
case 'inactive':
statusMessage = "Good Bye.";
break;
case 'background':
statusMessage = "App Is Hidden...";
break;
case 'active':
statusMessage = 'Welcome Back!'
break;
}
this.setState({ statusMessage });
}
render
方法在这个示例中非常基础,因为它只需要向用户显示状态消息,如下所示:
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
{this.state.statusMessage}
</Text>
</View>
);
}
- 该应用程序的样式很基础,包括字体大小、颜色和边距,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
welcome: {
fontSize: 40,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
- 完成的应用程序现在应该根据设备上应用程序的状态显示适当的状态消息。
工作原理...
在这个示例中,我们利用了内置的AppState
模块。该模块监听 Android 上的Activity
事件,在 iOS 上使用NSNotificationCenter
在各种UIApplication
事件上注册监听器。请注意,两个平台都支持active
和background
状态;然而,inactive
状态是 iOS 独有的概念。由于 Android 的多任务处理实现,它不明确支持inactive
状态,因此只在background
和active
状态之间切换应用程序。要在 Android 上实现等效于 iOS 不活动状态的效果,请参见本章后面的在多任务处理时隐藏应用程序内容示例。
复制和粘贴内容
在桌面和移动操作系统中最常用的功能之一是用于复制和粘贴内容的剪贴板。在移动设备上的常见情况是使用长文本填写表单,例如长电子邮件地址或密码。与其打字并出现几个拼写错误,不如直接打开您的联系人应用程序,从那里复制电子邮件并粘贴到您的TextInput
字段中会更容易。
这个示例将展示在 Android 和 iOS 上如何在 React Native 应用程序中复制和粘贴文本的基本示例。在我们的示例应用程序中,我们将有一个静态的Text
视图和一个TextInput
字段,您可以使用它来将其内容复制到剪贴板。此外,还将有一个按钮,用于将剪贴板的内容输出到视图中。
准备工作
对于这个示例,我们将创建一个名为 CopyPasteApp
的纯 React Native 应用程序。
在这个示例中,我们将再次使用 react-native-button
。使用 npm
安装它:
npm install react-native-button
或者,我们可以使用 yarn
:
yarn add react-native-button
如何做...
- 让我们首先创建一个
ClipboardText
组件,它既使用Text
组件来显示文本,又提供了通过长按将其内容复制到剪贴板的功能。在项目的根目录下创建一个component
文件夹,并在其中创建一个ClipboardText.js
文件。我们将首先导入依赖项,如下所示:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
Clipboard,
TextInput
} from 'react-native';
import Button from 'react-native-button';
- 接下来,我们将定义
App
类和初始的state
。我们将使用state
上的clipboardContent
属性来存储从剪贴板粘贴到 UI 中的文本,如下所示:
export default class App extends Component {
state = {
clipboardContent: null
}
// Defined in following steps
}
- UI 将有一个
Text
组件,其文本可以通过长按进行复制。让我们定义copyToClipboard
方法。我们将通过它的ref
(稍后我们将定义)获取输入,并通过其props.children
属性访问组件的文本。一旦文本被存储在一个本地变量中,我们只需将其传递给Clipboard
的setString
方法,以将文本复制到剪贴板,如下所示:
copyToClipboard = () => {
const sourceText = this.refs.sourceText.props.children;
Clipboard.setString(sourceText);
}
- 同样,我们还需要一个方法,它将从剪贴板中粘贴文本到应用的 UI 中。这个方法将使用
Clipboard
的getString
方法,并将返回的字符串保存到state
的clipboardContent
属性中,重新渲染应用的 UI 以反映粘贴的文本,如下所示:
getClipboardContent = async () => {
const clipboardContent = await Clipboard.getString();
this.setState({
clipboardContent
});
}
render
方法将由两个部分组成:第一部分是要复制的内容,第二部分是从剪贴板粘贴文本到 UI 的方法。让我们从第一部分开始,它包括一个Text
输入,其onLongPress
属性连接到我们在 步骤 3 中创建的copyToClipboard
方法,以及一个用于正常本地复制/粘贴的文本输入:
render() {
return (
<View style={styles.container}>
<Text style={styles.instructions}>
Tap and Hold the next line to copy it to the Clipboard:
</Text>
<Text
ref="sourceText"
onLongPress={this.copyToClipboard}
>
React Native Cookbook
</Text>
<Text style={styles.instructions}>
Input some text into the TextInput below and Cut/Copy as
you normally would:
</Text>
<TextInput style={styles.textInput} />
// Defined on next step
</View>
);
}
- UI 的第二部分包括一个
Text
组件,用于显示保存在state
的clipboardContent
中的当前值,并一个按钮,将使用我们在 步骤 4 中定义的getClipboardContent
方法从剪贴板中粘贴:
render() {
return (
<View style={styles.container}>
// Defined in previous step
<View style={styles.row}>
<Text style={styles.rowText}>
Clipboard Contents:
</Text>
</View>
<View style={styles.row}>
<Text style={styles.content}>
{this.state.clipboardContent}
</Text>
</View>
<Button
containerStyle={styles.buttonContainer}
style={styles.buttonStyle}
onPress={this.getClipboardContent}
>
Paste Clipboard
</Button>
</View>
);
}
最终的应用程序应该类似于以下截图:
它是如何工作的...
在这个示例中,我们通过使用 React Native 提供的Clipboard
API 构建了一个简单的复制粘贴应用程序。Clipboard
模块目前仅支持String
类型的内容,尽管设备可以复制更复杂的数据。这个模块使得使用剪贴板就像调用setString
和getString
方法一样简单。
通过指纹识别或指纹传感器进行认证
安全在软件中是一个重要的问题,特别是在任何形式的认证时。数据泄露和密码泄露已经成为日常新闻的一部分,各种规模的公司都在意识到需要在他们的应用程序中实施额外的安全措施。移动设备中的一种措施是生物识别认证,它使用指纹扫描或面部识别技术提供补充的身份验证方法。
这个示例介绍了如何添加指纹扫描和面部识别安全功能。由于react-native-touch-id
库的存在,这个过程在 React Native 应用程序开发中变得简化和流畅。
准备工作
对于这个示例,我们需要一个新的纯 React Native 应用。让我们称之为BiometricAuth
。
我们将使用react-native-button
和react-native-touch-id
库。使用npm
安装它们:
npm install react-native-button react-native-touch-id --save
或者,我们可以使用yarn
:
yarn add react-native-button react-native-touch-id
安装完成后,react-native-touch-id
需要进行链接,所以请务必跟进:
react-native link
权限也需要手动调整。对于 Android 权限,请在项目中找到AndroidManifest.xml
文件,应该在BiometricAuth/android/app/src/main/AndroidManifest.xml
。除了这个文件中的其他权限,你还需要添加以下内容:
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
对于 iOS 权限,你需要在文本编辑器中更新Info.plist
文件。Info.plist
可以在BiometricAuth/ios/BiometricAuth/Info.plist
找到。除了所有其他条目,添加以下内容:
<key>NSFaceIDUsageDescription</key>
<string>Enabling Face ID allows you quick and secure access to your account.</string>
如何做...
- 让我们首先在
App.js
文件中添加依赖项,如下所示:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View
} from 'react-native';
import Button from 'react-native-button';
import TouchID from 'react-native-touch-id';
- 接下来,我们将定义
App
类和初始state
。我们将在state
的authStatus
属性上跟踪认证状态,如下所示:
export default class App extends Component {
state = {
authStatus: null
}
// Defined in following steps
}
- 让我们定义
authenticate
方法,它将在按钮按下时触发,并在设备上启动认证。我们可以通过执行TouchID
组件的authenticate
方法来启动认证。这个方法的第一个参数是一个可选的字符串,解释请求的原因,如下所示:
authenticate = () => {
TouchID.authenticate('Access secret information!')
.then(this.handleAuthSuccess)
.catch(this.handleAuthFailure);
}
- 这个方法在成功时触发
handleAuthSuccess
方法。让我们现在来定义它。这个方法简单地将state
的authStatus
属性更新为字符串Authenticated
,如下所示:
handleAuthSuccess = () => {
this.setState({
authStatus : 'Authenticated'
});
}
- 同样,如果身份验证失败,将调用
handleAuthFailure
函数,该函数将更新相同的state.authStatus
为字符串Not Authenticated
,如下所示:
handleAuthFailure = () => {
this.setState({
authStatus : 'Not Authenticated'
});
}
render
方法将需要一个按钮来发起身份验证请求,以及两个Text
组件:一个用于标签,一个用于显示身份验证状态,如下所示:
render() {
return (
<View style={styles.container}>
<Button
containerStyle={styles.buttonContainer}
style={styles.button}
onPress={this.authenticate}>
Authenticate
</Button>
<Text style={styles.label}>Authentication Status</Text>
<Text style={styles.welcome}>{this.state.authStatus}</Text>
</View>
);
}
- 最后,我们将添加样式来设置 UI 的颜色、大小和布局,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#fff',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
label: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
buttonContainer: {
width: 150,
padding: 10,
margin: 5,
height: 40,
overflow: 'hidden',
backgroundColor: '#FF5722'
},
button: {
fontSize: 16,
color: 'white'
}
});
工作原理...
这个教程演示了将原生指纹和面部识别安全性简单地整合到 React Native 应用程序中的方法。调用TouchID.authenticate
还需要一个可选的选项对象参数,其中包括三个属性:title
用于确认对话框的标题(仅限 Android),color
用于对话框的颜色(仅限 Android),以及fallbackLabel
用于编辑默认的“显示密码”标签(仅限 iOS)。
在多任务处理时隐藏应用程序内容
保持应用程序安全主题的进行,有时我们必须警惕不速之客触摸我们的设备,可能获取对我们应用程序的访问权限。为了保护用户在查看敏感信息时免受窥视,我们可以在应用程序隐藏但仍处于活动状态时对应用程序进行遮罩。一旦用户返回到应用程序,我们只需移除遮罩,用户就可以继续正常使用应用程序。这在银行或密码应用程序中隐藏敏感信息时是一个很好的使用案例。
这个教程将向你展示如何渲染一个图像来遮罩你的应用程序,并在应用程序返回到前台或活动状态时将其移除。我们将涵盖 iOS 和 Android;然而,实现方式完全不同。对于 iOS,我们采用纯 Objective-C 实现以获得最佳性能。对于 Android,我们需要对MainActivity
进行一些修改,以便向 JavaScript 层发送应用程序失去焦点的事件。我们将在那里处理图像遮罩的渲染。
准备工作
当应用程序不在前台时,我们需要一个图像来用作遮罩。我选择使用了一张 iPhone 壁纸,你可以在这里找到:
www.hdiphone7wallpapers.com/2016/09/white-squares-iphone-7-and-7-plus-wallpapers.html
该图像是一种风格化的马赛克图案。它看起来像这样:
当然,您可以使用任何您喜欢的图像。在这个示例中,图像文件将被命名为hidden.jpg
,因此请相应地重命名您的图像。
我们需要一个新的纯 React Native 应用程序。让我们称之为HiddenContentApp
。
如何做...
-
让我们首先将面具图像添加到应用程序的 iOS 部分。我们需要在新的 React Native 应用程序的
ios/
目录中的 Xcode 中打开项目的ios
文件夹。 -
我们可以通过将图像拖放到 Xcode 项目的
Images.xcassets
文件夹中来将hidden.jpg
图像添加到项目中,如下图所示:
- 接下来,我们将向
AppDelegate.m
文件添加一个新的实现和两种方法。可以在下面找到整个文件,包括生成的代码。为了清晰起见,我们添加的代码已用粗体标记。我们正在扩展applicationWillResignActive
方法,每当给定应用程序从前台变为后台时,它都会触发,以添加一个带有hidden.jpg
作为其图像的imageView
。同样,我们还需要扩展相反的方法applicationDidBecomeActive
,以在应用程序重新进入前台时删除图像:
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
@implementation AppDelegate {
UIImageView *imageView;
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSURL *jsCodeLocation;
jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index" fallbackResource:nil];
RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
moduleName:@"HiddenContentApp"
initialProperties:nil
launchOptions:launchOptions];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application {
imageView = [[UIImageView alloc] initWithFrame:[self.window frame]];
[imageView setImage:[UIImage imageNamed:@"hidden.jpg"]];
[self.window addSubview:imageView];
}
- (void)applicationDidBecomeActive:(UIApplication *)application {
if(imageView != nil) {
[imageView removeFromSuperview];
imageView = nil;
}
}
@end
-
通过前面的三个步骤,iOS 应用程序中显示面具所需的所有工作已经完成。让我们通过在 Android Studio 中打开项目的 Android 部分来继续进行。在 Android Studio 中,选择打开现有的 Android Studio 项目,并打开项目的
android
目录。 -
我们需要更新 Android 项目中的唯一本地代码位于
MainActivity.java
中,位于此处:
我们需要添加一个方法,以及方法使用的来自 React 的三个导入。下面是完整的MainActivity.java
文件,加粗标记的是添加的代码。我们正在定义一个扩展基本方法功能的onWindowFocusChanged
方法。基本的onWindowFocusChanged
Android 方法在给定应用程序的焦点发生变化时触发,传递一个表示应用程序是否具有焦点的hasFocus
布尔值。我们的扩展将通过我们命名为focusChange
的事件有效地将该hasFocus
布尔值从父方法传递到 React Native 层,如下所示:
package com.hiddencontentapp;
import com.facebook.react.ReactActivity;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
public class MainActivity extends ReactActivity {
/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "HiddenContentApp";
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (getReactNativeHost().getReactInstanceManager().getCurrentReactContext() != null) {
WritableMap params = Arguments.createMap();
params.putBoolean("appHasFocus", hasFocus);
getReactNativeHost().getReactInstanceManager()
.getCurrentReactContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("focusChange", params);
}
}
}
-
要在 Android 中使用
hidden.jpg
遮罩图像,我们还需要将其添加到 React Native 项目中。让我们在 React Native 项目的根目录中创建一个新的assets
文件夹,并将hidden.jpg
图像文件添加到新文件夹中。 -
有了原生部分的基础,我们现在准备转向应用程序的 JavaScript 部分。让我们在
App.js
中添加我们将使用的导入,如下所示:
import React, {Component} from 'react';
import {
StyleSheet,
Text,
View,
DeviceEventEmitter,
Image
} from 'react-native';
- 接下来,让我们创建
App
类和初始state
。state
只需要一个showMask
布尔值,它将决定是否显示遮罩,如下所示:
export default class App extends Component {
state = {
showMask: null
}
// Defined in following steps
}
- 当组件挂载时,我们希望注册一个事件监听器,以便使用
DeviceEventEmitter
的addListener
方法监听从原生 Android 层发出的事件,将字符串focusChange
作为要监听的事件的名称作为第一个参数,并将要执行的回调作为第二个参数。您可能还记得,focusChange
是我们在MainActivity.java
中的onWindowFocusChange
方法中分配的事件名称,在步骤 5中注册事件监听器如下:
componentWillMount() {
this.subscription = DeviceEventEmitter.addListener(
'focusChange',
this.onFocusChange
);
}
- 在这一步中,我们将把事件监听器保存到类成员
this.subscription
中。这将允许在组件卸载时清理事件监听器。我们只需在组件卸载时通过componentWillUnmount
生命周期钩子调用this.subscription
上的remove
方法,如下所示:
componentWillUnmount() {
this.subscription.remove();
}
- 让我们定义在步骤 9中使用的
onFocusChange
处理程序。该方法接收一个params
对象,其中包含通过步骤 5中定义的onWindowFocusChanged
方法从原生层传递的appHasFocus
布尔值。通过将state
上的showMask
布尔值设置为appHasFocus
布尔值的相反值,我们可以在render
函数中使用它来切换显示hidden.jpg
图像,如下所示:
onFocusChange = (params) => {
this.setState({showMask: !params.appHasFocus})
}
render
方法的主要内容在这个示例中并不重要,但我们可以使用它来在state
的showMask
属性为true
时应用hidden.jpg
蒙版图像,如下所示:
render() {
if(this.state.showMask) {
return (<Image source={require('./assets/hidden.jpg')} />);
}
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
</View>
);
}
- 应用程序已经完成。一旦应用程序加载完成,您应该能够转到应用程序选择视图(在 iOS 上双击 home,或在 Android 上按方形按钮),并在应用程序不在前台时看到应用的蒙版图像。请注意,Android 模拟器可能无法按预期正确应用蒙版,因此这个功能可能需要使用 Android 设备进行测试。
工作原理...
在这个示例中,我们看到了需要使用两种不同的方法来完成相同的任务。对于 iOS,我们在本地层中独占地处理显示图像蒙版,而不需要 React Native 层。对于 Android,我们使用 React Native 来处理图像蒙版。
在步骤 3中,我们扩展了两个 Objective-C 方法:applicationWillResignActive
,当应用程序从前台切换时触发,以及applicationDidBecomeActive
,当应用程序进入前台时触发。对于每个事件,我们简单地切换显示在 Xcode 项目的Images.xcassettes
文件夹中存储的hidden.jpg
图像的imageView
。
在步骤 5中,我们使用了 React 类RCTDeviceEventEmitter
从DeviceEventManagerModule
来发出一个名为focusChange
的事件,传递一个带有appHasFocus
布尔值的params
对象到 React Native 层,如下所示:
getReactNativeHost().getReactInstanceManager()
.getCurrentReactContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("focusChange", params);
}
在步骤 9中,我们定义了componentWillMount
生命周期钩子,为从本地 Android 层发出的focusChange
事件设置了一个事件侦听器,触发onFocusChange
方法,该方法将根据本地appHasFocus
值更新state
的showMask
值,触发重新渲染,适当地显示蒙版。
iOS 上的后台处理
在过去的几年里,移动设备的处理能力大大增加。用户要求更丰富的体验,实现在现代移动设备上改进性能的一种方法是通过多线程。大多数移动设备今天都由多核处理器驱动,它们的操作系统现在为开发人员提供了在后台执行代码的简单抽象,而不会干扰应用程序 UI 的性能。
这个示例将涵盖 iOS 的Grand Central Dispatch(GCD)的使用,以在新线程上执行异步后台处理,并在处理完成时与 React Native 层进行通信。
准备工作
对于这个示例,我们需要一个新的纯 React Native 应用程序。让我们将其命名为MultiThreadingApp
。
我们还将使用react-native-button
库。使用npm
安装它:
npm install react-native-button --save
或者,我们可以使用yarn
:
yarn add react-native-button --save
如何做...
-
我们将首先在新的 React Native 应用程序的
ios
目录中打开 Xcode 中的 iOS 项目。 -
让我们添加一个名为
BackgroundTaskManager
的新的 Cocoa 类文件,其子类为NSObject
。有关在 Xcode 中执行此操作的更多详细信息,请参考本章中的公开自定义 iOS 模块示例。 -
接下来,让我们将新模块连接到 React 的
RCTBrideModule
,在新模块的头文件BackgroundTaskManager.h
中。要添加的代码在以下片段中用粗体标记出来:
#import <Foundation/Foundation.h>
#import <dispatch/dispatch.h>
#import "RCTBridgeModule.h"
@interface BackgroundTaskManager : NSObject <RCTBridgeModule> {
dispatch_queue_t backgroundQueue;
}
@end
- 我们将在
BackgroundTaskManager.m
文件中实现本机模块。同样,我们要添加的新代码在以下片段中用粗体标记出来:
#import "BackgroundTaskManager.h"
#import "RCTBridge.h"
#import "RCTEventDispatcher.h"
@implementation BackgroundTaskManager
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(loadInBackground) {
backgroundQueue = dispatch_queue_create("com.moduscreate.bgqueue", NULL);
dispatch_async(backgroundQueue, ^{
NSLog(@"processing background");
[self.bridge.eventDispatcher sendAppEventWithName:@"backgroundProgress" body:@{@"status": @"Loading"}];
[NSThread sleepForTimeInterval:5];
NSLog(@"slept");
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Done processing; main thread");
[self.bridge.eventDispatcher sendAppEventWithName:@"backgroundProgress" body:@{@"status": @"Done"}];
});
});
}
@end
- 接下来让我们转向 JavaScript 层。我们将首先在
App.js
文件中添加依赖项。作为依赖项的一部分,我们还需要导入在步骤 3和步骤 4中定义的BackgroundTaskManager
本机模块,如下所示:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
NativeModules,
NativeAppEventEmitter
} from 'react-native';
import Button from 'react-native-button';
const BackgroundTaskManager = NativeModules.BackgroundTaskManager;
- 让我们定义
App
类,初始状态为backgroundTaskStatus
设置为字符串Not Started
,并且doNothingCount
属性初始化为0
,如下所示:
export default class App extends Component {
state = {
backgroundTaskStatus: 'Not Started',
counter: 0
}
// Defined in following steps
}
- 我们需要监听从我们在步骤 3和步骤 4中创建的自定义模块的本机 iOS 层发出的
backgroundProcess
事件。让我们使用NativeAppEventEmitter
React Native 组件设置事件监听器,将state
的backgroundTaskStatus
属性设置为从本机事件接收到的事件对象上的status
值,如下所示:
componentWillMount = () => {
this.subscription = NativeAppEventEmitter.addListener(
'backgroundProgress',
event => this.setState({ backgroundTaskStatus: event.status })
);
}
- 当组件卸载时,我们需要从上一步中删除事件监听器,如下所示:
componentWillUnmount = () => {
this.subscription.remove();
}
- UI 将有两个按钮,每个按钮在按下时都需要调用一个方法。
runBackgroundTask
将运行我们在本机 iOS 层上定义并导出的loadInBackground
方法,该方法位于BackgroundTaskManager
自定义本机模块上。increaseCounter
按钮将简单地通过1
增加state
上的counter
属性,以显示主线程未被阻塞的情况,如下所示:
runBackgroundTask = () => {
BackgroundTaskManager.loadInBackground();
}
increaseCounter = () => {
this.setState({
counter: this.state.counter + 1
});
}
- 应用的 UI 将包括两个按钮来显示
Button
组件,以及一个Text
组件来显示在state
上保存的值。“Run Task”按钮将执行runBackgroundTask
方法来启动后台进程,并且this.state.backgroundTaskStatus
将更新以显示进程的新状态。在后台进程运行的五秒钟内,按下“Increase Counter”按钮仍然会增加计数器 1,证明后台进程是非阻塞的,如下面的代码片段所示:
render() {
return (
<View style={styles.container}>
<Button
containerStyle={styles.buttonContainer}
style={styles.buttonStyle}
onPress={this.runBackgroundTask}>
Run Task
</Button>
<Text style={styles.instructions}>
Background Task Status:
</Text>
<Text style={styles.welcome}>
{this.state.backgroundTaskStatus}
</Text>
<Text style={styles.instructions}>
Pressing "Increase Conter" button shows that the task is
not blocking the main thread
</Text>
<Button
containerStyle={[
styles.buttonContainer,
styles.altButtonContainer
]}
style={styles.buttonStyle}
onPress={this.increaseCounter}
>
Increase Counter
</Button>
<Text style={styles.instructions}>
Current Count:
</Text>
<Text style={styles.welcome}>
{this.state.counter}
</Text>
</View>
);
}
- 作为最后一步,让我们使用样式块来布局和设计应用,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
marginLeft: 20,
marginRight: 20
},
buttonContainer: {
width: 150,
padding: 10,
margin: 5,
height: 40,
overflow: 'hidden',
borderRadius: 4,
backgroundColor: '#FF5722'
},
altButtonContainer : {
backgroundColor : '#CDDC39',
marginTop : 30
},
buttonStyle: {
fontSize: 16,
color: 'white'
}
});
工作原理...
在这个示例中,我们创建了一个类似于本章前面暴露自定义 iOS 模块示例中涵盖的模块的本地模块。我们定义了本地模块来在 React Native 应用的后台执行任意操作。在这个示例中,后台进程由以下三个步骤组成:
-
创建一个新的线程。
-
在新线程上睡眠五秒钟。
-
在五秒的睡眠后(模拟运行后台进程的结束),从 iOS 层向 React Native 层分发一个事件,让它知道进程已经完成。这是通过操作系统的 GCD API 实现的。
这个应用的 UI 的目的是展示多线程已经实现。如果后台进程在 React Native 层执行,由于 JavaScript 的单线程特性,应用在后台进程运行时会被锁定五秒钟。当您按下按钮时,桥被调用,然后消息可以被发布到本地层。如果本地线程当前正在忙于睡眠,那么我们无法处理这条消息。通过将处理转移到新线程,两者可以同时执行。
在 Android 上进行后台处理
在这个示例中,我们将构建一个 Android 版本的前一个示例的等价物。这个示例还将使用原生的 Android 层来创建一个新的进程,通过睡眠五秒钟来保持该进程运行,并允许用户通过按钮进行交互,以展示应用的主处理线程没有被阻塞。
虽然最终结果将是非常相似的,但在 Android 项目中生成一个新进程与 iOS 处理方式有些不同。这个示例将利用本地的AsyncTask
函数,专门用于处理短期后台进程,以允许在 React Native 层执行而不阻塞主线程。
准备工作
对于这个示例,我们需要创建一个新的纯 React Native 应用。让我们命名它为MultiThreadingApp
。
我们还将使用react-native-button
库。使用npm
安装它:
npm install react-native-button --save
另外,我们可以使用yarn
:
yarn add react-native-button
如何做到…
-
首先在 Android Studio 中打开 Android 项目。在 Android Studio 中,选择打开现有的 Android Studio 项目,并打开新项目的
android
目录。 -
我们需要两个新的 Java 类:
BackgroundTaskManager
和BackgroundTaskPackage
。 -
现在这两个类都已创建,让我们打开
BackgroundTaskManager.java
并开始实现将包装AsyncTask
操作的本地模块,从导入和定义类开始。此外,像任何其他本地 Android 模块一样,我们需要定义getName
方法,用于为模块提供一个名称给 React Native,如下所示:
package com.multithreadingapp; import android.os.AsyncTask; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule;
public class BackgroundTaskManager extends ReactContextBaseJavaModule {
public BackgroundTaskManager(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
}
@Override
public String getName() {
return "BackgroundTaskManager";
}
// Defined in following steps
}
- 为了执行
AsyncTask
,它需要由一个私有类进行子类化。我们需要为此添加一个新的私有内部BackgroundLoadTask
子类。在我们定义它之前,让我们首先添加一个loadInBackground
方法,最终将被导出到 React Native 层。这个方法简单地创建一个BackgroundLoadTask
的新实例并调用它的execute
方法,如下所示:
public class BackgroundTaskManager extends ReactContextBaseJavaModule {
// Defined in previous step
@ReactMethod
public void loadInBackground() {
BackgroundLoadTask backgroundLoadTask = new BackgroundLoadTask();
backgroundLoadTask.execute();
}
}
BackgroundLoadTask
子类还将使用一个辅助函数来来回发送事件,以跨越 React Native 桥通信后台进程的状态。sendEvent
方法接受eventName
和params
作为参数,然后使用 React Native 的RCTDeviceEventEmitter
类来emit
事件,如下所示:
public class BackgroundTaskManager extends ReactContextBaseJavaModule {
// Defined in steps above
private void sendEvent(String eventName, WritableMap params) {
getReactApplicationContext().getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
}
}
- 现在让我们继续定义
BackgroundLoadTask
子类,它继承自AsyncTask
。子类将由三个方法组成:doInBackground
用于启动一个新线程并让其休眠五分钟,onProgressUpdate
用于向 React Native 层发送"Loading"
状态,以及onPostExecute
用于在后台任务完成时发送"Done"
状态,如下所示:
public class BackgroundTaskManager extends ReactContextBaseJavaModule {
// Defined in above steps
private class BackgroundLoadTask extends AsyncTask<String, String, String> {
@Override
protected String doInBackground(String... params) {
publishProgress("Loading");
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
return "Done";
}
@Override
protected void onProgressUpdate(String... values) {
WritableMap params = Arguments.createMap();
params.putString("status", "Loading");
sendEvent("backgroundProgress", params);
}
@Override
protected void onPostExecute(String s) {
WritableMap params = Arguments.createMap();
params.putString("status", "Done");
sendEvent("backgroundProgress", params);
}
}
}
-
由于 iOS 实现和 Android 实现之间的唯一区别存在于配方的本机层中,因此您可以按照上一个配方中的步骤 5至步骤 11来实现应用程序的 JavaScript 部分。
-
最终的应用程序应该在行为和外观上(除了设备上的差异)与上一个配方中的应用程序相同:
它是如何工作的...
在这个配方中,我们模仿了我们在 Android 上创建的在 iOS 上进行后台处理配方中创建的功能。我们创建了一个 Android 本机模块,其中一个方法在调用时在后台执行任意操作(休眠五秒)。当进程完成时,它会向 React Native 层发出事件,然后我们更新应用程序 UI 以反映后台进程的状态。Android 有多个选项可以在本机执行多线程操作。在这个配方中,我们使用了AsyncTask
,因为它适用于短期运行(几秒钟)的进程,相对简单实现,并且操作系统为我们管理线程创建和资源分配。您可以在官方文档中阅读更多关于AsyncTask
的信息:
developer.android.com/reference/android/os/AsyncTask
在 iOS 上播放音频文件
在实现复杂用户界面-第三部分章节中,我们使用 Expo SDK 提供的Audio
组件在创建音频播放器配方中构建了一个相对复杂的小型音频播放器。然而,Expo 的Audio
组件的一个缺点是它无法在应用程序被置于后台时播放音频。目前使用本机层是实现这一点的唯一方法。
在这个配方中,我们将创建一个本机模块来显示 iOS MediaPicker,然后选择要播放的音乐文件。所选文件将通过本机 iOS 媒体播放器播放,允许在应用程序被置于后台时播放音频,并允许用户通过本机 iOS 控制中心控制音频。
准备工作
对于这个配方,我们需要创建一个新的纯 React Native 应用。让我们称之为AudioPlayerApp
。
我们还将使用react-native-button
库,可以使用npm
安装:
npm install react-native-button --save
或者,我们可以使用yarn
:
yarn add react-native-button
这是一个只能在真实设备上预期工作的示例。您还需要确保您的 iOS 设备上同步了音乐并且在媒体库中可用。
如何做...
-
让我们首先在新的 React Native 应用程序的
ios
目录中打开 Xcode 中的 iOS 项目。 -
接下来,我们将创建一个名为
MediaManager
的新的 Objective-C Cocoa 类。 -
在
MediaManager
头文件(.h
)中,我们需要导入MPMediaPickerController
和MPMusicPlayerController
,以及 React Native 桥(RCTBridgeModule
),如下所示:
#import <Foundation/Foundation.h>
#import <MediaPlayer/MediaPlayer.h>
#import <React/RCTBridgeModule.h>
#import <React/RCTEventDispatcher.h>
@interface MediaManager : NSObject<RCTBridgeModule, MPMediaPickerControllerDelegate>
@property (nonatomic, retain) MPMediaPickerController *mediaPicker;
@property (nonatomic, retain) MPMusicPlayerController *musicPlayer;
@end
- 首先,我们需要开始添加原生
MediaPicker
到MediaManager
的实现(MediaManager.m
)中。首先的方法将是用于显示和隐藏MediaPicker
的:showMediaPicker
和hideMediaPicker
,如下所示:
#import "MediaManager.h"
#import "AppDelegate.h"
@implementation MediaManager
RCT_EXPORT_MODULE();
@synthesize bridge = _bridge;
@synthesize musicPlayer;
#pragma mark private-methods
-(void)showMediaPicker {
if(self.mediaPicker == nil) {
self.mediaPicker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio];
[self.mediaPicker setDelegate:self];
[self.mediaPicker setAllowsPickingMultipleItems:NO];
[self.mediaPicker setShowsCloudItems:NO];
self.mediaPicker.prompt = @"Select song";
}
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[delegate.window.rootViewController presentViewController:self.mediaPicker animated:YES completion:nil];
}
void hideMediaPicker() {
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
[delegate.window.rootViewController dismissViewControllerAnimated:YES completion:nil];
}
// Defined on following steps
@end
- 接下来,我们将实现
mediaPicker
需要的两个操作:didPickMediaItems
用于选择媒体项目,以及mediaPickerDidCancel
用于取消操作,如下所示:
-(void) mediaPicker:(MPMediaPickerController *)mediaPicker didPickMediaItems:(MPMediaItemCollection *)mediaItemCollection {
MPMediaItem *mediaItem = mediaItemCollection.items[0];
NSURL *assetURL = [mediaItem valueForProperty:MPMediaItemPropertyAssetURL];
[self.bridge.eventDispatcher sendAppEventWithName:@"SongPlaying"
body:[mediaItem valueForProperty:MPMediaItemPropertyTitle]];
if(musicPlayer == nil) {
musicPlayer = [MPMusicPlayerController systemMusicPlayer];
}
[musicPlayer setQueueWithItemCollection:mediaItemCollection];
[musicPlayer play];
hideMediaPicker();
}
-(void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker {
hideMediaPicker();
}
- 接下来,我们需要将我们的
MediaManager
暴露给 React Native 桥,并创建一个将被调用以显示MediaPicker
的方法,如下所示:
RCT_EXPORT_MODULE();
RCT_EXPORT_METHOD(showSongs) {
[self showMediaPicker];
}
- 我们准备继续进行 JavaScript 部分。让我们首先在
App.js
中添加依赖项。我们还需要使用NativeModules
组件导入我们在步骤 3到步骤 6中创建的MediaManager
原生模块,如下所示:
import React, { Component } from 'react';
import {
StyleSheet,
Text,
View,
NativeModules,
NativeAppEventEmitter
} from 'react-native';
import Button from 'react-native-button';
const MediaManager = NativeModules.MediaManager;
- 让我们定义
App
类和初始state
。currentSong
属性将保存当前播放歌曲的曲目信息,如从原生层传递的那样:
export default class App extends Component {
state = {
currentSong: null
}
// Defined on following steps
}
- 当组件挂载时,我们将订阅从原生层发出的
SongPlaying
事件,当歌曲开始播放时。我们将事件监听器保存到本地的subscription
类变量中,以便在组件卸载时使用remove
方法清除它,如下所示:
componentWillMount() {
this.subscription = NativeAppEventEmitter.addListener(
'SongPlaying',
this.updateCurrentlyPlaying
);
}
componentWillUnmount = () => {
this.subscription.remove();
}
- 我们还需要一种方法来更新
state
上的currentSong
值,并且需要一种方法来调用我们在步骤 3到步骤 6中定义的原生MediaManager
模块上的showSongs
方法,如下所示:
updateCurrentlyPlaying = (currentSong) => {
this.setState({ currentSong });
}
showSongs() {
MediaManager.showSongs();
}
render
方法将由一个Button
组件组成,用于在按下时执行showSongs
方法,以及用于显示当前播放歌曲信息的Text
组件,如下所示:
render() {
return (
<View style={styles.container}>
<Button
containerStyle={styles.buttonContainer}
style={styles.buttonStyle}
onPress={this.showSongs}>
Pick Song
</Button>
<Text style={styles.instructions}>Song Playing:</Text>
<Text style={styles.welcome}>{this.state.currentSong}</Text>
</View>
);
}
- 最后,我们将添加我们的样式来布局和设计应用程序,如下所示:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
buttonContainer: {
width: 150,
padding: 10,
margin: 5,
height: 40,
overflow: 'hidden',
borderRadius: 4,
backgroundColor: '#3B5998'
},
buttonStyle: {
fontSize: 16,
color: '#fff'
}
});
它是如何工作的...
在这个教程中,我们介绍了如何在 iOS 中使用Media Player
,通过将其功能封装在一个本地模块中。媒体播放器框架允许我们访问本机 iPod 库,并使用与本机 iOS 音乐应用相同的功能在设备上播放库中的音频文件。
在 Android 上播放音频文件
谷歌喜欢宣称 Android 相对于 iOS 具有处理文件存储的灵活性。Android 设备支持外部 SD 卡,可以存储媒体文件,并不需要像 iOS 那样需要专有的方法来添加多媒体文件。
在这个教程中,我们将使用 Android 的本机MediaPicker
,它是从一个意图开始的。然后我们将能够选择一首歌并通过我们的应用程序播放它。
准备工作
对于这个教程,我们将创建一个名为AudioPlayer
的 React Native 应用程序。
在这个教程中,我们将使用react-native-button
库。要安装它,请在项目根目录的终端中运行以下命令:
$ npm install react-native-button --save
确保您的 Android 设备或模拟器的Music/
目录中有音乐文件可用。
如何做...
-
让我们首先使用 Android Studio 打开 Android 项目。在 Android Studio 中,选择“打开现有的 Android Studio 项目”,然后打开项目的
android
目录。 -
对于这个教程,我们将需要两个新的 Java 类:
MediaManager
和MediaPackage
。 -
我们的
MediaManager
将使用意图来显示mediaPicker
,MediaPlayer
来播放音乐,以及MediaMetadataRetriever
来解析音频文件的元数据信息并发送回 JavaScript 层。让我们首先在MediaManager.java
文件中导入我们需要的所有依赖项,如下所示:
import android.app.Activity;
import android.content.Intent;
import android.media.AudioManager;
import android.media.MediaMetadataRetriever;
import android.media.MediaPlayer;
import android.net.Uri;
import android.provider.MediaStore;
import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.modules.core.DeviceEventManagerModule;
showSongs
,getName
,playSong
,mediaPlayer
,onActivityResult
,mediaMetadataRetreiver
和SongPlaying
应该以代码格式显示。替换为:
public class MediaManager extends ReactContextBaseJavaModule implements ActivityEventListener {
private MediaPlayer mediaPlayer = null;
private MediaMetadataRetriever mediaMetadataRetriever = null;
public MediaManager(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
reactApplicationContext.addActivityEventListener(this);
}
@Override
public String getName() {
return "MediaManager";
}
@Override
public void onCatalystInstanceDestroy() {
super.onCatalystInstanceDestroy();
mediaPlayer.stop();
mediaPlayer.release();
mediaPlayer = null;
}
@ReactMethod
public void showSongs() {
Activity activity = getCurrentActivity();
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
activity.startActivityForResult(intent, 10);
}
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (data != null) {
playSong(data.getData());
}
}
@Override
public void onNewIntent(Intent intent) {
}
private void playSong(Uri uri) {
try {
if (mediaPlayer != null) {
mediaPlayer.stop();
mediaPlayer.reset();
} else {
mediaMetadataRetriever = new MediaMetadataRetriever();
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
}
mediaPlayer.setDataSource(getReactApplicationContext(), uri);
mediaPlayer.prepare();
mediaPlayer.start();
mediaMetadataRetriever.setDataSource(getReactApplicationContext(), uri);
String artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
String songTitle = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
WritableMap params = Arguments.createMap();
params.putString("songPlaying", artist + " - " + songTitle);
getReactApplicationContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("SongPlaying", params);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
- 自定义模块还需要添加到
MainApplication.java
文件中的getPackages
数组中,如下所示:
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new MediaPackage()
);
}
- 正如本章前面的暴露自定义 Android 模块教程中所介绍的,我们必须为我们的
MediaManager
自定义模块添加必要的样板,以便将其导出到 React Native 层。有关更详细的解释,请参考该教程。按照以下步骤添加必要的样板:
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;
public class MediaPackage 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 MediaManager(reactContext));
return modules;
}
}
- Android 应用程序的 JavaScript 层与之前的 iOS 教程中的相同。使用本教程的步骤 7到步骤 12来完成应用程序的最后部分。
第十三章:与原生应用集成
在这一章中,我们将涵盖以下的配方:
-
将 React Native 应用和原生 iOS 应用结合
-
从 iOS 应用到 React Native 的通信
-
从 React Native 到 iOS 应用容器的通信
-
处理外部 iOS 应用的调用
-
将 React Native 应用和原生 Android 应用结合
-
从 Android 应用到 React Native 的通信
-
从 React Native 到 Android 应用容器的通信
-
处理外部 Android 应用的调用
介绍
React Native 被引入作为使用 JavaScript 构建原生应用的解决方案,目标是让更多的开发人员能够为多个平台构建真正的原生应用。作为一个团队构建 React Native 应用的结果,JavaScript 开发人员和原生开发人员密切合作是很常见的。
React Native 能够渲染原生 UI 视图的一个优势是它们可以轻松地嵌入到现有的原生应用中。公司已经拥有关键的原生应用对于他们的业务至关重要并不罕见。如果应用程序没有出现问题,可能没有立即需要将整个代码库重写为 React Native。在这种情况下,JavaScript 和原生开发人员都可以利用 React Native 编写的代码,将其集成到现有应用中。
本章将专注于在现有的原生 iOS 和 Android 应用中使用 React Native。我们将涵盖在原生应用中渲染 React Native 应用,如何在 React Native 应用和其原生父应用之间进行通信,以及我们的 React Native 应用如何在用户设备上与其他应用一起调用。
在处理 Android 配方时,建议您在 Android Studio 中启用自动导入设置,或使用Alt+Enter执行快速修复代码完成类导入。
将 React Native 应用和原生 iOS 应用结合
如果您在一家公司工作,或者有一个客户在世界上使用着一个活跃的 iOS 应用,重写它可能并不明智,特别是如果它构建良好,经常被使用,并受到用户的赞扬。如果您只想使用 React Native 构建新功能,React Native 应用可以嵌入并在现有的原生 iOS 应用中渲染。
本教程将介绍如何创建一个空白的 iOS 应用程序,并将其添加到 React Native 应用程序中,以便这两个层可以相互通信。我们将介绍两种呈现 React Native 应用程序的方法:嵌入在应用程序中作为嵌套视图,以及作为全屏实现的另一种方法。本教程讨论的步骤将作为呈现 React Native 应用程序以及原生 iOS 应用程序的基线。
准备工作
本教程将引用一个名为EmbeddedApp
的原生 iOS 应用程序。我们将在本节中介绍如何创建示例 iOS 应用程序。如果您已经有一个打算与 React Native 集成的 iOS 应用程序,可以直接跳转到教程说明。但是,您需要确保已安装cocoapods
。这个库是 Xcode 项目的包管理器。可以使用以下命令通过 Homebrew 安装它:
brew install cocoapods
安装了cocoapods
后,下一步是在 Xcode 中创建一个新的原生 iOS 项目。可以通过打开 Xcode 并选择文件|新建|项目来完成。在随后的窗口中,选择默认的单视图应用程序 iOS 模板开始,并点击下一步。
在新项目的选项屏幕中,确保将产品名称字段设置为EmbeddedApp
:
如何操作...
- 我们将首先创建一个新的原始 React Native 应用程序,作为我们项目的根。让我们将新项目命名为
EmbedApp
。您可以使用以下命令使用 CLI 创建新的 React Native 应用程序:
react-native init EmbedApp
-
通过使用 CLI 创建新应用程序,
ios
和android
子文件夹将自动为我们创建,其中包含每个平台的原生代码。让我们将我们在“准备工作”部分中创建的原生应用程序移动到ios
文件夹中,以便它位于/EmbedApp/ios/EmbeddedApp
。 -
现在我们已经为应用程序准备好了基本结构,我们需要添加一个 Podfile。这是一个文件,类似于 Web 开发中的
package.json
,用于跟踪项目中使用的所有 cocoapod 依赖项(称为 pods)。Podfile 应始终位于原始 iOS 项目的根目录中,在我们的情况下是/EmbedApp/ios/EmbeddedApp
。在终端中,cd
进入此目录并运行pod init
命令。这将为您生成一个基本的 Podfile。 -
接下来,在您喜欢的 IDE 中打开 Podfile。我们将向该文件添加应用程序所需的 pods。以下是最终 Podfile 的内容,其中新增的 React Native 依赖项已用粗体标出:
target 'EmbeddedApp' do
# Uncomment the next line if you're using Swift or would like to use dynamic frameworks
# use_frameworks!
# Pods for EmbeddedApp
target 'EmbeddedAppTests' do
inherit! :search_paths
# Pods for testing
end
target 'EmbeddedAppUITests' do
inherit! :search_paths
# Pods for testing
end
# Pods that will be used in the app
pod 'React', :path => '../../node_modules/react-native', :subspecs => [
'Core',
'CxxBridge', # Include this for RN >= 0.47
'DevSupport', # Include this to enable In-App Devmenu if RN >= 0.43
'RCTText',
'RCTNetwork',
'RCTWebSocket', # Needed for debugging
'RCTAnimation', # Needed for FlatList and animations running on native UI thread
# Add any other subspecs you want to use in your project
]
# Explicitly include Yoga if you are using RN >= 0.42.0
pod 'yoga', :path => '../../node_modules/react-native/ReactCommon/yoga'
# Third party deps podspec link
pod 'DoubleConversion', :podspec => '../../node_modules/react-native/third-party-podspecs/DoubleConversion.podspec'
pod 'glog', :podspec => '../../node_modules/react-native/third-party-podspecs/glog.podspec'
pod 'Folly', :podspec => '../../node_modules/react-native/third-party-podspecs/Folly.podspec'
end
请注意,我们正在添加的 React Native 依赖项中列出的每个路径都指向 React Native 项目的/node_modules
文件夹。如果您的本地项目(在我们的情况下是EmbeddedApp
)位于不同的位置,则必须相应地更新对/node_modules
的引用。
-
有了 Podfile,安装 pod 本身就像在终端中运行
pod install
命令一样容易,我们在创建 Podfile 的同一目录中运行。 -
接下来,让我们回到项目的根目录
/EmbedApp
中的 React Native 应用程序。我们将首先删除index.js
中生成的代码,并用我们自己的简单的 React Native 应用程序替换它。在文件底部,我们将在AppRegistry
组件上使用registerComponent
方法将EmbedApp
注册为 React Native 应用程序的根组件。这将是一个非常简单的应用程序,只是渲染文本Hello in React Native
,以便在后续步骤中可以与本地层区分开来:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
Text
} from 'react-native';
class EmbedApp extends Component {
render() {
return (
<View style={styles.container}>
<Text>Hello in React Native</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('EmbedApp', () => EmbedApp);
-
现在我们有了一个 React Native 应用程序,我们可以转到本地代码。当我们在步骤 3中初始化 cocoapods 时,它还生成了一个新的
.xcworkspace
文件。确保在 Xcode 中关闭EmbeddedApp
项目,然后使用EmbeddedApp.xcworkspace
文件重新在 Xcode 中打开它。 -
在 Xcode 中,让我们打开
Main.storyboard
:
- 在 Storyboard 中,我们需要添加两个按钮:一个标记为 Open React Native App,另一个标记为 Open React Native App(Embedded)。我们还需要在两个按钮下方添加一个新的容器视图。最终的 Storyboard 应该看起来像这样:
- 接下来,我们需要一个新的 Cocoa Touch 类。这可以通过菜单选择
File | New | File
来创建。我们将类命名为EmbeddedViewController
,并将其分配为UIViewController
的子类:
- 让我们回到
Main.storyboard
。在通过上一步添加类创建的新场景(第二个 View Controller 场景)中,选择 View Controller 子项。确保身份检查器在右侧面板中是打开的:
选择 View Controller 后,将Class
值更改为我们新创建的类EmbeddedViewController
:
- 接下来,在顶部 View Controller Scene 中,选择 Embed segue 对象:
- 选择 segue 后,从右侧面板中选择属性检查器,并将标识符字段更新为 embed 值。我们将使用此标识符将 React Native 层嵌入到原生应用程序中:
- 我们准备构建
ViewController
的实现。打开ViewController.m
文件。我们将从导入开始:
#import "ViewController.h"
#import "EmbeddedViewController.h"
#import <React/RCTRootView.h>
- 在导入下面,我们可以添加一个接口定义,指向我们在步骤 10中创建的
EmbeddedViewController
:
@interface ViewController () {
EmbeddedViewController *embeddedViewController;
}
@end
- 接下来是
@interface
,我们将向@implementation
添加我们需要的方法。第一个方法openRNAppButtonPressed
将连接到我们在故事板中创建的第一个按钮,标有“打开 React Native 应用程序”。同样,openRNAppEmbeddedButtonPressed
方法将连接到第二个按钮“打开 React Native 应用程序(嵌入式)”。
您可能会注意到,这两种方法几乎是相同的,第二种方法引用了embeddedViewController
,与我们在第 10 步中创建的EmbeddedViewController
类相同([embeddedViewController setView:rootView];
)。这两种方法都使用jsCodeLocation
定义了值为http://localhost:8081/index.bundle?platform=ios
的 URL,这是 React Native 应用程序将被提供的 URL。另外,请注意,这两种方法中的moduleName
属性都设置为EmbedApp
,这是 React Native 应用程序的导出名称,我们在步骤 6中定义了它:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)openRNAppButtonPressed:(id)sender {
NSURL *jsCodeLocation = [NSURL
URLWithString:@"http://localhost:8081/index.bundle?platform=ios"];
RCTRootView *rootView =
[[RCTRootView alloc] initWithBundleURL : jsCodeLocation
moduleName : @"EmbedApp"
initialProperties : nil
launchOptions : nil];
UIViewController *vc = [[UIViewController alloc] init];
vc.view = rootView;
[self presentViewController:vc animated:YES completion:nil];
}
- (IBAction)openRNAppEmbeddedButtonPressed:(id)sender {
NSURL *jsCodeLocation = [NSURL
URLWithString:@"http://localhost:8081/index.bundle?platform=ios"];
RCTRootView *rootView =
[[RCTRootView alloc] initWithBundleURL : jsCodeLocation
moduleName : @"EmbedApp"
initialProperties : nil
launchOptions : nil];
[embeddedViewController setView:rootView];
}
// Defined in next step
@end
- 我们还需要定义
prepareForSegue
方法。在这里,您可以看到segue.identifier isEqualToString:@"embed"
,这是指我们在步骤 13中给 segue 的嵌入标识符:
// Defined in previous steps - (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"embed"]) {
embeddedViewController = segue.destinationViewController;
}
}
@end
- 在我们的
ViewController
实现就位后,现在我们需要将按钮操作连接到按钮本身。让我们返回到Main.storyboard
。Ctrl +单击第一个按钮以获取可分配给按钮的操作菜单,通过从 Touch Up Inside 返回到故事板,将按钮映射到我们在步骤 15中定义的openRNAppButtonPressed
方法。对于第二个按钮,重复这些步骤,将其链接到openRNAppEmbeddedButtonPressed
方法:
- 为了使 React Native 层能够与原生层通信,我们还需要添加一个安全异常,这将允许我们的代码与
localhost
通信。右键单击Info.plist
文件,然后选择打开为|源代码。在基本<dict>
标签内,添加以下条目:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
- 我们的应用程序完成了!从
/EmbedApp
根目录,使用以下命令通过 CLI 启动 React Native 应用程序:
react-native start
- 随着 React Native 应用程序的运行,让我们也从 Xcode 运行原生应用程序
EmbeddedApp
。现在,按下打开 React Native 应用程序按钮应该会全屏打开我们在步骤 6中创建的 React Native 应用程序,并且在按下打开 React Native 应用程序(嵌入式)按钮时,相同的 React Native 应用程序应该在我们在步骤 9中创建的容器视图中打开。
工作原理...
在这个教程中,我们介绍了通过两种不同的方法在原生 iOS 应用程序中渲染 React Native 应用程序。第一种方法是用 React Native 应用程序替换应用程序的主UIViewController
实例,在原生代码中称为RCTRootView
。这是在openRNAppButtonPressed
方法中完成的。第二种方法稍微复杂一些,是将 React Native 应用程序与原生应用程序内联渲染。这是通过创建一个容器视图来实现的,该容器视图链接到不同的UIViewController
实例。在这种情况下,我们用我们的RCTRootView
实例替换了embedViewController
的内容。这就是openRNAppEmbeddedButtonPressed
方法触发时发生的事情。
另见
为了更好地理解 cocoapods 在 Xcode/React Native 开发中的作用,我建议观看 Google 的Route 85 Show在 YouTube 上涵盖该主题的视频。视频可以在www.youtube.com/watch?v=iEAjvNRdZa0
找到。
从 iOS 应用程序到 React Native 的通信
在上一个教程中,我们学习了如何将 React Native 应用程序渲染为较大的原生 iOS 应用程序的一部分。除非您正在构建一个华丽的应用程序容器或门户,否则您可能需要在原生层和 React Native 层之间进行通信。这将是接下来两个教程的主题,每个教程都涉及通信的一个方向。
在这个示例中,我们将介绍从本地层到 React Native 层的通信,通过在 iOS 应用程序中使用UITextField
将数据发送到我们嵌入的 React Native 应用程序。
准备工作
由于这个示例需要一个嵌套的 React Native 应用程序的本地应用程序,我们将从上一个示例的结尾开始,有效地接着上次离开的地方。这将帮助您了解基本的跨层通信如何工作,以便您可以在自己的本地应用程序中使用相同的原则,这可能已经存在并具有复杂的功能。因此,跟随这个示例的最简单方法是使用上一个示例的终点作为起点。
如何做...
- 让我们从更新本地层的
ViewController.m
实现文件开始。确保通过上一个示例中项目中/ios/EmbeddApp
目录中放置的EmbeddedApp
的.xcworkspace
文件在 Xcode 中打开项目。我们将从导入开始:
#import "ViewController.h"
#import "EmbeddedViewController.h"
#import <React/RCTRootView.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
- 下一步是通过
ViewController
接口添加对 React Native 桥的引用,有效地将本地控制器与 React Native 代码链接起来:
@interface ViewController () <RCTBridgeDelegate> {
EmbeddedViewController *embeddedViewController;
RCTBridge *_bridge;
BOOL isRNRunning;
}
- 我们还需要一个
@property
引用userNameField
,我们将在后面的步骤中将其连接到UITextField
:
@property (weak, nonatomic) IBOutlet UITextField *userNameField;
@end
- 在这个参考下面,我们将开始定义类方法。我们将从
sourceURLForBridge
方法开始,该方法定义了 React Native 应用程序的服务位置。在我们的情况下,应用程序的 URL 应该是http://localhost:8081/index.bundle?platform=ios
,这指向了 React Native 应用程序的index.js
文件,一旦它使用react-native start
命令运行:
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
NSURL *jsCodeLocation = [NSURL
URLWithString:@"http://localhost:8081/index.bundle?platform=ios"];
return jsCodeLocation;
}
- 我们将保留
viewDidLoad
和didReveiveMemoryWarning
方法不变:
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- 接下来,我们需要更新
openRNAppEmbeddedButtonPressed
方法。注意moduleName
属性设置为FromNativeToRN
。这是我们导出 React Native 应用程序时给出的名称的引用,我们将在后面的步骤中定义。这次,我们还定义了一个userName
属性,用于向 React Native 层传递数据:
- (IBAction)openRNAppEmbeddedButtonPressed:(id)sender {
NSString *userName = _userNameField.text;
NSDictionary *props = @{@"userName" : userName};
if(_bridge == nil) {
_bridge = [[RCTBridge alloc] initWithDelegate:self
launchOptions:nil];
}
RCTRootView *rootView =
[[RCTRootView alloc] initWithBridge :_bridge
moduleName : @"FromNativeToRN"
initialProperties : props];
isRNRunning = true;
[embeddedViewController setView:rootView];
}
- 我们还需要一个
onUserNameChanged
方法。这是将数据实际发送到 React Native 层的方法。我们在这里定义的事件名称是UserNameChanged
,我们将在后面的步骤中在 React Native 层中引用它。这也将传递当前文本输入中的文本,该文本将被命名为userNameField
:
- (IBAction)onUserNameChanged:(id)sender {
if(isRNRunning == YES && _userNameField.text.length > 3) {
[_bridge.eventDispatcher sendAppEventWithName:@"UserNameChanged" body:@{@"userName" : _userNameField.text}];
}
}
- 我们还需要
prepareForSegue
来配置embeddedViewController
,就在它显示之前:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"embed"]) {
embeddedViewController = segue.destinationViewController;
}
}
@end
- 回到
Main.storyboard
,让我们添加一个文本字段,以及一个定义输入用途的标签。您还可以将输入命名为 User Name Field,以便在视图控制器场景中更容易识别:
- 接下来,我们需要为
User Name Field
文本输入的文本更改事件和引用出口进行连接,以便视图控制器知道如何引用它。这两者都可以通过连接检查器完成,连接检查器可以通过右侧面板顶部的最后一个按钮(图标是一个右指向箭头在一个圆圈中)访问。选择文本输入后,从Editing Changed
拖动到视图控制器(通过主故事板表示),并选择我们在步骤 7中定义的onUserNameChange
方法。然后,通过将项目拖动到ViewController
来创建以下连接。类似地,通过从新引用出口拖动到视图控制器,这次选择我们在步骤 7中定位的 userNameField 值,添加一个新的引用出口。您的连接检查器设置现在应该如下所示:
-
我们现在已经完成了原生应用程序中所需的步骤。让我们继续进行 React Native 层。回到
index.js
文件,我们将从导入开始。请注意,我们现在包括了NativeAppEventEmitter
。 -
将以下函数放在类定义内部:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
Text,
NativeAppEventEmitter
} from 'react-native';
- 我们将应用程序命名为
FromNativeToRN
,以匹配我们在步骤 6中定义的原生层中的模块名称,使用AppRegistry.registerComponent
来注册具有相同名称的应用程序。我们还将保留基本样式。
class FromNativeToRN extends Component {
// Defined in following steps
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});
AppRegistry.registerComponent('FromNativeToRN', () => FromNativeToRN);
- 我们将设置一个初始的
state
对象,其中包含一个userName
字符串属性,用于存储和显示从原生层接收到的文本:
class FromNativeToRN extends Component {
state = {
userName: ''
}
// Defined in following steps
}
- 传递到 React Native 层的
userName
值将作为属性接收。当组件挂载时,我们希望做两件事:如果原生层已经定义了userName
状态属性,则设置userName
状态属性,并将事件监听器连接到在原生层中更新userName
时更新userName
。回想一下,在步骤 7中,我们定义了事件的名称为UserNameChanged
,这就是我们要监听的事件。当接收到事件时,我们将更新state.userName
为事件传递的文本:
componentWillMount() {
this.setState({
userName : this.props.userName
});
NativeAppEventEmitter.addListener('UserNameChanged', (body) => {
this.setState({userName : body.userName});
});
}
- 最后,我们可以添加
render
函数,它简单地渲染state.userName
中存储的值:
render() {
return (
<View style={styles.container}>
<Text>Hello {this.state.userName}</Text>
</View>
);
}
- 是时候运行我们的应用程序了!首先,在项目的根目录中,我们可以使用以下命令通过 React Native CLI 启动 React Native 应用程序:
react-native start
接着我们通过 Xcode 在模拟器中运行原生应用程序:
从 React Native 通信到 iOS 应用程序容器
上一个示例涵盖了在原生到 React Native 方向上的层间通信。在这个示例中,我们将涵盖在相反方向上的通信:从 React Native 到原生。这次,我们将在 React Native 应用程序中渲染一个用户输入元素,并设置从 React Native 到在原生应用程序中渲染的 UI 组件的单向绑定。
准备工作
就像上一个示例一样,这个示例取决于本章第一个应用程序的最终产品,在将 React Native 应用程序和原生 iOS 应用程序组合示例中。要跟着做,请确保你已经完成了那个示例。
如何做...
- 让我们从原生层开始。通过
.xcworkspace
文件在 Xcode 中打开EmbeddedApp
原生应用程序。我们首先要在ViewController.m
中添加导入:
#import "ViewController.h"
#import "EmbeddedViewController.h"
#import <React/RCTRootView.h>
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
- 与上一个示例一样,我们需要通过
ViewController
接口添加对 React Native 桥的引用,提供原生控制器和 React Native 代码之间的桥接:
@interface ViewController () <RCTBridgeDelegate> {
EmbeddedViewController *embeddedViewController;
RCTBridge *_bridge;
BOOL isRNRunning;
}
- 我们还需要一个
@property
引用userNameField
,我们将在后面的步骤中将其连接到UITextField
:
@property (weak, nonatomic) IBOutlet UITextField *userNameField;
@end
- 让我们继续定义
@implementation
。同样,我们必须提供 React Native 应用程序的源,它将从localhost
提供:
@implementation ViewController
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge {
NSURL *jsCodeLocation = [NSURL
URLWithString:@"http://localhost:8081/index.bundle?platform=ios"];
return jsCodeLocation;
}
- 使用
viewDidLoad
方法,我们还可以将控制器连接到在容器视图中打开 React Native 应用程序的方法(openRNAppEmbeddedButtonPressed
)。我们将保持didReveiveMemoryWarning
方法不变:
- (void)viewDidLoad {
[super viewDidLoad];
[self openRNAppEmbeddedButtonPressed:nil];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- 与上一个配方一样,我们需要更新
openRNAppEmbeddedButtonPressed
方法。这次,moduleName
属性设置为FromRNToNative
,以反映我们在导出时将给 React Native 应用程序的名称,如后面的步骤中定义的。我们还定义了一个userName
属性,用于向 React Native 层传递数据:
- (IBAction)openRNAppEmbeddedButtonPressed:(id)sender {
if(_bridge == nil) {
_bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:nil];
}
RCTRootView *rootView =
[[RCTRootView alloc] initWithBridge :_bridge
moduleName : @"FromRNToNative"
initialProperties : nil];
isRNRunning = true;
[embeddedViewController setView:rootView];
}
- 我们在这个文件中还需要的最后两个方法是
prepareForSegue
,用于在显示之前配置embeddedViewController
,以及一个updateUserNameField
方法,当我们在本地层的文本输入中使用用户的新文本更新时将被触发:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if([segue.identifier isEqualToString:@"embed"]) {
embeddedViewController = segue.destinationViewController;
}
}
-(void) updateUserNameField:(NSString *)userName {
[_userNameField setText:userName];
}
@end
- 与上一个配方不同,我们还需要更新
ViewController
头文件(ViewController.h
)。在这里引用的方法updateUserNameField
将在我们定义ViewController
实现时使用:
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
- (void) updateUserNameField:(NSString *)userName;
@end
- 接下来,我们需要创建一个新的
UserNameManager
本地模块。首先,创建一个名为UserNameManager
的 Cocoa Touch 类。创建后,让我们打开实现文件(UserNameManger.m
)并添加我们的导入:
#import "UserNameManager.h"
#import "AppDelegate.h"
#import "ViewController.h"
#import <React/RCTBridgeModule.h>
要深入了解创建本地模块,请参阅第十一章中的公开自定义 iOS 模块配方。
- 接下来,我们将定义类实现。这里的主要要点是
setUserName
方法,这是我们从本地层导出供 React Native 应用程序使用的方法。我们将在 React Native 应用程序中使用此方法来更新本地文本字段中的值。然而,由于我们正在更新本地 UI 组件,操作必须在主线程上执行。这就是methodQueue
函数的目的,它指示模块在主线程上执行:
@implementation UserNameManager
RCT_EXPORT_MODULE();
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
RCT_EXPORT_METHOD(setUserName: (NSString *)userName) {
AppDelegate *delegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
ViewController *controller = (ViewController *)delegate.window.rootViewController;
[controller updateUserNameField:userName];
}
@end
- 我们还需要更新
UserNameMangager.h
头文件以使用 React Native 桥接模块:
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
@interface UserNameManager : NSObject <RCTBridgeModule>
@end
- 与上一个配方一样,我们需要为用户名输入添加一个文本字段和标签:
- 我们还需要从上一组中创建的文本字段到我们的
userNameField
属性添加一个引用输出:
如果您需要更多关于如何创建引用输出的信息,请查看上一个配方的步骤 10。
- 我们已经完成了这个项目的本地部分,现在让我们转向我们的 React Native 代码。让我们打开项目根目录下的
index.js
文件。我们将从导入开始:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
Text,
TextInput,
NativeModules
} from 'react-native';
- 让我们使用名称
FromRNToNative
来定义应用程序,以便与我们在原生代码中步骤 6中声明的moduleName
对齐,并使用相同名称注册组件。state
对象只需要一个userName
字符串属性来保存保存到TextInput
组件的值,我们将在组件的render
函数中添加它:
class FromRNToNative extends Component {
state = {
userName: ''
}
// Defined on next step
}
AppRegistry.registerComponent('FromRNToNative', () => FromRNToNative);
- 应用程序的
render
函数使用TextInput
组件从用户那里获取输入,然后通过 React Native 桥将其发送到原生应用程序。它通过在TextInput
的值改变时调用onUserNameChange
方法来实现这一点:
render() {
return (
<View style={styles.container}>
<Text>Enter User Name</Text>
<TextInput
style={styles.userNameField}
onChangeText={this.onUserNameChange}
value={this.state.userName}
/>
</View>
);
}
- 我们需要做的最后一件事是定义
onUserNameChange
方法,该方法由我们在上一步中定义的TextInput
组件的onChangeText
属性使用。此方法将state.userName
更新为文本输入中的值,并通过 React Native 中的NativeModules
组件将该值发送到原生代码。NativeModules
具有我们在原生层步骤 9中定义为 Cocoa Touch 类的UserNameManager
类。我们在步骤 10中调用了我们在类中定义的setUserName
方法,将该值传递到原生层,在那里它将显示在我们在步骤 12中创建的文本字段中:
onUserNameChange = (userName) => {
this.setState({userName});
NativeModules.UserNameManager.setUserName(userName);
}
- 应用程序完成了!返回到项目的根目录,使用以下命令启动 React Native 应用程序:
react-native start
然后,启动 React Native 应用程序后,从 Xcode 运行原生EmbeddedApp
项目。现在,React Native 应用程序中的输入应该将其值传递给父原生应用程序中的输入:
它是如何工作的...
为了从我们的 React Native 应用程序通信到父原生应用程序,我们创建了一个名为UserNameManager
的原生模块,其中包含一个setUserName
方法,我们从原生层导出,并在 React Native 应用程序中使用,在其onUserNameChange
方法中。这是从 React Native 到原生通信的推荐方式。
处理被外部 iOS 应用程序调用
原生应用程序之间通过链接进行通信也是一种常见行为,并且通常提示用户使用短语“在...中打开”,以及更好地处理操作的应用程序的名称。这是通过使用特定于您的应用程序的协议来完成的。就像任何网站链接都有http://
或https://
的协议一样,我们也可以创建一个自定义协议,允许任何其他应用程序打开并向我们的应用程序发送数据。
在这个教程中,我们将创建一个名为invoked://
的自定义协议。通过使用invoked://
协议,任何其他应用程序都可以使用它来运行我们的应用程序并向其传递数据。
准备工作
对于这个教程,我们将从一个新的原生 React Native 应用程序开始。让我们将其命名为InvokeFromNative
。
如何操作...
- 首先,让我们在 Xcode 中打开新项目的本地层。我们需要做的第一件事是调整项目的构建设置。这可以通过在左侧面板中选择根项目,然后选择中间面板顶部的构建设置选项卡来完成:
- 我们需要向
Header Search Paths
字段添加一个新条目:
为了使项目知道 React Native JavaScript 的位置,它需要$(SRCROOT)/../node_modules/react-native/Libraries
的值。让我们将其添加为递归条目:
- 我们还需要注册我们的自定义协议,这将被其他应用程序使用。打开
Info.plist
文件作为源代码(右键单击然后选择Open As | Source Code
)。让我们向文件添加一个条目,以注册我们的应用程序在invoked://
协议下:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>invoked</string>
</array>
</dict>
</array>
- 接下来,我们需要将
RCTLinkingManager
添加到AppDelegate
实现中,它位于AppDelegate.m
中,并将其连接到我们的应用程序:
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#import <React/RCTLinkingManager.h>
@implementation AppDelegate
// The rest of the AppDelegate implementation
- (BOOL)application:(UIApplication *)application
openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options
{
return [RCTLinkingManager application:application openURL:url options:options];
}
@end
- 现在,让我们继续进行 React Native 层。在
index.js
中,我们将添加我们的导入,其中包括Linking
组件:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Text,
View,
Linking
} from 'react-native';
- 接下来,我们将创建类定义并将组件注册为
InvokeFromNative
。我们还将定义一个初始的state
对象,其中包含一个status
字符串属性,其值为'App Running'
:
class InvokeFromNative extends Component {
state = {
status: 'App Running'
}
// Defined on following steps
}
AppRegistry.registerComponent('InvokeFromNative', () => InvokeFromNative);
- 现在,我们将使用挂载和卸载生命周期钩子来
add
/remove
对invoked://
协议的事件监听器。当事件被听到时,将触发下一步中定义的onAppInvoked
方法:
componentWillMount() {
Linking.addEventListener('url', this.onAppInvoked);
}
componentWillUnmount() {
Linking.removeEventListener('url', this.onAppInvoked);
}
onAppInvoked
函数简单地接受事件监听器的事件并更新state.status
以反映发生了调用,通过event.url
显示协议:
onAppInvoked = (event) => {
this.setState({
status: `App Invoked by ${ event.url }`
});
}
- 在这个教程中,
render
方法的唯一真正目的是在状态上呈现status
属性:
render() {
return (
<View style={styles.container}>
<Text style={styles.instructions}>
App Status:
</Text>
<Text style={styles.welcome}>
{this.state.status}
</Text>
</View>
);
}
- 我们还将添加一些基本样式来居中和调整文本的大小:
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
- 我们的应用程序已经完成。一旦您开始运行应用程序,您应该会看到类似于这样的东西:
- 应用程序运行时,我们可以模拟另一个应用程序打开我们的 React Native 应用程序的操作,使用
invoked://
协议可以通过以下终端命令完成:
xcrun simctl openurl booted invoked://
一旦调用,应用程序应更新以反映调用:
工作原理…
在这个配方中,我们介绍了如何注册自定义协议(或 URL 模式),以允许我们的应用程序被其他应用程序调用。这个配方的目的是尽可能简单地保持我们的示例,因此我们没有构建通过链接机制传递给应用程序的数据处理。但是,如果您的应用程序需要,完全可以这样做。要深入了解Linking
组件,请查看官方文档facebook.github.io/react-native/docs/linking
。
结合 React Native 应用程序和本机 Android 应用程序
由于 Android 平台仍然在智能手机市场占据主导地位,您可能希望为 Android 和 iOS 构建应用程序。React Native 开发的一个重大优势是使这一过程更加容易。但是,当您想要使用 React Native 为已经发布的 Android 应用程序编写新功能时会发生什么?幸运的是,React Native 也可以实现这一点。
本文将介绍在现有 Android 应用程序中嵌入 React Native 应用程序的过程,方法是在容器视图中显示 React Native 应用程序。这些步骤将作为后续配方的基线,这些配方涉及与 React Native 应用程序的通信。
准备工作
在本节中,我们将使用 Android Studio 创建一个名为EmbedApp
的示例 Android 应用程序。如果您有一个基本的 Android 应用程序要使用,可以跳过这些步骤并继续进行实际实现:
-
打开 Android Studio 并创建一个新项目(文件|新项目)
-
将应用程序名称设置为
EmbeddedApp
并填写您的公司域。按“下一步” -
保留“空活动”作为默认选择,然后按“下一步”
-
将 Activity 属性保留为默认值,然后按“完成”
如何做…
- 此时,我们的应用程序没有与 React Native 相关的引用,因此我们将从安装它开始。在应用程序的根文件夹中,在终端中使用
yarn
命令安装 React Native:
yarn add react-native
或者,您可以使用npm
:
npm install react-native --save
- 我们还需要一个用于启动 React Native 应用程序的 Node.js 脚本。让我们打开
package.json
并将以下属性添加为scripts
对象的成员:
"start": "node node_modules/react-native/local-cli/cli.js start"
- 对于这个示例,我们只需要一个非常简单的 React Native 应用程序。让我们创建一个带有以下样板应用程序的
index.android.js
文件:
import React, { Component } from 'react';
import { AppRegistry, StyleSheet, View, Text } from 'react-native';
export default class EmbedApp extends Component {
render() {
return (<View style={styles.container}>
<Text>Hello in React Native</Text>
</View>);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center', backgroundColor: '#F5FCFF'
}
});
AppRegistry.registerComponent('EmbedApp', () => EmbedApp);
将此文件命名为index.android.js
表示对 React Native 来说,此代码仅适用于此应用程序的 Android 版本。这是官方文档推荐的做法,当平台特定的代码更复杂时。您可以在facebook.github.io/react-native/docs/platform-specific-code#platform-specific-extensions
了解更多信息。
- 让我们返回到 Android Studio 并打开
build.gradle
文件(来自app
模块),并将以下内容添加到dependencies
中:
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "com.android.support:appcompat-v7:27.1.1"
implementation "com.facebook.react:react-native:+" // From node_modules
}
- 我们还需要一个对本地 React Native maven 目录的引用。打开另一个
build.gradle
文件,并将以下行添加到allprojects.repositories
对象中:
allprojects {
repositories {
mavenLocal()
maven {
url "$rootDir/../node_modules/react-native/android"
}
google()
jcenter()
}
}
- 接下来,让我们更新应用程序的权限以使用互联网和系统警报窗口。我们将打开
AndroidManifest.xml
并将以下权限添加到<manifest>
节点:
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="com.warlyware.embeddedapp">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<application
android:name=".EmbedApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
- 我们准备更新
MainApplication
Java 类。这里的getUseDeveloperSupport
方法将启用开发菜单。getPackages
方法是应用程序使用的包的列表,只包括MainReactPackage()
,因为我们只使用主要的 React 包。getJSMainModuleName
方法返回index.android
字符串,它指的是 React Native 层中的index.android.js
文件:
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 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.<ReactPackage>asList(
new MainReactPackage()
);
}
};
@Override
public ReactNativeHost getReactNativeHost() {
return mReactNativeHost;
}
@Override
protected String getJSMainModuleName() {
return "index.android";
}
}
- 接下来,让我们创建另一个名为
ReactFragment
的新 Java 类。这个类需要三个方法:OnAttach
在片段附加到主活动时调用,OnCreateView
实例化片段的视图,OnActivityCreated
在活动被创建时调用:
import android.app.Fragment;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.ReactRootView;
public abstract class ReactFragment extends Fragment {
private ReactRootView mReactRootView;
private ReactInstanceManager mReactInstanceManager;
// This method returns the name of our top-level component to show
public abstract String getMainComponentName();
@Override
public void onAttach(Context context) {
super.onAttach(context);
mReactRootView = new ReactRootView(context);
mReactInstanceManager =
((EmbedApp) getActivity().getApplication())
.getReactNativeHost()
.getReactInstanceManager();
}
@Override
public ReactRootView onCreateView(LayoutInflater inflater, ViewGroup group, Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
return mReactRootView;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
mReactRootView.startReactApplication(
mReactInstanceManager,
getMainComponentName(),
getArguments()
);
}
}
- 最后,创建一个名为
EmbedFragment
的 Java 类,它将扩展ReactFragment
:
import android.os.Bundle;
public class EmbedFragment extends ReactFragment {
@Override
public String getMainComponentName() {
return "EmbedApp";
}
}
-
让我们打开
MainActivity.java
并在类定义中添加implements DefaultHardwareBackBtnHandler
以处理硬件返回按钮事件。您可以在此处查看此 React Native 类的带注释源代码:github.com/facebook/react-native/blob/master/ReactAndroid/src/main/java/com/facebook/react/modules/core/DefaultHardwareBackBtnHandler.java
。 -
我们还将向类中添加一些方法。
onCreate
方法将把内容视图设置为主活动,并添加一个 FAB 按钮,当点击时,将实例化我们在步骤 10中定义的EmbedFragment
的新实例。EmbedFragment
的这个实例由片段管理器用于将 React Native 应用添加到视图中。其余方法处理设备系统按钮被按下时发生的事件(如返回、暂停和恢复按钮):
import android.app.Fragment;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.View;
import com.facebook.react.ReactInstanceManager;
import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler;
public class MainActivity extends AppCompatActivity implements DefaultHardwareBackBtnHandler {
private ReactInstanceManager mReactInstanceManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Fragment viewFragment = new EmbedFragment();
getFragmentManager().beginTransaction().add(R.id.reactnativeembed, viewFragment).commit(); }
});
mReactInstanceManager = ((EmbedApp) getApplication()).getReactNativeHost().getReactInstanceManager();
}
@Override
public void invokeDefaultOnBackPressed() {
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostPause(this);
}
}
@Override
protected void onResume() {
super.onResume();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostResume(this, this);
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mReactInstanceManager != null) {
mReactInstanceManager.onHostDestroy(this);
}
}
@Override
public void onBackPressed() {
if (mReactInstanceManager != null) {
mReactInstanceManager.onBackPressed();
} else {
super.onBackPressed();
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_MENU && mReactInstanceManager != null) {
mReactInstanceManager.showDevOptionsDialog();
return true;
}
return super.onKeyUp(keyCode, event);
}
}
- 最后一步是在片段加载时添加一些布局设置。我们需要编辑位于
/res
文件夹中的content_main.xml
文件。这是视图的主要内容。它包含我们将附加片段的容器视图(FrameLayout
),以及其他本机元素应该显示的内容:
<FrameLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:id="@+id/reactnativeembed"
android:background="#FFF">
</FrameLayout>
- 在终端中运行以下命令:
react-native start
这将构建和托管 React Native 应用。现在,我们可以在 Android 模拟器中打开应用。按下 FAB 按钮后,您将看到以下内容:
工作原理...
为了在我们的 Android 应用程序中呈现 React Native,我们需要执行一些步骤。首先,我们需要定义一个实现ReactApplication
接口的Application
类。然后,我们需要创建一个负责实例化和呈现ReactRootView
的Fragment
。通过片段,我们能够在我们的MainActivity
中呈现 React Native 视图。在这个教程中,我们将片段添加到我们的片段容器视图中。这实质上用 React Native 应用程序替换了所有应用程序内容。
在这个教程中,我们涵盖了大量的集成代码。要更深入地了解每个部分的工作原理,您可以阅读官方文档facebook.github.io/react-native/docs/integration-with-existing-apps.html
。
从 Android 应用程序到 React Native 的通信
现在我们已经介绍了如何在将 React Native 应用程序和本机 Android 应用程序结合的教程中渲染我们的 React Native 应用程序,我们准备迈出下一步。我们的 React Native 应用程序应该不仅仅是一个虚拟 UI。它应该能够对其父应用程序中正在进行的操作做出反应。
在这个教程中,我们将完成从我们的 Android 应用程序发送数据到我们嵌入的 React Native 应用程序。当 React Native 应用程序首次实例化时,它可以接受数据,然后在运行时。我们将介绍如何完成这两种方法。这个教程将在 Android 应用程序中使用EditText
,并设置单向绑定到 React Native 应用程序。
准备工作
对于这个教程,请确保您有一个嵌入了 React Native 应用程序的 Android 应用程序。如果您需要指导来完成这一点,请完成将 React Native 应用程序和本机 Android 应用程序结合的教程。
如何做...
-
在 Android Studio 中,打开 React Native 应用程序的 Android 部分。首先,我们需要编辑
content_main.xml
。 -
对于这个应用程序,我们只需要一个非常简单的布局。您可以通过按下底部的“文本”选项卡来编辑文件,打开源编辑器并添加/替换以下节点:
<TextView android: layout_width = "wrap_content"
android: layout_height = "wrap_content"
android: text = "Press the Mail Icon to start the React Native application"
android: id = "@+id/textView" />
<FrameLayout android: layout_width = "match_parent"
android: layout_height = "300dp"
android: layout_centerVertical = "true"
android: layout_alignParentStart = "true"
android: id = "@+id/reactnativeembed"
android: background = "#FFF" >
</FrameLayout>
<LinearLayout android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_below="@+id/textView"
android:layout_centerHorizontal="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="User Name:"
android:id="@ + id / textView2"
android:layout_weight="0.14 " />
<EditText android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@ + id / userName"
android:layout_weight="0.78"
android:inputType="text"
android:singleLine="true"
android:imeOptions="actionDone"/>
</LinearLayout>
- 打开
MainActivity.java
并添加以下类字段:
private ReactInstanceManager mReactInstanceManager;
private EditText userNameField;
private Boolean isRNRunning = false;
- 在“onCreate”方法中,使用以下代码设置
userNameField
属性:
userNameField = (EditText) findViewById(R.id.userName);
- 我们将使用 FAB 按钮来更新 Android 应用程序的内容为我们的 React Native 应用程序。我们需要用以下内容替换
FloatingActionButtononClickListener
:
fab.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View view) {
Fragment viewFragment = new EmbedFragment();
if (userNameField.getText().length() > 0) {
Bundle launchOptions = new Bundle();
launchOptions.putString("userName",
userNameField.getText().toString());
viewFragment.setArguments(launchOptions);
}
getFragmentManager().beginTransaction().add(R.id.reactnativeembed, viewFragment).commit();
isRNRunning = true;
}
});
- 接下来,我们需要在
onCreate
方法中为我们的userNameField
添加一个TextChangedListener
:
userNameField.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override public void afterTextChanged(Editable s) {
if (isRNRunning) {
sendUserNameChange(s.toString());
}
}
});
- 我们需要为我们的
Activity
做的最后一项更改是添加方法,将事件发送到 React Native 桥接中:
private void sendUserNameChange(String userName) {
WritableMap params = Arguments.createMap();
params.putString("userName", userName);
sendReactEvent("UserNameChanged", params);
}
private void sendReactEvent(String eventName, WritableMap params) {
mReactInstanceManager.getCurrentReactContext()
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit(eventName, params);
}
- 让我们回到 JavaScript 层。我们将使用
NativeAppEventEmitter
组件的addListener
方法来监听从本机 Android 代码发送的UserNameChanged
事件,并使用事件中的数据更新state.userName
:
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
View,
Text,
NativeAppEventEmitter
} from 'react-native';
export default class EmbedApp extends Component<{}> {
componentWillMount() {
this.setState({
userName : this.props.userName
});
NativeAppEventEmitter.addListener('UserNameChanged', (body) => {
this.setState({userName : body.userName});
});
}
render() {
return (
<View style={styles.container}>
<Text>Hello {this.state.userName}</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('EmbedApp', () => EmbedApp);
- 现在,如果您运行应用程序,您可以在“用户名”字段中输入文本,并启动 React Native 应用程序:
它是如何工作的...
在这个示例中,我们将片段呈现为内联视图。在步骤 2中,我们添加了一个空的FrameLayout
,我们在步骤 5中将其定位为呈现片段。通过使用 React Native 桥接器RCTDeviceEventEmitter
来实现绑定功能。这最初是设计用于与本地模块一起使用的,但只要您可以访问ReactContext
实例,就可以将其用于与 React Native JavaScript 层的任何通信。
从 React Native 通信到 Android 应用程序容器
正如我们在上一个示例中讨论的那样,让我们的嵌入式应用程序了解其周围发生的事情是非常有益的。我们还应该努力让我们的 Android 父应用程序了解 React Native 应用程序内部发生的事情。应用程序不仅应能执行业务逻辑-还应能更新其 UI 以反映嵌入应用程序中的更改。
这个示例向我们展示了如何利用本地模块来更新在 Android 应用程序内部创建的本地 UI。我们的 React Native 应用程序中将有一个文本字段,用于更新在主机 Android 应用程序中呈现的文本字段。
准备工作
对于这个示例,请确保您有一个嵌入了 React Native 应用程序的 Android 应用程序。如果您需要指导来完成这个任务,请完成组合 React Native 应用程序和本地 Android 应用程序示例。
如何做...
-
打开 Android Studio 到您的项目并打开
content_main.xml
。 -
按底部的 Text 标签打开源编辑器,并添加/替换以下节点:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.embedapp.MainActivity"
tools:showIn="@layout/activity_main">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Press the Mail Icon to start the React Native application"
android:id="@+id/textView" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:id="@+id/reactnativeembed"
android:background="#FFF"></FrameLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="75dp"
android:layout_below="@+id/textView"
android:layout_centerHorizontal="true">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="User Name:"
android:id="@+id/textView2"
android:layout_weight="0.14" />
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/userName"
android:layout_weight="0.78"
android:inputType="text"
android:singleLine="true"
android:imeOptions="actionDone"/>
</LinearLayout>
</RelativeLayout>
- 创建一个名为
UserNameManager
的 Java 类。这将是一个本地模块,用于更新我们添加到布局中的EditTextfield
的目的。
如果您不熟悉为 React Native 创建本地模块,请参阅第十一章中的公开自定义 Android 模块示例,添加本地功能。
UserNameManager.java
中的大部分工作都是在setUserName
方法中完成的。在这里,Android 层根据从 React Native 层发送的内容更新视图的文本内容。React 方法不一定会在主 UI 线程上运行,因此我们使用mainActivity.runOnUiThread
在主 UI 线程准备好时更新视图:
public class UserNameManager extends ReactContextBaseJavaModule {
public UserNameManager(ReactApplicationContext reactApplicationContext) {
super(reactApplicationContext);
}
@Override public String getName() {
return "UserNameManager";
}
@ReactMethod public void setUserName(final String userName) {
Activity mainActivity = getReactApplicationContext().getCurrentActivity();
final EditText userNameField = (EditText) mainActivity.findViewById(R.id.userName);
mainActivity.runOnUiThread(new Runnable() {
@Override public void run() {
userNameField.setText(userName);
}
});
}
}
- 要导出
UserNameManager
模块,我们需要编辑UserNamePackage
Java 类。我们可以通过调用modules.add
将其导出到 React Native 层,传入一个以reactContext
为参数的新UserNameManager
:
public class UserNamePackage implements ReactPackage {
@Override public List < Class << ? extends JavaScriptModule >> createJSModules() {
return Collections.emptyList();
}
@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 UserNameManager(reactContext));
return modules;
}
}
- 在
MainApplication
的getPackages
方法中添加UserNamePackage
:
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new UserNamePackage()
);
}
-
现在,我们需要让我们的 React Native UI 渲染一个
TextField
并调用我们的UserNameManager
本地模块。打开index.android.js
并从'react-native'
导入TextInput
和NativeModules
模块。 -
为
UserNameManager
创建一个变量引用:
const UserNameManager = NativeModules.UserNameManager;
- React Native 应用程序只需要一个
TextInput
来操作state
对象上的userName
属性:
let state = {
userName: ''
}
onUserNameChange = (userName) => {
this.setState({
userName
});
UserNameManager.setUserName(userName);
}
render() {
return (
<View style={styles.container}>
<Text>Embedded RN App</Text>
<Text>Enter User Name</Text>
<TextInput style={styles.userNameField}
onChangeText={this.onUserNameChange}
value={this.state.userName}
/>
</View>
);
}
- 运行应用程序,启动 React Native 嵌入式应用程序,并向文本字段添加文本,您应该看到类似于以下截图所示的内容:
工作原理...
为了使我们的 React Native 应用程序更新本机应用程序容器,我们创建了一个本机模块。这是从 JavaScript 通信到本机层的推荐方式。但是,由于我们必须更新本机 UI 组件,因此操作必须在主线程上执行。这是通过获取对MainActivity
的引用并调用runOnUiThread
方法来实现的。这是在步骤 4的setUserName
方法中完成的。
处理外部 Android 应用程序调用
在本章的前面,我们介绍了如何在 iOS 中处理外部应用程序的调用,在处理外部 Android 应用程序调用中。在这个配方中,我们将介绍 Android 中深度链接的相同概念。
如何做...
-
让我们首先在 Android Studio 中打开 React Native Android 项目,并导航到
AndroidManifest.xml
。 -
对于我们的示例,我们将在
invoked://scheme
下注册我们的应用程序。我们将更新<activity>
节点如下:
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
有关此intent-filter
的工作原理的更多信息,请参阅官方 Android 文档developer.android.com/training/app-links/deep-linking
。
- 接下来,我们需要创建一个简单的 React Native 应用程序,其 UI 对被调用做出反应。让我们打开
index.android.js
文件。我们将从'react-native'
的import
块中导入Linking
模块:
import React from 'react';
import { Platform, Text, Linking } from 'react-native';
- 让我们为 React Native 应用构建
App
类。当组件挂载时,我们将使用一个名为url
的事件注册一个Linking
事件监听器。当这个事件发生时,onAppInvoked
将被触发,更新状态的status
属性,以及传递给回调函数的事件:
export default class App extends React.Component {
state = {
status: 'App Running'
}
componentWillMount() {
Linking.addEventListener('url', this.onAppInvoked);
}
componentWillUnmount() {
Linking.removeEventListener('url', this.onAppInvoked);
}
onAppInvoked = (event) => {
this.setState({ status: `App Invoked by ${event.url}` });
}
render() {
return (
<View style={styles.container}>
<Text style={styles.instructions}>
App Status:
</Text>
<Text style={styles.welcome}>
{this.state.status}
</Text>
</View>
);
}
}
- 运行应用程序并从另一个应用程序调用它将看起来像这样:
工作原理...
在这个教程中,我们通过编辑步骤 2中的AndroidManifest.xml
文件来注册我们的 URL 模式以进行链接。需要注意的一点是将launchMode
更改为singleTask
。这可以防止操作系统创建我们的 React 活动的多个实例。如果你想要能够正确捕获随意图传递的数据,这一点非常重要。
第十四章:部署您的应用程序
在本章中,我们将介绍以下配方:
-
将开发构建部署到 iOS 设备
-
将开发构建部署到 Android 设备
-
将测试构建部署到 HockeyApp
-
将 iOS 测试构建部署到 TestFlight
-
将生产构建部署到 Apple 应用商店
-
将生产构建部署到 Google Play 商店
-
部署空中更新
-
优化 React Native 应用程序大小
介绍
如果您是独立开发者,您可能会经历几个不同的开发阶段。第一阶段将使您在个人 iOS 或 Android 设备上测试您的应用程序。在耗尽这个阶段之后,您可能会想要与一小部分人分享它,以获得用户反馈。最终,您将达到一个可以通过应用商店发布应用程序的阶段。本章将逐个介绍这些阶段,并涵盖推送更新到您的应用程序以及一些优化技巧。
将开发构建部署到 iOS 设备
在开发过程中,您可能会花费大部分时间使用 Xcode 安装的 iOS 模拟器测试您的 iOS 应用程序。虽然 iOS 模拟器是迄今为止性能最好且最接近在 iOS 设备上运行应用程序的方法,但它仍然不同于真实设备。iOS 模拟器使用计算机的 CPU 和 GPU 来渲染模拟的操作系统,因此根据您的开发机器,它可能表现得比实际设备更好(或更差)。
幸运的是,Expo 能够在实际设备上测试运行代码,这让我们离真正的最终产品更近了一步,但最终应用程序和在 Expo 中运行的开发应用程序之间仍然存在差异。如果您正在构建纯 React Native 应用程序,您将无法像使用 Expo 那样轻松地在设备上运行应用程序。
无论如何,您最终都希望在实际设备上测试真正的应用程序,以便体验最终产品的实际用户体验和性能。
在本配方中,我们将指导您将 React Native 应用程序部署到 iPhone 或 iPad。
准备工作
我们只需要一个新的纯 React Native 应用程序,我们将其命名为TestDeployApp
。您可以通过以下命令创建该应用程序:
react-native init
另外,请确保您的 iOS 设备通过 USB 连接到开发机器。
如何做...
-
让我们首先在 Xcode 中打开新创建的 React Native iOS 项目。通过在左侧面板中选择项目的根目录来打开项目编辑器。
-
在项目编辑器的“常规”选项卡下,在左侧的“目标”部分中选择 iOS 应用程序。在“签名”部分下,选择您的团队,如下所示:
-
对于“目标”列表中的每个条目,重复此步骤两次。
-
在目标选择器中选择您的设备,如下所示:
- 要在连接的设备上开始运行应用程序,只需按下播放按钮。您必须确保您的设备已插入,已解锁,并且已信任,才能在 Xcode 的设备列表中显示出来。如果这是您在此设备上首次运行您开发的应用程序,您还需要调整设置以信任来自您的开发者帐户的应用程序。在 iOS 设备上,此设置可以在“设置”|“常规”|“设备管理”中找到。
它是如何工作的...
将我们的开发构建部署到设备上只涉及指定一个团队,然后像在 Xcode 模拟器上使用一样运行应用程序,但是将目标设备更改为已插入的设备。我们使用本地主机打包程序来创建我们的捆绑文件。然后将此文件保存在设备上以供将来使用。请注意,由于这是开发构建,因此代码尚未像在最终发布时那样进行优化。在转到生产版本时,您将看到显着的性能提升。
将开发构建部署到 Android 设备
在开发 Android 应用程序时,您最有可能会在 Android 模拟器上运行应用程序。虽然方便,但与真实的 Android 设备相比,模拟器的性能较差。
测试应用程序的最佳方法是使用实际的 Android 设备。本教程将介绍将 React Native 应用程序部署到实际的 Android 设备。
准备就绪
我们只需要一个新的纯 React Native 应用程序,我们将其命名为TestDeployApp
。您可以通过以下命令创建该应用程序:
react-native init
此外,请确保您的 iOS 设备通过 USB 连接到开发计算机。
如何做...
-
让我们首先在 Android Studio 中打开我们的 React Native Android 项目。
-
接下来,按照以下步骤按下运行按钮:
- 确保选择“选择运行设备”单选按钮,并且您的设备显示在列表中。按 OK 继续,如下所示:
还有更多...
当您运行应用程序时,React Native 打包程序应该会启动。如果没有,您将需要手动启动打包程序。如果您看到一个带有消息“无法获取 BatchedBridge”的错误屏幕,请确保您的捆绑包正确打包,或者“无法连接到开发服务器”,您应该能够通过在终端中运行以下命令来解决这个问题:
adb reverse tcp:8081 tcp:8081
工作原理...
与 Xcode 类似,我们可以通过简单地插入一个真实的设备,按下运行,然后选择应用程序应该运行的设备来运行我们的应用程序。可能出现的唯一复杂情况是设置设备和开发机之间的通信。这些问题通常可以通过以下命令解决:
adb reverse
这将从设备到主机计算机建立一个端口转发。这是一个开发版本,代码还没有优化,所以一旦应用程序作为生产版本构建,性能将会提高。
将测试版本部署到 HockeyApp
在将应用程序发布到市场之前,重要的是对应用程序进行压力测试,并在可能的情况下获得用户反馈。为了实现这一点,您需要创建一个签名的应用程序构建,可以与一组测试用户共享。对于一个强大的测试构建,您需要两样东西:应用程序性能的分析/报告,以及交付的机制。HockeyApp 为 iOS 和 Android 上的测试构建提供了这些以及更多功能。虽然苹果应用商店和谷歌 Play 商店的官方平台都提供了测试和分析功能,但 HockeyApp 提供了一个统一的处理这些问题的地方,以及度量、崩溃报告等的第二来源。
值得注意的是,HockeyApp 最近被微软收购。他们宣布 HockeyApp 产品将在 2019 年 11 月停止,并转而使用微软的 App Center。您可以在产品过渡页面上阅读更多信息hockeyapp.net/transition
。本食谱将介绍如何将 React Native 应用程序部署到 HockeyApp 进行测试。我们将介绍 iOS 和 Android 版本的发布。
准备就绪
对于这个食谱,我们将使用上两个食谱中的相同的空的、纯净的 React Native 应用,我们将其命名为TestDeployApp
。对于 iOS 部署,您需要加入苹果开发者计划,并且需要安装cocoapods
。安装cocoapods
的最简单方法是使用 homebrew,通过以下命令:
brew install cocoapods
您还需要拥有 HockeyApp 帐户,您可以在他们的网站 hockeyapp.net/
上注册。
如何做到...
- 首先,我们需要在我们的应用程序中安装
react-native-hockeyapp
模块。打开终端,转到应用程序的根项目目录,并输入以下命令:
npm install react-native-hockeyapp --save
- 进入你的
ios/
目录并初始化你的 Podfile:
pod init
-
打开你的 Podfile 并在你的目标中添加
pod "HockeySDK"
。 -
回到终端,按照以下步骤安装 Podfile:
pod install
-
现在,让我们打开 Xcode 并打开我们的 React Native 项目:(
ios/TestDeployApp.xcodeproj
)。 -
我建议将您的 Bundle 标识符更改为比默认值更有意义的内容,请在常规设置对话框中更改如下:
- 将
./ios/Pods/Pods.xcodeproj
拖放到项目导航器中的 Libraries 组中,如下所示:
-
将位于
./node_modules/react-native-hockeyapp/RNHockeyApp/RNHockeyApp
的RNHockeyApp.h
和RNHockeyApp.m
文件拖放到相同的 Libraries 组中。 -
接下来,我们将转到 HockeyApp 网站并在那里创建我们的应用程序。登录并点击新应用。
-
由于我们的构建还没有准备好,所以在下一个模态中点击手动,而不是上传构建?创建应用程序。
-
在创建应用程序表单中填写字段时,请确保与我们在 步骤 6 中定义的标题和 Bundle 标识符匹配,然后点击保存,如下所示:
-
记下应用程序 ID,因为我们将在下一步中使用它。
-
打开
App.js
并添加以下代码:
import HockeyApp from 'react-native-hockeyapp';
export default class
TestDeployApp extends Component {
componentWillMount() {
HockeyApp.configure(YOUR_APP_ID_HERE, true);
}
componentDidMount() {
HockeyApp.start();
HockeyApp.checkForUpdate();
}
}
- 回到 Xcode,将通用 iOS 设备设置为目标目标并构建(Product | Build)应用程序,如下所示:
-
现在,我们需要创建我们的
.ipa
文件。这可以通过 Xcode 菜单中的 Product | Archive 完成。 -
这将打开归档列表。点击分发应用程序按钮开始创建
.ipa
的过程。 -
选择开发选项并点击下一步。
-
您的配置团队应该会自动选择。选择正确的团队后,点击下一步。
-
保留默认的导出设置并点击下一步。在摘要页面上,也点击下一步。
-
选择目标目录并点击导出。
-
回到 HockeyApp 浏览器窗口,点击添加版本。
-
将刚刚导出的
.ipa
文件拖放到模态窗口中。 -
我们可以将设置保留为默认设置,所以继续按下一步,直到最后一个模态屏幕,然后在摘要屏幕上按下完成。这就是 iOS 应用的全部内容。您可以向HockeyApp应用添加用户,然后您的测试人员应该能够下载您的应用。让我们转到 Android 端。打开 Android Studio,然后打开 React Native 中的 Android 文件夹。
-
重复步骤 8到步骤 11,将平台更改为 Android,如下所示:
- 现在,我们需要构建我们的
.apk
文件。您可以在 React Native 文档中找到构建.apk
的最新方法,位于:
facebook.github.io/react-native/docs/signed-apk-android.html
- 重复从我们的 Android 项目生成的
.apk
的步骤 21和步骤 22。
它是如何工作的...
对于这个教程,我们使用HockeyApp的两个主要功能:其 beta 分发和其 HockeySDK(支持崩溃报告、指标、反馈、身份验证和更新通知)。对于 iOS,beta 分发是通过HockeyApp托管的 OTA 企业分发机制完成的。当您签署您的应用时,您可以控制哪些设备可以打开它。HockeyApp只是发送通知并提供 URL 供 beta 测试人员通过其企业应用商店下载您的应用。Android 更简单,因为不需要担心应用是如何传输的。这意味着HockeyApp将.apk
文件托管在测试人员可以下载和安装的 Web 服务器上。
有关在 Android 上设置HockeyApp的更多信息,您可以阅读官方文档support.hockeyapp.net/kb/client-integration-android/hockeyapp-for-android-sdk
。
将 iOS 测试构建部署到 TestFlight
在HockeyApp出现之前,用于测试移动应用程序的最流行的服务是 TestFlight。事实上,它在这方面做得非常好,以至于苹果收购了其母公司并将其整合到了 iTunes Connect 中。TestFlight 现在作为苹果的官方应用程序测试平台。TestFlight 和HockeyApp之间有一些区别需要考虑。首先,TestFlight 在被苹果收购后变成了仅适用于 iOS。其次,在 TestFlight 中有两种测试样式:内部和外部。内部测试涉及与团队的开发人员或管理员角色成员共享应用程序,并将分发限制为每个设备 10 个设备上的 25 个测试人员。外部测试允许您邀请最多 2,000 名不必是您组织成员的测试人员。这也意味着这些测试人员不会使用您的设备配额。外部测试应用程序需要经过苹果进行的Beta App Review,这并不像苹果对将应用程序发布到 App Store 的审查那样严格,但这是一个很好的第一步。
本食谱侧重于将我们的 React Native 应用程序部署到 TestFlight 进行测试构建。我们将设置一个内部测试,因为我们不希望苹果审查我们的示例 React Native 应用程序,但是内部和外部测试的程序是相同的。
准备工作
对于这个食谱,我们将使用之前食谱中的相同的 React Native 应用程序模板,我们将其命名为TestDeployApp
。您还需要加入苹果开发者计划,需要在 Xcode 中设置开发和分发证书,并且您的应用程序需要设置其 AppIcon。
操作步骤...
-
让我们首先通过
ios/TestDeployApp.xcodeproj
文件在 Xcode 中打开我们的项目。 -
正如上一篇食谱中所述,我还建议将您的 Bundle Identifier 更改为比默认值更有意义的内容,例如:
-
接下来,让我们登录到苹果开发者计划,并转到位于https//:developer.apple.com/account/ios/identifier/bundle的 App ID 注册页面。
-
在这里,填写项目的名称和 Bundle ID,然后按下“继续”按钮,接着按“注册”按钮,最后按“完成”按钮完成应用程序的注册。
-
接下来,我们将登录到位于
itunesconnect.apple.com
的 iTunes Connect 网站。 -
在 iTunes Connect 中,导航到“我的应用程序”,然后按“加号(+)”按钮并选择“新应用程序”以添加新应用程序。
-
在新应用程序对话框中,填写名称和语言。选择与之前创建的 Bundle ID 匹配的 Bundle ID,并在 SKU 字段中添加一个唯一的应用程序引用,然后按“创建”。
-
接下来,导航到您的应用程序的 TestFlight 部分,并确保填写本地化信息部分。
-
让我们返回 Xcode 创建
.ipa
文件。选择通用 iOS 设备作为活动方案,然后通过 Xcode 菜单(产品|存档)创建文件。这将打开存档列表,您可以按“上传到 App Store”按钮上传应用程序。 -
您的配置团队应该自动选择。确保选择正确的团队,然后按“选择”。创建存档后,按“上传”按钮。
-
上传应用程序后,您需要等待直到收到来自 iTunes Connect 的电子邮件,通知您构建已完成处理。处理完成后,您可以返回 iTunes Connect 页面并打开内部测试视图。
-
在内部测试部分,单击“选择要测试的版本”,然后选择您的构建,然后单击“下一步”按钮。在“导出合规性”屏幕上,按“确定”。
-
我们准备添加内部测试人员。选择您想要测试该应用程序的用户,然后单击“开始测试”按钮,并在随后的模态中确认您的选择。您的用户现在应该收到邀请邮件来测试您的应用程序!
工作原理...
TestFlight 在 App Store 发布流程中扮演着一流的角色。苹果已经将其支持应用程序测试分发的支持直接集成到 iTunes Connect 中,为开发人员创建了一个流畅无缝的流程。这个过程与部署到 App Store 基本相同,只是在使用 iTunes Connect 时,您必须启用和配置测试。
对于测试人员来说,这也是一个无缝的体验。一旦您在 iTunes Connect 中添加了测试用户,他们将收到安装 TestFlight 应用程序的通知,从而可以轻松访问他们可以测试的应用程序。TestFlight 还通过不需要开发人员添加任何额外的第三方库或代码来支持 TestFlight,使开发人员的流程更加简单,而这在使用HockeyApp时是必需的。
将生产版本部署到苹果应用商店
一旦您彻底测试了您的应用程序,您就可以继续进行下一个(可能也是最激动人心的)步骤:发布到 Apple 应用商店。
这个步骤将指导您准备生产版本并将其提交到 Apple 应用商店。我们实际上不会将应用提交到商店,因为我们使用的是示例应用而不是生产就绪的应用。然而,流程的最后几个步骤非常简单。
准备工作
对于这个步骤,我们将再次使用之前的示例应用程序TestDeployApp
。当然,您还需要加入苹果开发者计划,并在 Xcode 中设置开发和分发证书,就像本章前面讨论的那样。对于真正的生产应用程序部署,您还需要设置 AppIcon 并准备用于 iTunes 的应用程序截图。
如何做...
-
让我们从使用
ios/TestDeployApp.xcodeproj
文件打开 Xcode 开始。 -
如前所述,建议您将 Bundle Identifier 更改为比默认值更有意义的内容,因此请务必在“常规设置”对话框中更改它。
-
在设备上以生产模式测试应用程序也是一个好主意。这可以通过将应用程序方案的构建配置(通过“产品”|“方案”|“编辑方案”菜单找到)更改为 Release 来完成,如下所示:
- 接下来,您需要在 App ID 注册页面上注册应用,该页面位于:
developer.apple.com/account/ios/identifier/bundle
这一步需要一个活跃的苹果开发者计划账户。
-
填写项目的名称和 Bundle ID 字段,然后按 Continue 按钮。
-
接下来,我们将登录到位于
itunesconnect.apple.com
的 iTunes Connect 网站。在“My Apps”部分,按加号(+)按钮,然后选择“新建应用”。 -
您需要在以下对话框中填写名称和语言,然后选择与之前在步骤中创建的 Bundle ID 相匹配的 Bundle ID。此外,添加一个唯一的应用程序引用 SKU,并按 Create 按钮。
-
让我们返回 Xcode 并创建
.ipa
文件。选择 Generic iOS Device 作为活动方案,并通过菜单(产品|存档)创建文件,这将打开存档列表。最后,按“上传到 App Store”。 -
选择您的配置团队,然后按“选择”。
-
一旦存档创建完成,按“上传”按钮。一旦构建完成,您将收到来自 iTunes Connect 的电子邮件。
-
应用程序处理完毕后,返回到 iTunes Connect。在 App Store 部分,打开 App 信息并选择您的应用所适合的类别。
-
在 iOS APP 下的 1.0 准备提交部分中打开。填写所有必填字段,包括应用程序截图、描述、关键字和支持 URL。
-
接下来,在“构建”部分,选择我们在第 8 步中构建的
.ipa
。 -
最后,填写版权和应用程序审查信息部分,然后点击“提交审核”按钮。
它是如何工作的...
在这个食谱中,我们介绍了将 iOS 应用程序发布到 App Store 的标准流程。在这种情况下,我们不需要遵循任何 React Native 特定的步骤,因为最终产品(.ipa
文件)包含了运行 React Native 打包程序所需的所有代码,这将进而以发布模式构建main.jsbundle
文件。
将生产版本部署到 Google Play 商店
本食谱将介绍准备我们的应用程序的生产版本并将其提交到 Google Play 商店的过程。与上一个食谱一样,我们将在实际提交到 App Store 之前停下来,因为这只是一个示例 React Native 应用程序,但是这个过程的其余部分也很简单。
准备工作
对于这个食谱,我们将使用本章节中一直使用的相同简单的 React Native 应用程序TestDeployApp
。您需要拥有 Google Play 开发者帐户才能将应用程序提交到商店,并且如果您想要实际发布应用程序,还需要准备好所有的图标和 Play 商店的截图。
如何做...
-
让我们从在 Android Studio 中打开 React Native 项目开始。第一步是构建
.apk
文件。正如本章前面提到的,从 React Native 项目创建生产 Android 应用程序的过程是复杂的,而且容易发生变化。访问 React Native 文档以获取有关创建.apk
的信息:facebook.github.io/react-native/docs/signed-apk-android.html
。 -
接下来,让我们在 Web 浏览器中打开 Google Play 开发者控制台,网址是
play.google.com/apps/publish/
。 -
让我们通过点击“添加新应用程序”来开始这个过程。填写标题字段,然后点击“上传 APK”按钮,如下所示:
-
接下来,您将看到发布屏幕的 APK 部分。点击“上传您的第一个 APK 到生产”,然后拖放(或选择)您的
.apk
文件。 -
接下来将出现一系列不言自明的模态。浏览左侧侧边菜单中的每个类别(商店列表、内容评级等),并相应地填写所有信息。
-
一旦满足所有要求,点击“发布应用程序”按钮。
它是如何工作的...
在这个教程中,我们介绍了将 Android 应用程序发布到 Google Play 商店的过程。通过按照步骤2 中链接的说明,您的 React Native 应用程序将经历 Gradle assembleRelease
过程。assemble
过程运行打包程序以创建 JavaScript 捆绑文件,编译 Java 类,将它们与适当的资源打包在一起,最后允许您将应用程序签名为.apk
。
部署 Over-The-Air 更新
我们的 React Native 应用程序以 JavaScript 编写的一个有用的副作用是,代码是在运行时加载的,这类似于 Cordova 混合应用程序的工作方式。我们可以利用这个功能来使用OTA(Over-The-Air)推送更新到我们的应用程序。这允许添加功能和修复错误,而无需经过应用商店的批准流程。对于 React Native 的 OTA 更新的唯一限制是,我们不能推送编译的(Objective-C 或 Java)代码,这意味着更新代码必须仅在 JavaScript 层中。有一些流行的服务提供基于云的 OTA 应用程序更新。我们将重点介绍微软的服务CodePush
。
这个教程将涵盖在 iOS 和 Android 上为我们的 React Native 应用程序设置和推送更新使用CodePush
。
准备工作
对于这个教程,我们将使用相同的简单的 React Native 应用程序,我们在本章中一直在使用的TestDeployApp
。我们将把应用程序部署到以生产/发布模式运行的物理设备上,这将允许应用程序从 CodePush 服务器接收更新。
如何做...
- 为了使用 CodePush,我们需要安装 CodePush CLI 并创建一个免费帐户。这可以通过在终端中运行以下两个命令来完成:
npm install -g code-push-cli
code-push register
- 下一步是向 CodePush 注册我们的应用程序。记下通过运行
code-push register
输出提供的应用程序的部署密钥。我们将在这个示例中使用暂存密钥。文档建议为每个平台添加一个应用程序,每个应用程序都带有-IOS
或-Android
后缀。要将应用程序添加到 CodePush,请使用此命令:
code-push app add TestDeployApp**-IOS**
**code-push app add TestDeployApp-Android**
- 我们还需要在 React Native 项目目录中安装 React Native CodePush 模块。可以使用
npm
完成这个操作,如下所示:
npm install --save react-native-code-push
或者,使用yarn
:
yarn add react-native-code-push
- 下一步是将 CodePush 本机模块与我们的项目进行链接。在提示输入 Android 和 iOS 的部署密钥时,请使用步骤 2中讨论的暂存密钥。可以使用以下命令链接本机模块:
react-native link react-native-code-push
- 接下来,我们需要设置我们的 React Native 应用程序以使用 CodePush。在
index.js
中,我们需要添加三个东西:CodePush 导入,一个选项对象,以及在通过AppRegistry.registerComponent
注册应用程序时调用导入的codePush
模块。设置应用程序如下:
import {AppRegistry} from 'react-native';
import App from './App';
import codePush from 'react-native-code-push';
const codePushOptions = {
updateDialog : true
}
AppRegistry.registerComponent('TestDeployApp',
() => codePush(codePushOptions)(App)
)
- 为了测试我们在 iOS 应用程序中的更改,让我们部署到我们的 iOS 设备。在 Xcode 中打开 React Native 项目,将方案的构建配置(产品|方案|编辑方案...)更改为 Release,然后按照以下步骤运行:
- 接下来,在应用程序中对 React Native 代码进行某种任意更改,然后在终端中运行以下命令以使用新代码更新应用程序:
code-push release-react TestDeployApp ios -m --description "Updating using CodePush"
- 接下来,在您的 iOS 设备上关闭并重新打开应用程序。您应该会看到以下提示:
-
继续通过提示后,应用程序将自动更新到最新版本!
-
让我们还在 Android 上测试一下这个功能。您需要按照 React Native 文档中
facebook.github.io/react-native/docs/signed-apk-android.html
中概述的步骤将您的 Android 应用程序制作成.apk
文件。 -
将 Android 设备连接到开发计算机后,在
android/
目录中运行以下命令:
adb install
app/build/outputs/apk/app-release.apk
- 接下来,对 React Native JavaScript 代码进行更改。只要添加了新代码,我们就可以使用该更改的代码来更新应用程序。然后,在终端中运行以下命令:
code-push release-react TestDeployApp android -m --description "Updating using CodePush"
- 再次在 Android 设备上关闭并重新打开应用程序,以获得以下提示:
- 在继续操作提示之后,应用程序将自行更新到最新版本。
它是如何工作的...
CodePush(以及其他云托管的 OTA 更新平台)的工作原理与 React Native 自诞生以来一直存在的技术相同。当应用程序初始化时,React Native 会加载一个 JavaScript 捆绑包。在开发过程中,此捆绑包从localhost:3000
加载。然而,一旦我们部署了一个应用程序,它将寻找一个名为main.jsbundle
的文件,该文件已包含在最终产品中。通过在步骤 5中的registerComponent
中添加对codePush
的调用,应用程序将与 CodePush API 进行检查以查看是否有更新。如果有新的更新,它将提示用户。接受提示会下载新的jsbundle
文件并重新启动应用程序,从而更新代码。
优化 React Native 应用程序大小
在将我们的应用程序部署到生产环境之前,将应用程序捆绑包的大小缩小到尽可能小的文件是一个很好的主意,我们可以利用几种技术来实现这一点。这可能涉及支持更少的设备或压缩包含的资产。
这个配方将涵盖一些限制 iOS 和 Android React Native 应用程序生产包文件大小的技术。
准备工作
对于这个配方,我们将使用本章节中一直使用的相同简单的 React Native 应用程序,TestDeployApp
。您还需要在 iOS 上使用代码签名,并且能够创建.apk
文件,就像在之前的配方中介绍的那样。
如何做...
- 我们将从对我们捆绑的资产进行一些简单的优化开始,这通常包括图像和外部字体:
-
对于 PNG 和 JPEG 压缩,您可以使用诸如
www.tinypng.com
之类的服务来减小文件大小,几乎不会降低图像质量。 -
如果您使用
react-native-vector-icons
库,您会注意到它捆绑了八种不同的字体图标集。建议您删除应用程序未使用的任何图标字体库。 -
SVG 文件也可以进行压缩和优化。用于此目的的一个服务是
compressor.io
。 -
打包到您的应用程序中的任何音频资产应该使用可以利用高质量压缩的文件格式,例如 MP3 或 AAC。
-
对于 iOS,除了默认启用的发布方案设置外,几乎没有什么可以进一步减小文件大小的方法。这些设置包括为应用瘦身启用 Bitcode 和将编译器优化设置为 Fastest, Smallest [-Os]。
-
对于 Android,有两件事情可以改善文件大小:
- 在 Android Studio 中,打开
android/app/build.gradle
文件,找到以下行,然后将它们的值更新为以下内容:
def enableSeparateBuildPerCPUArchitecture = true
def enableProguardInReleaseBuilds = true
- 如果您只打算针对基于 ARM 架构的 Android 设备,我们可以完全阻止其构建 x86 支持。在
build.gradle
文件中,找到splits abi
对象,并添加以下行以不包括 x86 支持:
include "armeabi-v7a"
您可以在 Android 文档中阅读有关 ABI 管理的更多信息:
developer.android.com/ndk/guides/abis
工作原理...
在这个教程中,我们介绍了可以用来减小应用文件大小的技术。JavaScript 捆绑包越小,JavaScript 解释器就能更快地解析代码,从而实现更快的应用加载时间和更快的 OTA 更新。我们能够保持.ipa
和.apk
文件越小,用户就能越快地下载应用。
第十五章:优化您的应用的性能
在本章中,我们将涵盖以下配方:
-
优化我们的 JavaScript 代码
-
优化自定义 UI 组件的性能
-
保持动画以 60 FPS 运行
-
充分利用 ListView
-
提升我们应用的性能
-
优化原生 iOS 模块的性能
-
优化原生 Android 模块的性能
-
优化原生 iOS UI 组件的性能
-
优化原生 Android UI 组件的性能
介绍
性能是软件开发中几乎每一个技术的关键要求。React Native 被引入是为了解决混合应用中存在的性能不佳的问题,这些混合应用将 Web 应用程序包装在本地容器中。React Native 具有既灵活又出色性能的架构。
在考虑 React Native 应用的性能时,重要的是要考虑 React Native 的工作方式的整体情况。React Native 应用有三个主要部分,它们的相对性能如下图所示:
本章的配方侧重于使用占用更少内存并具有更少操作的低级函数,从而降低任务完成所需的时间。
优化我们的 JavaScript 代码
可以说,您的 React Native 应用可能主要是用 JavaScript 编写的。可能会有一些原生模块和自定义 UI 组件,但大部分视图和业务逻辑可能都是用 JSX 和 JavaScript 编写的。如果您使用现代 JavaScript 开发技术,您还将使用 ES6、ES7 及更高版本引入的语言构造。这些可能作为 React Native 捆绑的 JavaScript 解释器(JavaScriptCore)的一部分本地可用,也可能由 Babel 转译器进行填充。由于 JavaScript 可能构成任何给定的 React Native 应用的大部分,这应该是我们优化的第一部分,以便从应用中挤出额外的性能。
这个配方将提供一些有用的提示,以优化 JavaScript 代码,使其尽可能高效。
准备工作
这个技巧不一定依赖于 React Native,因为它侧重于用于编写任何 React 应用程序的 JavaScript。其中一些建议是微优化,可能只会提高旧版/较慢设备的性能。根据您打算支持的设备,一些技巧会比其他技巧更有效。
如何做...
- 要考虑的第一个优化是加快迭代速度。通常,您可能会使用将迭代器函数作为参数的函数(
forEach
,filter
和map
)。一般来说,这些方法比使用标准的for
循环要慢。如果您要迭代的集合非常大,这可能会有所不同。以下是一个更快的 filter 函数的示例:
let myArray = [1,2,3,4,5,6,7];
let newArray;
// Slower:
function filterFn(element) {
return element > 2;
}
newArray = myArray.filter(filterFn);
// Faster:
function filterArray(array) {
var length = array.length,
myNewArray = [],
element,
i;
for(i = 0; i < length; i++) {
element = array[i];
if(element > 2) {
myNewArray.push(array[i]);
}
}
return myNewArray;
}
newArray = filterArray(myArray);
- 在优化迭代时,还可以更有效地确保将您正在访问的变量存储在迭代附近的某个地方:
function findInArray(propertyerties, appConfig) {
for (let i = 0; i < propertyerties.length; i++) {
if (propertyerties[i].somepropertyerty ===
appConfig.userConfig.permissions[0]) {
// do something
}
}
}
function fasterFindInArray(propertyerties, appConfig) {
let matchPermission = appConfig.userConfig.permissions[0];
let length = propertyerties.length;
let i = 0;
for (; i < length; i++) {
if (propertyerties[i].somepropertyerty === matchPermission) {
// do something
}
}
}
- 您还可以优化逻辑表达式。将执行速度最快且最接近的语句放在左边:
function canViewApp(user, isSuperUser) {
if (getUserPermissions(user).canView || isSuperUser) {
return true;
}
}
function canViewApp(user, isSuperUser) {
if (isSuperUser || getUserPermissions(user).canView) {
return true;
}
}
-
虽然现代 JavaScript(ES6,ES7 等)构造可能更容易开发,但它们的一些特性执行速度比它们的 ES5 对应物要慢。这些特性包括
for of
,generators
,Object.assign
等。有关性能比较的良好参考资料可以在kpdecker.github.io/six-speed/
找到。 -
避免使用
try-catch
语句可能会有所帮助,因为它们会影响解释器的优化(就像在 V8 中一样)。 -
数组应该有相同类型的成员。如果需要一个类型可以变化的集合,可以使用对象。
工作原理...
JavaScript 性能是一个不断争论的话题。由于谷歌、苹果、Mozilla 和全球开源社区一直在努力改进他们的 JavaScript 引擎,因此有时很难跟上最新的性能指标。对于 React Native,我们关注的是 WebKit 的 JavaScriptCore。
优化自定义 UI 组件的性能
在构建 React Native 应用程序时,可以肯定会创建自定义 UI 组件。这些组件可以是由几个其他组件组成的,也可以是在现有组件的基础上构建并添加更多功能的组件。随着功能的增加,复杂性也会增加。这种增加的复杂性会导致更多的操作,从而可能导致减速。幸运的是,有一些方法可以确保我们的自定义 UI 组件性能最佳。本文介绍了几种技术,以便充分利用我们的组件。
准备工作
本文要求您有一个带有一些自定义组件的 React Native 应用程序。由于这些性能建议可能对您的应用程序提供或不提供价值,请谨慎选择是否将其应用于您的代码。
如何做到...
-
我们应该首先查看的优化是跟踪给定组件的
state
对象中的内容。我们应该确保state
中的所有对象都被使用,并且每个对象都可能发生变化,从而引起所需的重新渲染。 -
查看每个组件的
render
函数。总体目标是使该函数尽可能快地执行,因此请尽量确保其中不会发生长时间运行的过程。如果可以的话,缓存计算和常量值,使其在render
函数之外不会每次实例化。 -
如果在
render
函数中可能返回条件性 JSX,请尽早return
。以下是一个微不足道的例子:
// unoptimized
render() {
let output;
const isAdminView = this.propertys.isAdminView;
if(isAdminView) {
output = (<AdminButton/>);
} else {
output = (
<View style={styles.button}>
<Text>{this.propertys.buttonLabel}</Text>
</View>
);
}
return output;
}
// optimized
render() {
const isAdminView = this.propertys.isAdminView;
if (isAdminView) {
return (<AdminButton/>);
}
return (
<View style={styles.button}>
<Text>{this.propertys.buttonLabel}</Text>
</View>
);
}
- 我们可以进行的最重要的优化是如果不需要,可以完全跳过
render
方法。这是通过实现shouldComponentUpdate
方法并从中返回false
来实现的,使其成为纯组件。以下是如何使组件成为PureComponent
的方法:
import React, { PureComponent } from 'react';
export default class Button extends PureComponent {
}
工作原理...
大多数 React Native 应用程序将由自定义组件组成。将有一些有状态和无状态的组件。正如在步骤 2中所强调的,总体目标是以尽可能短的时间渲染我们的组件。如果一个组件只需要渲染一次,然后保持不变,那么也可以实现另一个收益,就像在步骤 4中介绍的那样。有关纯组件的使用方法和其益处的更多信息,请访问60devs.com/pure-component-in-react.html
。
另请参阅
您可以在官方文档reactjs.org/docs/optimizing-performance.html
中找到有关 React 组件性能优化的更多信息。
保持动画以 60FPS 运行
任何高质量移动应用程序的重要方面是用户界面的流畅性。动画用于提供丰富的用户体验,任何卡顿或抖动都可能对此产生负面影响。动画可能会用于各种交互,从在视图之间切换到对组件上的用户触摸交互做出反应。创建高质量动画的最重要因素之一是确保它们不会阻塞 JavaScript 线程。为了保持动画流畅并且不中断 UI 交互,渲染循环必须在 16.67 毫秒内渲染每一帧,以便实现 60FPS。
在这个教程中,我们将介绍几种改善动画性能的技术。这些技术特别关注于防止 JavaScript 执行中断主线程。
准备工作
对于这个教程,我们假设您有一个定义了一些动画的 React Native 应用程序。
如何做...
- 首先,在调试 React Native 中的动画性能时,我们需要启用性能监视器。要这样做,显示开发菜单(摇动设备或从模拟器中使用cmd + D)并点击显示性能监视器。
在 iOS 中的输出将类似于以下截图:
在 Android 中的输出将类似于以下截图:
- 如果您想要对组件的过渡(
opacity
)或尺寸(width
,height
)进行动画处理,则确保使用LayoutAnimation
。您可以在第六章的向您的应用程序添加基本动画中找到使用LayoutAnimation
的示例,在展开和折叠容器的教程中。
如果您想在 Android 上使用LayoutAnimation
,则需要在应用程序启动时添加以下代码:UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true)
。
- 如果您需要对动画有限的控制,建议您使用 React Native 附带的
Animated
库。该库允许您将所有动画工作卸载到本地 UI 线程上。为此,我们必须将useNativeDriver
属性添加到我们的Animated
调用中。让我们以一个示例Animated
示例并将其卸载到本地线程:
componentWillMount() {
this.setState({
fadeAnimimation: new Animated.Value(0)
});
}
componentDidMount() {
Animated.timing(this.state.fadeAnimimation, {
toValue: 1,
useNativeDriver: true
}).start();
}
目前,仅动画库的部分功能支持本地卸载。请参考还有更多...部分以获取兼容性指南。
- 如果您无法将动画工作卸载到本地线程上,仍然有解决方案可以提供流畅的体验。我们可以使用
InteractionManager
在动画完成后执行任务:
componentWillMount() {
this.setState({
isAnimationDone: false
});
}
componentWillUpdate() {
LayoutAnimation.easeInAndOut();
}
componentDidMount() {
InteractionManager.runAfterInteractions(() => {
this.setState({
isAnimationDone: true
});
})
}
render() {
if (!this.state.isAnimationDone) {
return this.renderPlaceholder();
}
return this.renderMainScene();
}
- 最后,如果您仍然遇到性能问题,您将不得不重新考虑您的动画策略,或者在目标平台上将性能不佳的视图实现为自定义 UI 视图组件。这意味着使用 iOS 和/或 Android SDK 本地实现您的视图和动画。在第十一章中,添加本地功能,我们介绍了在渲染自定义 iOS 视图组件和渲染自定义 Android 视图组件中创建自定义 UI 组件的方法。
工作原理...
本食谱中的提示侧重于防止 JavaScript 线程锁定的简单目标。一旦我们的 JavaScript 线程开始丢帧(锁定),即使只有一小部分时间,我们也失去了与应用程序交互的能力。这似乎微不足道,但敏锐的用户立即就能感受到影响。本食谱中的提示的重点是将动画卸载到 GPU 上。当动画在主线程上运行(由 GPU 渲染的本地层),用户可以自由地与应用程序交互,而不会出现卡顿、挂起、抖动或颤动。
还有更多...
这里是useNativeDriver
可用的快速参考:
功能 | iOS | Android |
---|---|---|
style , value , propertys |
√ | √ |
decay |
√ | |
timing |
√ | √ |
spring |
√ | |
add |
√ | √ |
multiply |
√ | √ |
modulo |
√ | |
diffClamp |
√ | √ |
interpoloate |
√ | √ |
event |
√ | |
division |
√ | √ |
transform |
√ | √ |
充分利用 ListView
React Native 提供了一个非常高性能的列表组件。它非常灵活,支持在其中渲染几乎任何您可以想象的组件,并且渲染速度相当快。如果您想阅读更多关于如何使用ListView
的示例,本书中有一些示例,包括第二章中的显示项目列表,创建一个简单的 React Native 应用程序。React Native 的ListView
是建立在ScrollView
之上的,以实现使用任何视图组件呈现可变高度行的灵活性。
ListView
组件的主要性能和资源缺点是当您使用非常大的列表时发生。当用户滚动列表时,下一页的行在底部被渲染。顶部的不可见行可以设置为从渲染树中删除,我们将很快介绍。但是,只要组件被挂载,行的引用仍然在内存中。当我们的组件使用可用内存时,将会减少快速访问存储给即将到来的组件的空间。这个示例将涵盖处理一些潜在的性能和内存资源问题。
准备工作
对于这个示例,我们假设您有一个使用ListView
的 React Native 应用程序,最好是使用大型数据集。
如何做...
-
"在某些情况下,该功能可能存在错误(内容丢失)-使用时需自担风险。"
-
接下来,如果每行中呈现的组件不复杂,我们可以增加
pageSize
。 -
另一个优化是将
scrollRenderAheadDistance
设置为舒适的值。如果您可以预期用户很少会滚动到初始视口之外,或者他们可能会滚动得很慢,那么您可以降低该值。这可以防止ListView
提前渲染太多行。 -
最后,我们可以利用
removeClippedSubviews
属性进行最后的优化。然而,官方文档指出:
让我们从对我们的原始ListView
组件进行一些优化开始。如果我们将initialListSize
属性设置为1
,我们可以加快初始渲染。
- 步骤 1 到步骤 4的组合可以在以下示例代码中看到:
renderRow(row) {
return (
<View style={{height:44, overflow:'hidden'}}>
<Text>Item {row.index}</Text>
</View>
)
}
render() {
return (
<View style={{flex:1}}>
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderRow}
pageSize={10}
initialListSize={1}
pageSize={10}
scrollAheadDistance={200}
/>
</View>
)
}
它是如何工作的...
与开发任何应用一样,某样东西越灵活和复杂,性能就越慢。ListView
就是这个概念的一个很好的例子。它非常灵活,因为它可以在一行中呈现任何View
,但如果不小心使用,它可能会迅速使您的应用停滞不前。在步骤 1到步骤 4中定义的优化结果将根据您正在呈现的内容和ListView
使用的数据结构在不同情况下有所不同。您应该尝试这些值,直到找到一个良好的平衡。作为最后的手段,如果您仍然无法达到所需的性能基准,可以查看一些提供新的ListView
实现或替代方案的社区模块。
另请参阅
以下是一些第三方ListView
实现的列表,承诺提高性能:
-
recyclerlistview
:这个库是ListView
的最强大的替代品,拥有一长串的改进和功能,包括支持交错网格布局、水平模式和页脚支持。存储库位于github.com/Flipkart/recyclerlistview
。 -
react-native-sglistview
:这将removeClippedSubviews
提升到一个新的水平,当屏幕外的行从渲染树中移除时,会刷新内存。存储库位于github.com/sghiassy/react-native-sglistview
。
提升我们应用的性能
React Native 存在的原因是使用 JavaScript 构建原生应用。这与 Ionic 或 Cordova 混合应用等类似框架不同,后者包装了用 JavaScript 编写的 Web 应用,并尝试模拟原生应用行为。这些 Web 应用只能访问原生 API 来执行处理,但无法在应用内部呈现原生视图。这是 React Native 应用的一个主要优势,因此使它们本质上比混合应用更快。由于它的性能在开箱即用时更高,我们通常不必像处理混合 Web 应用那样担心整体性能。不过,通过一点额外的努力,可能可以实现性能的轻微改进。本文将提供一些快速的方法,可以用来构建更快的 React Native 应用。
如何做…
-
我们可以做的最简单的优化是不输出任何语句到控制台。执行
console.log
语句并不像您想象的那样简单,因此建议在准备捆绑最终应用程序时删除所有控制台语句。 -
如果您在开发过程中使用了大量控制台语句,您可以使用
transform-remove-console
插件让 Babel 在创建捆绑时自动删除它们。这可以通过使用yarn
在终端中将其安装到项目中。
yarn add babel-plugin-transform-remove-console
- 或者,您可以使用
npm
:
npm install babel-plugin-transform-remove-console --save
安装了该软件包后,您可以通过添加包含以下代码的.babelrc
文件将其添加到项目中:
{
"presets": ["react-native"],
"env": {
"production": {
"plugins": ["transform-remove-console"]
}
}
}
-
接下来,请确保在分析性能时,您的应用程序处于生产模式下运行,最好在设备上。如果您想了解如何做到这一点,您可以参考第十三章部署我们的应用程序中的将测试构建部署到 HockeyApp的配方。
-
有时,当您在动画化
View
的位置或布局时,您可能会注意到 UI 线程的性能下降。您可以通过将shouldRasterizeIOS
和renderToHardwareTextureAndroid
属性设置为 iOS 和 Android 平台的 true 来减轻这种情况。请注意,这可能会显著增加内存使用量,因此请确保在这些更改后测试性能。 -
如果您发现需要在执行同步的、潜在的长时间运行的过程的同时进行视图转换,这可能会成为性能瓶颈。当构建
ListView
的DataSource
或转换数据以支持即将到来的视图时,通常会发生这种情况。您应该尝试仅处理数据的初始子集,足以快速渲染 UI。一旦页面转换之间的动画完成,您可以使用InteractionManager
来加载其余的数据。您可以参考保持动画以 60 FPS 运行的配方,了解如何使用InteractionManager
的更多信息。 -
最后,如果您已经确定了减慢应用程序速度的特定组件或任务,并且找不到可行的解决方案,那么您应该考虑通过创建本机模块或本机 UI 组件将其移动到本机线程来实现这一功能。
它是如何工作的...
这个教程涵盖了一些更高级和更广泛的适用于所有 React Native 应用程序的技巧。您可能会从这些技巧中看到的最显著的性能提升是将组件移动到本地层,如步骤 7中所述。
优化原生 iOS 模块的性能
在构建 React Native 应用程序时,通常需要使用原生 Android 和 iOS 代码。您可能已经构建了这些原生模块,以公开本地 API 提供的一些额外功能,或者您的应用程序需要执行密集的后台任务。
正如之前提到的,工作在本地层确实允许您充分利用设备的容量。但这并不意味着我们编写的代码将自动成为最快的。总是有优化和实现性能提升的空间。
在这个教程中,我们将提供一些关于如何使用 iOS SDK 使您的 Objective-C 代码运行得更快的技巧。我们还将考虑 React Native 和 React Native 桥接,它用于在 JavaScript 和本地层之间进行通信,如何融入更大的画面。
准备工作
对于这个教程,您应该有一个使用为 iOS 创建的原生模块的 React Native 应用程序。如果您需要帮助编写原生模块,请查看第十一章中的暴露自定义 iOS 模块教程,添加原生功能。
如何做...
-
首先,当使用原生模块时,我们必须注意通过 React Native 桥传递的数据。始终保持跨桥事件和回调中的数据最少化是目标,因为 Objective-C 和 JavaScript 之间的数据序列化非常缓慢。
-
如果您需要将数据缓存在内存中,以便原生模块使用,可以将其存储在本地属性或字段变量中。原生模块是单例。这样做可以避免返回一个大对象以存储在 React Native 组件中。
-
有时,我们必须利用因功能集而强大而庞大的类。对于 Objective-C 和 iOS 方面的事情,与其每次通过
RCT_EXPORT_METHOD
公开功能时在您的方法中实例化类似NSDateFormatter
,不如将该类的引用存储为属性或实例变量。 -
此外,诸如
NSDateFormatter
之类的原生方法通常非常耗费资源,因此在可能的情况下应避免使用它们。例如,如果您的应用程序只能处理 UNIX 时间戳,那么您可以轻松地使用以下函数从时间戳获取NSDate
对象:
- (NSDate*)dateFromUnixTimestamp:(NSTimeInterval)timestamp {
return [NSDate dateWithTimeIntervalSince1970:timestamp];
}
- 如果情况允许,您可以进行最重要的性能优化,即生成异步后台线程来处理密集处理。React Native 非常适合这种模型,因为它使用异步消息/事件系统在 JavaScript 和原生线程之间进行通信。当后台进程完成时,您可以调用回调/承诺,或者触发一个事件供 JavaScript 线程处理。要了解如何在 React Native iOS 原生模块中创建和利用后台进程,请查看第十一章的在 iOS 上进行后台处理。
它是如何工作的...
Objective-C 代码执行非常快 - 几乎和纯 C 一样快。因此,我们进行的优化与执行任务的方式无关,而是与实例化方式和不阻塞原生线程有关。您将看到的最大性能提升是通过使用Grand Central Dispatch(GCD)生成后台进程,如步骤 5中所述。
优化原生 Android 模块的性能
在开发 React Native 应用程序时,您可能会发现自己编写原生 Android 模块,以创建跨平台功能(在 iOS 和 Android 上)或者利用尚未作为一方模块包装的 Android 原生 API。希望您在第十一章的添加原生功能中找到了一些有用的关于使用原生模块的建议。
在这个配方中,我们将介绍几种加快 React Native Android 原生模块速度的技术。其中许多技术仅限于 Android 上的一般开发,还有一些将涉及与 React Native JavaScript 层的通信。
准备工作
对于这个配方,您应该有一个使用您为 Android 创建的原生模块的 React Native 应用程序。如果您需要帮助编写原生模块,请查看第十一章的暴露自定义 Android 模块配方。
如何做...
-
首先,与 iOS 原生模块一样,您需要限制通过 React Native 桥传输的数据量。将事件和回调中的数据保持最少将有助于避免由 Java 和 JavaScript 之间的序列化引起的减速。此外,与 iOS 一样,尽量将数据缓存在内存中供原生模块使用;将其存储在私有字段中。原生模块是单例。这应该被利用,而不是返回一个大对象存储在 React Native 组件中。
-
在为 Android 编写 Java 代码时,您应尽量避免创建短期对象。如果可能的话,尤其是对于数组等数据集,使用基本类型。
-
最好重用对象,而不是依赖垃圾收集器来回收未使用的引用并实例化一个新对象。
-
Android SDK 提供了一种内存高效的数据结构,用于替代
Map
的使用,它将整数映射到对象,称为SparseArray
。使用它可以减少内存使用并提高性能。以下是一个例子:
SparseArray<SomeType> map = new SparseArray<SomeType>();
map.put(1, myObjectInstance);
还有SparseIntArray
,它将整数映射到整数,以及SparseBooleanArray
,它将整数映射到布尔值。
-
虽然对于习惯于在 Java 中进行面向对象编程的开发人员来说可能听起来有些反直觉,但是通过直接访问实例字段来避免使用 getter 和 setter 也可以提高性能。
-
如果您曾经使用
String
连接,可以使用StringBuilder
。 -
最后,如果可能的话,您可以进行最重要的性能优化,即通过利用 React Native 的异步消息/事件系统在 JavaScript 和本地线程之间进行通信,从而生成异步后台线程来执行繁重的计算。当后台进程完成时,您可以调用回调/承诺,或者触发一个事件,让 JavaScript 线程接手。要了解如何在 React Native Android 原生模块中创建后台进程,请阅读第十一章中的在 Android 上进行后台处理。
工作原理...
这个配方中的大部分提示都围绕着高效的内存管理。Android 操作系统使用类似桌面 Java 虚拟机的传统垃圾收集器。当垃圾收集器启动时,释放内存可能需要 100-200 毫秒。步骤 3-6都提供了减少应用程序内存使用的建议。
优化原生 iOS UI 组件的性能
React Native 为我们提供了一个优秀的基础,可以使用内置组件和样式构建几乎任何类型的用户界面。使用 Objective-C 使用 iOS SDK、OpenGL 或其他绘图库构建的组件通常比使用 JSX 组合预构建组件性能更好。在使用这些原生视图组件时,可能会有一些用例对应用程序性能产生负面影响。
这个配方将专注于在渲染自定义视图时充分利用 iOS UIKit SDK。我们的目标是尽可能快地渲染所有内容,以使我们的应用程序以 60 FPS 运行。
准备工作
对于这个配方,你应该有一个渲染你为 iOS 编写的自定义原生 UI 组件的 React Native 应用程序。如果你需要帮助在 React Native 中包装 UI 组件,请查看第十一章中的暴露自定义 iOS 视图组件配方,添加原生功能。
如何做...
-
如前所述,只有在无法避免的情况下才通过 React Native 桥传递数据,因为 Objective-C 和 JavaScript 类型之间的数据序列化速度很慢。
-
如果有数据需要在不久的将来引用,最好将其存储在你初始化的原生类中。根据你的应用程序,你可以将其存储为
ViewManager
的属性,为View
的实例提供服务的单例,或者View
本身的属性。 -
如果你的视图组件涉及将多个
UIView
实例渲染为父UIView
容器的子级,请确保所有实例的opaque
属性设置为true
。 -
如果你在视图组件内部渲染图像(而不是使用 React Native 的
Image
组件),那么将图像设置为与UIImageView
组件相同的尺寸可以提高性能。缩放和其他图像转换是影响帧率的重操作。 -
在编写 iOS 视图组件时,避免离屏渲染是最有影响力的调整之一。如果可能的话,避免使用 SDK 功能进行以下操作:
-
使用以Core Graphics(CG)库开头的类
-
覆盖
UIView
的drawRect
实现 -
设置
shouldRasterize=YES
,或者在UIView
实例的layer
属性上使用setMasksToBounds
或setShadow
-
使用
CGContext
进行自定义绘图
- 如果您需要向视图添加阴影,请确保设置
shadowPath
以防止离屏渲染。以下是初始化和阴影定义的示例应该如何看起来:
RCT_EXPORT_MODULE()
- (UIView *)view {
UIView *view = [[UIView alloc] init];
view.layer.masksToBounds = NO;
view.layer.shadowColor = [UIColor blackColor].CGColor;
view.layer.shadowOffset = CGSizeMake(0.0f, 5.0f);
view.layer.shadowOpacity = 0.5f;
view.layer.shadowPath = [[UIBezierPath bezierPathWithRect:view.bounds] CGPath];
return view;
}
它是如何工作的...
这个教程侧重于一些有用的技巧,可以让 GPU 尽可能多地完成工作。第二部分讨论了如何尽可能减少 GPU 的负荷。在步骤 3中强制执行opaque
属性告诉 GPU 不要担心检查其他组件的可见性,以便它可以计算透明度。步骤 5和步骤 6防止了离屏渲染。离屏渲染使用 CPU 生成位图图像(这是一个缓慢的过程),更重要的是,它会使 GPU 在生成图像之前无法渲染视图。
优化本地 Android UI 组件的性能
在过去的几年里,Android 本地 UI 性能有了显著改善。这主要是由于其使用 GPU 硬件加速来渲染组件和布局的能力。在您的 React Native 应用程序中,您可能会发现自己使用自定义视图组件,特别是如果您想使用尚未作为 React Native 组件包装的内置 Android 功能。尽管 Android 平台已经有意增加了其 UI 的性能,但组件的渲染方式很快就会抵消所有这些好处。
在这个教程中,我们将讨论一些方法,以获得我们自定义 Android 视图组件的最佳性能。
准备工作
对于这个教程,您应该有一个 React Native 应用程序,用于渲染您为 Android 编写的自定义本地 UI 组件。如果您需要帮助在 React Native 中包装 UI 组件,请查看第十一章中的公开自定义 Android 视图组件教程,添加本地功能。
如何做...
-
如前所述,只有在必要时才通过 React Native 桥传递数据。将数据保留在事件和回调中最小化,因为 Java 和 JavaScript 之间的数据序列化很慢。
-
如果有数据需要在不久的将来进行引用存储,最好将其存储在您初始化的原生类中。根据您的应用程序,您可以将其存储为
SimpleViewManager
上的属性,这是一个为View
的实例提供服务的单例,或者是View
本身的属性。 -
在构建视图时,请考虑组件通常由其他子组件组成。这些组件以布局的层次结构保存。过度嵌套布局可能会变得非常昂贵。如果您正在使用多级嵌套的 LinearLayout 实例,请尝试用单个 RelativeLayout 替换它们。
-
您可以使用 HierarchyViewer 工具来分析布局的效率,该工具已捆绑在 Android 设备监视器中。要从 Android 设备监视器中打开它,请单击窗口|打开透视图...|层次结构视图,然后选择确定。
-
如果您在 Java 中原生执行自定义视图上的重复动画(而不是使用 React Native Animated API),那么您可以利用硬件层来提高性能。只需在 animate 调用中添加 withLayer 方法调用。例如:
myView.animate()
.alpha(0.0f)
.withLayer()
.start();
它是如何工作的...
不幸的是,当涉及到渲染 Android UI 组件时,你可以执行的优化并不多。它们通常围绕着不要过度嵌套布局,因为这会增加复杂性数倍。当您遇到布局性能问题时,应用程序很可能受到 GPU 的过度使用或过度绘制的影响。过度绘制发生在 GPU 在已经渲染的现有视图上渲染新视图时。您可以在 Android 开发者设置菜单中启用 GPU 过度绘制调试。过度绘制的严重程度顺序为无颜色 -> 蓝色 -> 绿色 -> 浅红色 -> 深红色。
在步骤 5中,我们提供了一个快速提示,用于改善动画的性能。这对于重复动画特别有效,因为它会将动画输出缓存到 GPU 上并重放。