ReactNative-入门指南-全-

ReactNative 入门指南(全)

原文:zh.annas-archive.org/md5/9ccccd6d4fe3953455f539c29dae3b9b

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

为什么会有这么多使用原生语言编写移动应用的替代方案?更重要的是,为什么世界还需要另一种方法?显然,肯定存在尚未解决的问题。

开发者希望只用一种语言来为 iOS 和 Android 开发。Web 开发者希望重用他们现有的 JavaScript 知识,并利用他们已经熟悉和喜爱的 Web 框架。这就是 Apache Cordova(PhoneGap)存在的原因。通过在原生应用中包装一个浏览器,开发者可以将他们的 HTML、CSS 和 JavaScript 应用程序打包在原生外壳中,但为什么不是所有移动应用都基于 Cordova 呢?

用户期望原生性能,拥有原生的用户体验。混合应用并不能解决用户的问题,它们解决的是开发者的问题。我们需要一种能够做到这两点的技术!

React Native 通过真正原生的应用改变了游戏规则。它不使用 WebView 或将 JavaScript 转换为原生语言。把它想象成由 JavaScript 大脑控制的原生 UI 组件。结果是用户体验与任何其他原生应用无法区分,同时开发者体验利用了 JavaScript 和 React 框架的惊人生产力优势。

拥有了 React Native,你终于能够在不牺牲质量或性能的情况下,利用你的 Web 开发技能在移动世界。这是圣杯,我们很兴奋地向你展示 React Native 能做什么,并看到你用它创造出多么惊人的应用!

这本书涵盖了什么内容

第一章,探索示例应用程序,是运行示例 iOS 应用程序的逐步指南。

第二章,理解 React Native 基础知识,涵盖了 React Native 的基础知识,并简要介绍了虚拟 DOM 如何提高性能。然后通过创建你的第一个组件介绍了 props 和 state。

第三章,从示例应用程序开始,从为 iOS 和 Android 生成项目文件开始。然后继续创建第一个屏幕并为应用程序添加导航。

第四章,使用样式和布局,涵盖了在 React Native 中布局和样式化内容的方方面面。学习如何将 React CSS 和 Flexbox 应用到你的组件中。

第五章,显示和保存数据,使用 ListView 显示数据,并使用 AsyncStorage API 保存笔记。

第六章,使用地理位置和地图,讨论了地理位置 API 和地图组件。

第七章,集成本地模块,专注于将 React Native 社区中的第三方本地模块集成到您的应用程序中。

第八章,发布应用程序,介绍了 iOS 和 Android 的发布流程,以便您准备好将应用程序提交到 AppStore 或 Google Play Store。

您需要本书的内容

本书所需的软件要求如下:

  • Xcode

  • 命令行工具

  • npm 2.x

  • JDK

  • Android SDK

本书面向对象

本书面向希望学习使用他们已有的技能构建快速、美观的本地移动应用的 Web 开发者。如果您已经具备一些 JavaScript 知识或正在 Web 上使用 React,那么您将能够快速上手 React Native 用于 iOS 和 Android。

习惯用法

在本书中,您将找到许多文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名如下所示:“在 Xcode 中打开ReactNotes.xcodeproj文件,位于ios/文件夹中。”

代码块如下设置:

NSURL *jsCodeLocation;

/**
* Loading JavaScript code - uncomment the one you want.
*
* OPTION 1
* Load from development server. Start the server from the repository root:
*
* $ npm start
*
* To run on device, change `localhost` to the IP address of your computer
* (you can get this by typing `ifconfig` into the terminal and selecting the
* `inet` value under `en0:`) and make sure your computer and iOS device are
* on the same Wi-Fi network.
*/

当我们希望将您的注意力引到代码块的一个特定部分时,相关的行或项目将以粗体显示:

/**
* OPTION 2
* Load from pre-bundled file on disk. To re-generate the static bundle
* from the root of your project directory, run
*
* $ react-native bundle --minify
*
* see http://facebook.github.io/react-native/docs/runningondevice.html
*/

//jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

任何命令行输入或输出都如下所示:

# cp /usr/src/asterisk-addons/configs/cdr_mysql.conf.sample
 /etc/asterisk/cdr_mysql.conf

新术语重要词汇以粗体显示。屏幕上显示的单词,例如在菜单或对话框中,以如下形式显示:“选择运行,然后在信息选项卡下将构建配置发布更改为调试。”

注意

警告或重要注意事项以如下框中的形式出现。

小贴士

技巧和窍门以如下形式出现。

读者反馈

我们读者的反馈总是受欢迎的。请告诉我们您对本书的看法——您喜欢或不喜欢什么。读者反馈对我们来说很重要,因为它帮助我们开发出您真正能从中获得最大收益的标题。

要向我们发送一般反馈,只需发送电子邮件至<feedback@packtpub.com>,并在邮件主题中提及本书的标题。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从www.packtpub.com下载您购买的所有 Packt 出版物的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

下载本书的彩色图像

我们还为您提供了一个包含本书中使用的截图/图表彩色图像的 PDF 文件。彩色图像将帮助您更好地理解输出的变化。您可以从[www.packtpub.com/sites/default/files/downloads/Getting Started with React Native_ColorImages.pdf](http://www.packtpub.com/sites/default/files/downloads/Getting Started with React Native_ColorImages.pdf)下载此文件。

勘误

尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在我们的某本书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站或添加到该标题的勘误部分下的现有勘误列表中。

要查看之前提交的勘误,请访问www.packtpub.com/books/content/support,并在搜索字段中输入书籍名称。所需信息将出现在勘误部分下。

盗版

互联网上对版权材料的盗版是一个跨所有媒体的持续问题。在 Packt,我们非常重视保护我们的版权和许可证。如果您在互联网上发现任何形式的我们作品的非法副本,请立即提供位置地址或网站名称,以便我们可以寻求补救措施。

请通过<copyright@packtpub.com>与我们联系,并提供疑似盗版材料的链接。

我们感谢您在保护我们的作者和为您提供有价值内容的能力方面的帮助。

问题

如果您对本书的任何方面有问题,您可以通过<questions@packtpub.com>联系我们,我们将尽力解决问题。

第一章. 探索示例应用程序

React Native 正在开始改变移动开发世界。作为网页开发者,您可以使用您已有的技能,获得一套熟悉的方法来为移动设备构建用户界面。在本书中,我们将通过开发一个名为React Notes笔记应用程序,向您介绍许多 React Native 的功能。在构建基本功能,如创建笔记、将笔记保存到设备、查看已保存笔记列表以及在不同屏幕间导航时,您将学习到开发您自己的应用程序所需的基本技能。您还将有机会通过添加将图片和地理位置数据与笔记一起存储的功能来超越基础。功能只是构成优秀应用程序的一部分——它还必须看起来很棒,所以我们确保您对布局和样式有全面的理解。到本书结束时,您将从头到尾开发一个功能齐全的应用程序,并拥有分享您的 React Native 应用程序所需的所有技能!

在本章中,我们将向您介绍 React Notes,这是您将学习如何构建的示例应用程序。如果您急于开始尝试示例应用程序以查看会发生什么,我们甚至会为您指明正确的方向。

本章将重点介绍以下内容:

  • 在 Mac OS X 上安装 Xcode

  • 在 iOS 模拟器中运行示例应用程序

  • 查看示例应用程序的功能

  • 修改示例应用程序

安装 Xcode

在 OS X 中获取运行示例应用程序的工具很简单。安装 Xcode 最简单的方法是通过 App Store。在右上角的栏中,搜索术语Xcode,然后从结果列表中导航到 Xcode 商店页面,如下面的截图所示:

安装 Xcode

通过点击按钮安装或更新到 Xcode 的最新版本。

注意

您需要注册 Apple ID 才能从 App Store 下载 Xcode。

您还需要 Xcode 的命令行工具CLT)。当需要安装时,会显示提示。您也可以直接从苹果开发者的developer.apple.com/downloads/下载部分下载命令行工具。

运行示例应用程序

源代码包含我们将全书构建的应用程序。我们将从运行应用程序开始。源代码已经配置好,可以在 iOS 模拟器中运行:

  1. 在 Xcode 中打开ios/文件夹中的ReactNotes.xcodeproj,或从命令行打开:

    ReactNotes$ open ios/ReactNotes.xcodeproj/
    
    

    运行示例应用程序

    小贴士

    下载示例代码

    您可以从www.packtpub.com下载您购买的所有 Packt Publishing 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。

  2. 本书针对 iPhone 6 进行开发;尽管它也适用于其他 iOS 版本,但我们建议使用这个版本。确保在 iOS 模拟器设备下拉菜单中选择 iPhone 6。如果您拥有 iPhone 6,您可以选择iOS 设备运行样本应用程序

  3. 按下运行按钮(F5)以启动 iOS 模拟器:运行样本应用程序

样本应用程序的预览

本书的目标是向您介绍 React Native 如何快速帮助您搭建用户界面。无论您构建什么类型的移动应用程序,您都极有可能需要某些功能。您的 UI 可能包含多个屏幕,因此您需要能够在它们之间导航的能力。在第三章从示例应用程序开始中,我们将开始为导航和笔记屏幕打下基础:

样本应用程序的预览

在您看到裸机应用程序不久之后,您可能想要开始让它看起来更好。让我们深入到第四章处理样式和布局中,并将这些课程贯穿到本书的其余部分。

很难想象一个没有数据列表的应用程序,React Notes 也不例外。我们将在第五章显示和保存数据中介绍如何处理列表:

样本应用程序的预览

区分移动应用程序和 Web 应用程序的能力之一是访问 GPS 数据的能力。我们在第六章处理地理位置和地图中展示了如何使用地图捕获地理位置数据:

样本应用程序的预览

在移动设备上捕捉照片是非常常见的。相机屏幕将允许用户将照片附加到他们的笔记中,并保存下来以便稍后查看。你将在第七章使用原生模块中学习如何为你的应用程序添加相机支持:

样本应用程序的预览

注意

注意,在 iOS 模拟器中相机屏幕将是黑色的。这也在第七章使用原生模块中稍后进行了解释。

在样本应用程序中进行实验

如果你是个喜欢冒险的人,那么请随意开始尝试和修改示例应用程序代码。将 iOS 应用程序切换到开发模式有两个步骤:

  1. 在 Xcode 中打开AppDelegate.m文件,取消注释OPTION 1中的jsCodeLocation赋值,并注释掉OPTION 2中的语句:

    NSURL *jsCodeLocation;
    
    /**
    * Loading JavaScript code - uncomment the one you want.
    *
    * OPTION 1
    * Load from development server. Start the server from the repository root:
    *
    * $ npm start
    *
    * To run on device, change `localhost` to the IP address of your computer
    * (you can get this by typing `ifconfig` into the terminal and selecting the
    * `inet` value under `en0:`) and make sure your computer and iOS device are
    * on the same Wi-Fi network.
    */
    
    jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios"];
    
    /**
    * OPTION 2
    * Load from pre-bundled file on disk. To re-generate the static bundle
    * from the root of your project directory, run
    *
    * $ react-native bundle --minify
    *
    * see http://facebook.github.io/react-native/docs/runningondevice.html
    */
    
    //jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];
    
  2. 然后,转到产品 | 方案 | 编辑方案…。选择运行,在信息选项卡下将构建配置发布更改为调试,如图所示:实验示例应用程序

  3. 从 Xcode 中运行F5)以在开发模式下启动应用程序。使用 iOS 模拟器的Shake手势(硬件 | 摇动 | 手势)将显示开发菜单。可能需要从命令行运行react-native start来加载 JavaScript 包。

就这样!从这里你可以自由地修改index.ios.js中的任何源代码或在Components文件夹中的代码。稍后我们将解释如何在模拟器中快速重新加载你的代码,而无需从 Xcode 重新编译。

摘要

本章为我们简要概述了本书其余部分我们将介绍的功能类型和用户界面。我们将深入探讨导航、列表、用户输入等功能。在 Xcode 已经设置好的情况下,你将能够直接开始 iOS 开发,对于 Android 开发者,我们将在第三章,从示例应用程序开始中开始设置。接下来,我们将展示 React Native 如何利用你作为网络开发者所学的技能,在快速移动开发中提供价值。

让我们开始吧!

第二章:理解 React Native 基础知识

你可能对 React Web 的工作原理不太熟悉,所以我们将在本章中介绍其基础知识。我们还将解释 React Web 在底层是如何工作的核心原则。一旦你对基础知识有了扎实的理解,我们将深入探讨 React Web 的工作原理以及移动端和 Web 端之间的细微差别。到本章结束时,你将具备开始构建示例应用程序所需的技能。

在本章中,我们将涵盖以下主题:

  • 虚拟 DOM

  • 介绍组件和 JSX

  • 编写我们的第一个组件

  • 组件的属性和状态

虚拟 DOM

你知道如何编写 JavaScript 函数吗?如果你知道,那太好了!你正走在理解 React 和 React Native 底层工作原理的道路上。我们具体指的是什么?当你研究 React 的工作原理时,你最终会遇到有人用以下方式解释它:

UI = f(data)

你可能会说,警告:这有什么用? 好吧,这是说你的 UI 是你的数据的函数。用更熟悉的话来说,让我们说:

var todos = function(data) { return data.join( " -- " ) }

你可以用数据数组调用该函数,例如:

var ui = todos( ["wake up", "get out of bed", "drag a comb across my head"] );
console.log(ui);

这不是特别震撼人心的代码;然而,你现在正在渲染一些内容,在这种情况下是到控制台。

如果所有的 UI 渲染代码都能如此可预测呢?它可以!让我们开始变得稍微高级一些。除了我们的todos()函数外,如果我们还有一个名为todoItem()的函数,例如:

var todoItem = function(data) { return "<strong>" + data + "</strong>" }

这看起来很像我们的原始UI函数,不是吗?:

UI = f(data)

假设我们开始组合我们的todos()todoItems(),例如:

var ui = todos( [todoItem("wake up"), todoItem("get out of bed")] );

你可以开始想象,我们可以通过组合简单的函数来渲染越来越复杂的输出。

如果我们想开始将内容渲染到浏览器中呢?我敢肯定你能想象将我们的todoItem()改为使用 jQuery 添加元素到 DOM;然而,在这种情况下,我们将开始重复很多次,有很多appendChild()调用和 jQuery 选择器的实例。如果我们真的很聪明,我们可能会编写一个框架来抽象 DOM 操作,这样我们就可以编写对我们应用程序重要的代码,而不仅仅是针对浏览器的代码。

好吧,现在假设我们神奇地获得了一个框架,它允许我们将 UI 表示为一个data函数,我们不必考虑内容是如何渲染到 DOM 中的。我们可以不断地更改我们的数据,并观察 DOM 的更新!从理论上讲,这听起来很棒,但当我们有数十个深度嵌套的div元素时,底层的 DOM 操作变得复杂且效率低下。

如果我们的魔法框架有一个 DOM 的中间表示形式怎么办?让我们称它为虚拟 DOM,并且让我们说,我们不是对 DOM 进行每一个小更改,而是将更改批量处理在一起。我们甚至可以比较虚拟 DOM 的当前和之前状态。找出差异,减少我们需要执行的真正 DOM 操作次数。现在我们真的开始有所作为了!

因此,我们现在可以将我们的 UI 表达为数据的一个函数。我们不必考虑底层的 DOM 操作代码,而且我们的 UI 既美观又迅速,因为底层框架非常智能,减少了它需要执行的 DOM 操作次数。有一个能够为我们做这件事的框架将会非常棒,但你知道吗,什么会真正酷?如果 DOM 不需要是浏览器 DOM 怎么办?如果那个允许我们编写对应用有意义的代码的相同抽象能够用来,比如说,更新原生移动组件怎么办?这就是 React Native。

组件

现在有一个有趣的问题;我们遇到了这个伟大的框架,用于快速在虚拟 DOM 和其原生组件之间进行差异比较。我们如何告诉 React Native 要表示什么 UI 或何时更改它?一个 React Native 组件是一个简单、可重用的、类似函数的对象,使我们能够描述我们想要渲染的原生移动组件。它们将始终包含属性、状态和一个渲染方法。让我们从创建我们自己的组件开始,从非常简单的地方开始。

创建您的第一个组件

在 React Native 中创建一个新的组件将类似于以下内容:

import React, {
  Text,
  View
  } from 'react-native';
class HelloComponent extends React.Component {
  render () {
    return (
    <View>
      <Text>Hello React</Text>
    <View>
  );
  }
}

小贴士

记得导入 React Native 模块。在这里,我们使用 ES6 导入语句;它类似于 node require 模块的工作方式。

等等… 这些奇怪的 XML 元素在我的 JavaScript 代码中做什么?Facebook 为描述 React 组件在 JavaScript 上创建了自己的语法扩展。以下是完全相同的代码,但用普通的 JavaScript 编写:

var HelloComponent = React.createClass({displayName: "HelloComponent"}, render: function () {
  return (
    React.createElement(View, null,
      React.createElement(Text, null, "Hello React")
  )
));

虽然只使用 JavaScript 编写 React Native 应用程序是可能的,但之前的语法为开发者提供了许多额外的优势。

JSX

JavaScript XML(JSX)是 ECMAScript 规范的一个类似 XML 的扩展。它将组件逻辑(JavaScript)和标记(DOM 或原生 UI)合并到一个文件中。

JSX 元素将具有以下形式:

var element = (
  <JSXElement>
    <SubJSXElement />
    <SubJSXElement />
    <SubJSXElement />
  <JSXElement />
);

JSX 规范还定义了以下内容:

  • JSX 元素可以是自闭合的<JSXElement></JSXElement>或自闭合的<JSXElement />

  • 以表达式{}或字符串""的形式接受属性<Component attr="attribute">。表达式是 JavaScript 片段。

  • 子元素可以是文本、表达式或元素。

注意

如果你有多个组件或组件列表怎么办?

只能有一个根元素;这意味着如果你有多个组件,你必须将它们包裹在一个父组件中。

这很酷!我们已经从深层嵌套和命令式的 JavaScript 代码转变为描述我们希望在组件中看到的精确元素的声明性格式。由于我们的逻辑与我们的标记耦合,因此没有关注点的分离,这使得组件更容易调试和测试。由于你总是可以将相同的组件包含在多个其他组件中,因此根本不需要重复代码。

注意,JSX 仅应作为预处理器使用,不建议在生产构建中进行转译。有关 JSX 的更多信息,请参阅官方 React 文档facebook.github.io/react/docs/jsx-in-depth.html或官方 JSX 规范facebook.github.io/jsx/

回到我们的第一个组件

在我们的组件中,我们忽略了一些事情。ViewText是 React Native 提供的许多组件中的一部分,用于构建 UI。这些不是在 JavaScript 层渲染的常规组件,它们可以直接映射到其原生容器部分!View 组件映射到 iOS 中的UIView和 Android 中的android.view,而 Text 是分别在每个平台上显示文本的通用组件。ViewText支持各种功能,如布局、样式和触摸处理。

反复显示相同的静态文本并不令人兴奋。让我们扩展这个简单的组件并添加一些更多功能。

属性和状态

在这个阶段,你可能想知道随着组件数量的增长形成组件层次结构时,React Native 是如何处理组件操作和通信的。组件层次结构类似于树,从根组件开始,可以包含许多子组件。React Native 提供了两种数据传递方法;一种用于组件层次结构中的数据流向下传递,另一种用于维护内部状态。

属性

同一组件层次结构中的组件之间是如何进行通信的呢?数据通过通常称为属性的属性向下传递。按照惯例,属性被认为是不可变的,不应直接修改。要将属性传递给组件,只需在组件中添加一个驼峰式命名的属性即可:

<HelloComponent text="Hello React" />

可以通过this.props在组件内部访问属性:

import React, {
  Text,
  View
} from 'react-native';

class HelloComponent extends React.Component {
  render () {
    return (
      <View>
        <Text>{this.props.text}</Text>
      View>
    );
  }
}

小贴士

如果我想要传递很多属性怎么办?

使用 ES7 扩展运算符<HelloComponent {...props} />可以将属性数组传递给组件。

并非总是需要将属性包含在组件中,但如果你需要为属性指定默认值,可以将defaultProps对象分配给组件类的构造函数。

HelloComponent.defaultProps = {text: "Default Text!"};

验证属性

如果你打算将你的组件公开给公众,限制开发者使用它的方式是有意义的。为了确保你的组件被正确使用,可以使用 PropTypes 模块来验证传入的任何 props。如果某个 prop 未通过 propType 验证,则会在控制台中向开发者显示警告。PropTypes 覆盖了广泛的 JavaScript 类型和中型,包括嵌套对象。你可以在组件的类构造函数上定义 propTypes

HelloComponent.propTypes = {text: React.PropTypes.string};

更多关于 propTypes 的信息,请访问 React 文档的 Prop 验证部分 facebook.github.io/react/docs/reusable-components.html

状态

因此,现在我们可以传递数据,但如果数据发生变化,我们如何向用户显示这些变化呢?组件可以包含可选的状态,这是一个可变且私有的数据集。状态是跟踪用户输入、异步请求和事件的绝佳方式。当用户与组件交互时,让我们更新我们的组件以添加额外的文本:

import React, {
  Text,
  View,
  Component
  } from 'react-native';
  class HelloComponent extends React.Component{
    constructor (props) {
      super(props);
      this.state = {  // Set Initial State
      appendText: ''
    };
  }
  render () {
    return (
      <View>
        <Text onPress={() => setState({text: ' Native!'})}>{this.props.text + this.state.appendText}</Text>
        <View>
    );
  }
}

触摸 Text 组件将触发其 onPress prop 中的函数。我们正在利用 ES6 箭头语法将我们的功能与文本组件一起放在一行中。

注意

使用 ES6 箭头语法将自动将 this 绑定到函数上。对于任何非箭头函数,如果你需要访问 this,那么你需要在 <Text onPress={this.myFunction.bind(this)}> 的 props 表达式中将值绑定到函数上。

setState 函数将你传递给第一个参数的对象与组件的当前状态合并。调用 setState 将触发一个新的渲染,其中 this.state.appendText 将在原始从 props 传递的文本值上追加 Native!,最终结果是 "Hello React" + " Native!",生成 "Hello React Native!"

永远不要尝试直接修改这个状态值。直接更改状态可能导致下一次 setState 调用期间数据丢失,并且不会触发另一个重新渲染。

摘要

现在,希望你能理解 React 在实现性能方面所采取的激进新方向。虚拟 DOM 在幕后处理所有的 DOM 操作。同时,它使用高效的差异算法来最小化对 DOM 的调用次数。我们也看到了 JSX 如何允许我们声明式地表达我们的组件,并将应用程序逻辑合并到一个文件中。通过使用 props 和 state,我们可以通过组件传递数据并动态更新它们。

希望你现在能够利用本章学到的信息,说服你的老板立即开始使用 React Native!

第三章。从示例应用程序开始

现在你已经了解了 React Native 的工作原理以及如何创建组件,让我们创建你的第一个 React Native 应用程序。在整个书中,我们将开发一个记事应用,我们将其称为 ReactNotes。到书末,你将拥有一个功能齐全的应用程序,允许你创建笔记、将它们保存到设备上、查看已保存的笔记列表、使用设备拍照并将照片附加到笔记中,等等。

在本章中,我们将构建应用程序的框架,创建 HomeScreenNoteScreen。我们还将添加导航,允许你在屏幕之间切换,在这个过程中,你将学习如何创建自己的组件和处理事件。

本章我们将涵盖的主题包括:

  • 如何生成 iOS 和 Android 项目文件

  • 检查 React Native 入门模板

  • 创建第一个组件,SimpleButton

  • 使用 Chrome 开发者工具进行调试

  • 探索导航和屏幕间切换

  • 开发创建笔记的 UI

生成项目

要开始构建我们的 iOS 记事应用,我们需要一些命令行工具。

太好了,现在我们有了这些工具,我们可以安装 react-native-clireact-native-cli 提供了一个接口,为我们设置新的 React Native 项目:

  1. 要安装 react-native-cli,请使用 npm 命令:

    npm install -g react-native-cli
    
    
  2. 接下来,我们将使用 clireact-native init 命令生成一个名为 ReactNotes 的新 React Native 项目。命令的输出类似于以下内容:

    $ react-native init ReactNotes
    
    

    这将指导你创建一个位于 /Users/ethanholmes/ReactNotes 的新 React Native 项目。

  3. /Users/ethanholmes/ReactNotes 中设置一个新的 React Native 应用:

      create .flowconfig
      create .gitignore
      create .watchmanconfig
      create index.ios.js
      create index.android.js
      create ios/main.jsbundle
      create ios/ReactNotes/AppDelegate.h
      create ios/ReactNotes/AppDelegate.m
      create ios/ReactNotes/Base.lproj/LaunchScreen.xib
      create ios/ReactNotes/Images.xcassets/AppIcon.appiconset/Contents json
      create ios/ReactNotes/Info.plist
      create ios/ReactNotes/main.m
      create ios/ReactNotesTests/ReactNotesTests.m
      create ios/ReactNotesTests/Info.plist
      create ios/ReactNotes.xcodeproj/project.pbxproj
      create ios/ReactNotes.xcodeproj/xcshareddata/xcschemes/ReactNotes.xcscheme
      create android/app/build.gradle
      create android/app/proguard-rules.pro
      create android/app/src/main/AndroidManifest.xml
      create android/app/src/main/res/values/strings.xml
      create android/app/src/main/res/values/styles.xml
      create android/build.gradle
      create android/gradle.properties
      create android/settings.gradle
      create android/app/src/main/res/mipmap-hdpi/ic_launcher.png
      create android/app/src/main/res/mipmap-mdpi/ic_launcher.png
      create android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
      create android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
      create android/gradle/wrapper/gradle-wrapper.jar
      create android/gradle/wrapper/gradle-wrapper.properties
      create android/gradlew
      create android/gradlew.bat
      create android/app/src/main/java/com/reactnotes/MainActivity.java
    

    要在 iOS 上运行你的应用:

       Open /Users/ethanholmes/ReactNotes/ios/ReactNotes.xcodeproj in Xcode
       Hit Run button
    

    要在 Android 上运行你的应用:

       Have an Android emulator running, or a device connected
       cd /Users/ethanholmes/ReactNotes
       react-native run-android
    

    Xcode 项目的 root 目录在 ReactNotes 文件夹中生成,其名称与我们运行 react-native-cli 命令时给出的名称相同。查看 React Native 设置步骤的末尾,以了解它产生的结果。

Xcode 和 iOS 模拟器

我们将首先通过 Xcode 在 iOS 模拟器中运行入门模板:

  1. 在 Xcode 中,选择 文件 | 打开 并导航到 ReactNotes 文件夹。

  2. 打开 ReactNotes.xcodeproj 文件,如图所示:Xcode 和 iOS 模拟器

  3. 点击 运行(或 Cmd + R)在 iOS 模拟器中运行应用程序,以下截图将显示:Xcode 和 iOS 模拟器

    就这样,我们已经在 iOS 模拟器上成功运行了 React Native 模板!

Android SDK 和模拟器

Facebook 在 Android SDK 和模拟器上有一个详细的分步指南。您可以在 facebook.github.io/react-native/docs/android-setup.html 访问 React Native 文档。在本节中,我们只介绍在 Android 模拟器上运行应用程序的基本知识。

当在 iOS 模拟器中运行项目时,我们可以从 Xcode IDE 中运行它。另一方面,Android 不需要特定的 IDE,可以直接从命令行启动。

要将 android apk 安装到模拟器,请使用以下命令:

$ react-native run-android

下面的截图将生成:

Android SDK 和模拟器

让我们从修改启动模板的内容并显示不同的消息开始。

修改 React Native 启动模板

在文本编辑器中打开位于根目录中的 index.ios.js。以下是 react-native-cli 生成的代码:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 */
'use strict';

var React = require('react-native');
var {
  AppRegistry,
  StyleSheet,
  Text,
  View,
} = React;

var ReactNotes = React.createClass({
  render: function() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          Welcome to React Native!
        </Text>
        <Text style={styles.instructions}>
          To get started, edit index.ios.js
        </Text>
        <Text style={styles.instructions}>
          Press Cmd+R to reload,{'\n'}
          Cmd+D or shake for dev menu
        </Text>
      </View>
    );
  }
});

var 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('ReactNotes', () => ReactNotes);

注意

虽然 react-native-cli 使用 ES5 的 createClass 生成启动模板,但我们将使用 ES6 类来创建我们的组件。

这里包含了很多内容,但请耐心等待,我们将为您逐一解释。如果我们仔细查看渲染方法,我们可以看到我们在上一章中遇到的熟悉的 ViewText 组件。注意索引文件本身就是一个组件(ReactNotes)。将第 30 行的值更改为 Welcome to React Notes!。保存后,然后从模拟器中按 Cmd + R,或者在顶部菜单中导航到 硬件 | 摇动手势,并从弹出操作表中选择 重新加载。屏幕上的文本重新渲染以显示我们刚刚修改的文本!我们不再受限于等待 Xcode 重新编译才能看到我们的更改,因为我们可以直接从模拟器中重新加载。继续进行更改,并在模拟器中重新加载以获得工作流程的感觉。

应用程序的架构

是时候给我们的应用程序添加一点交互性了。您可以从向屏幕添加一个可触摸的简单按钮组件开始。在根目录中创建一个名为 App 的文件夹,并在 App 文件夹内创建一个名为 Components 的文件夹。在 Components 目录中添加一个名为 SimpleButton.js 的文件。这将是我们存储和引用创建的组件的目录。

注意

注意,本章中创建的 React Native 代码将适用于 iOS 和 Android。如果您只对 Android 感兴趣,只需将 index.ios.js 替换为 index.android.js。截图和说明将主要针对 iOS 模拟器。

创建 SimpleButton 组件

让我们从将一些文本渲染到屏幕并将它导入到我们的 index.ios.js 文件开始。在 SimpleButton.js 中添加:

import React, {
    Text,
    View
} from 'react-native';

export default class SimpleButton extends React.Component {
  render () {
    return (
      <View>
        <Text>Simple Button</Text>
      </View>
    );
  }
}

注意

ES6 解构赋值 var [a, b] = [1, 2]; 用于从 React Native 模块中提取 TextView

我们将把新创建的组件包含到 index.ios.js 中,并将其简化为 ES6 语法:

import React, {
  AppRegistry,
  StyleSheet,
  View
} from 'react-native';

import SimpleButton from './App/Components/SimpleButton';

class ReactNotes extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <SimpleButton />
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }
});
AppRegistry.registerComponent('ReactNotes', () => ReactNotes);

上一段代码的输出结果为:

创建 SimpleButton 组件

我们已经取得了良好的开端;现在是时候给我们的按钮添加一些交互性了。在 SimpleButton.js 中,将 TouchableOpacity 组件添加到解构赋值中。TouchableHighlightTouchableOpacityTouchableWithoutFeedback 是响应触摸的类似组件,它们需要一个 onPress 属性来执行触摸时的响应。将现有代码包裹在渲染函数中的 TouchableOpacity 组件内:

  import React, {
  Text,
  TouchableOpacity,
  View
} from 'react-native';

export default class SimpleButton extends React.Component {
  render () {
    return (
      <TouchableOpacity onPress={() => console.log('Pressed!')}>
        <View>
          <Text>Simple Button</Text>
        </View>
      </TouchableOpacity>
    );
  }
}

现在尝试点击(或单击)文本,你应该能够看到当你按下它时文本的透明度降低。但我们的 console.log(…) 输出到哪里去了?打开 开发者 菜单(硬件 | 摇动手势)并在 Chrome 中选择 调试。这将打开一个 Chrome 窗口,地址为 localhost:8081/debugger-ui,用于调试,如下面的截图所示:

创建 SimpleButton 组件

嘿,看这里,这是我们 SimpleButton 组件中指定的控制台日志。幕后,JavaScript 代码在 Chrome 标签页中运行,并在启动或重新加载时加载到移动设备上。从这里,你可以访问你通常使用的所有 Chrome 开发者工具,包括添加断点。

导航

现在,是时候让我们的应用程序更具行动力了。让我们首先将 SimpleButton 转换为“创建笔记”按钮。当用户点击“创建笔记”按钮时,它会将用户过渡到另一个屏幕来创建笔记。为此,我们需要我们的按钮能够通过 index.ios.js 的 props 接受一个函数来激活过渡。我们还将添加一些自定义文本以增加额外的魅力:

  import React, {
  Text,
  TouchableOpacity,
  View
} from 'react-native';

export default class SimpleButton extends React.Component {
  render () {
    return (
      <TouchableOpacity onPress={this.props.onPress}>
        <View>
          <Text>{this.props.customText || 'Simple Button'}</Text>
        </View>
      </TouchableOpacity>
    );
  }
}

SimpleButton.propTypes = {
  onPress: React.PropTypes.func.isRequired,
  customText: React.PropTypes.string
};

现在,我们已经将我们的 SimpleButton 组件扩展为可重用,只需进行最小改动。我们总是可以通过 onPress 属性传递不同的函数,并且如果我们选择的话,还可以添加自定义文本。这就是我们需要修改 SimpleButton 的全部内容;现在要将过渡功能包含到我们的 index.io.js 文件中。

以下图像显示了重新访问验证 props 的页面:

导航

小贴士

记得上一章中的 propTypes 吗?如果我们忘记传递 onPress 属性,控制台将记录一个警告,提醒我们传递它。注意,对于 customText 没有警告,因为它没有被设置为 isRequired

导航组件

Navigator组件是 React Native 提供的UINavigationController的重实现,用于管理各种屏幕。类似于堆栈,你可以在导航器上推送、弹出和替换路由。它在 iOS 和 Android 上都是完全可定制的,我们将在下一章中介绍。将导航器导入到index.ios.js中,并用以下内容替换render方法的内容:

import React, {
  AppRegistry,
  Navigator,
  StyleSheet,
  View
} from 'react-native';

render () {
  return (
    <Navigator
      initialRoute={{name: 'home'}}
      renderScene={this.renderScene}
    />
  );
}

导航器接收一个名为initialRoute的属性,它接受一个对象,作为第一个要放入堆栈的路由。路由对象可以包含你需要传递给屏幕组件的任何属性。我们现在只需要过渡到我们想要屏幕的名称。接下来,我们需要创建传递给renderScene属性的function。在ReactNotes组件中,我们将创建一个接受routenavigator作为参数的function,如下所示:

class ReactNotes extends React.Component {
  renderScene (route, navigator) {
     ...
  }
  render () {
    ...
  }
}

当我们首次加载应用程序时,参数route将是传递到initialRoute的对象。使用switch语句并查看route.name的值,我们可以选择我们想要渲染的组件:

renderScene (route, navigator) {
  switch (route.name) {
    case 'home':
      return (
        <View style={styles.container}>
          <SimpleButton
            onPress={() => console.log('Pressed!')}
            customText='Create Note'
          />
        </View>
      );
    case 'createNote':
  }
}

在这里,在home情况下,你可以看到我们从ReactNotes中的原始render方法稍作修改的代码;我们包含了之前创建的onPresscustomText属性。你可以在App/Componets/下添加另一个名为NoteScreen.js的组件;这个屏幕将包含创建新笔记的功能:

import React, {
  StyleSheet,
  Text,
  View
} from 'react-native';

export default class NoteScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <Text>Create Note Screen!</Text>
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }
});

目前,我们只会在点击创建笔记按钮时使用这个屏幕。在onPress属性的箭头函数中,我们将使用navigator.push将一个新的路由推送到堆栈:

import NoteScreen from './App/Components/NoteScreen';

class ReactNotes extends React.Component {
  renderScene (route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <View style={styles.container}>
            <SimpleButton
              onPress={() => {
                navigator.push({
                  name: 'createNote'
                });
              }}
              customText='Create Note'
            />
          </View>
        );
      case 'createNote':
        return (
            <NoteScreen />
        );
    }
  }

注意,push也接受一个常规的 JavaScript 对象,因此我们需要为我们的NoteScreen包含名称属性;在模拟器中重新加载应用程序并点击创建笔记按钮。两个屏幕之间将发生平滑的动画过渡,而无需添加任何额外的代码。

到目前为止,你可能正在想按钮是可以的,但是有没有更好的、更原生的方式来处理导航?当然,作为导航组件的一部分,你可以传递一个navigationBar属性来在每一个屏幕上添加一个持久的顶部导航栏。Navigator.NavigationBar是一个子组件,它接受一个定义左侧和右侧按钮、标题和样式的对象(尽管我们将在下一章将其设置为unstyled)。修改ReactNotesrender函数以包含navigationBar,如下所示:

render () {
  return (
    <Navigator
      initialRoute={{name: 'home'}}
      renderScene={this.renderScene}
      navigationBar={
        <Navigator.NavigationBar
          routeMapper={NavigationBarRouteMapper}
        />
      }
    />
  );
}

routeMapper属性接受一个包含LeftButtonRightButtonTitle属性函数的对象。让我们在index.ios.js顶部的导入之后插入这个对象:

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    ...
  },

  RightButton: function(route, navigator, index, navState) {
    ...
  },

  Title: function(route, navigator, index, navState) {
    ...
  }
};

将我们的应用程序流程推进到CreateNote屏幕将需要显示导航栏中的右侧按钮。幸运的是,我们已经有了一个简单的按钮设置,可以将状态推送到导航器。在RightButton函数中添加:

var NavigationBarRouteMapper = {
  ...

  RightButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => {
              navigator.push({
                name: 'createNote'
              });
            }}
            customText='Create Note'
          />
        );
      default:
         return null;
    }
  },

  ...
};

与我们之前的renderScene方法类似,我们可以根据route.name的值进行切换。switch语句中的默认表达式是为了确保除非我们包含它们,否则不同的屏幕不会返回按钮。让我们还添加一个LeftButtonNavigationBar,当它在NoteScreen上时,以便返回主页。

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'createNote':
        return (
          <SimpleButton
            onPress={() => navigator.pop()}
            customText='Back'
           />
        );
      default:
        return null;
    }
  },

  ...
};

navigator.pop()将移除栈顶的路由;因此,返回到我们的原始视图。最后,为了添加标题,我们在Title属性函数中做完全相同的事情:

var NavigationBarRouteMapper = {
  ...

  Title: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <Text>React Notes</Text>
        );
      case 'createNote':
        return (
          <Text>Create Note</Text>
        );
    }
  }
};

现在,让我们更新原始的renderScene函数,以移除按钮并包含主页作为组件。创建一个新的组件叫做HomeScreen;这个屏幕的内容现在不会很重要,因为我们稍后会回到它:

import React, {
  StyleSheet,
  Text,
  View
  } from 'react-native';
export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <Text>Home</Text>
      </View>
    );
  }
}
var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  }
});

然后将其导入到index.ios.jsindex.android.js

import HomeScreen from './App/Components/HomeScreen';

...

class ReactNotes extends React.Component {
  renderScene (route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen />
        );
      case 'createNote':
        return (
          <NoteScreen />
        );
    }
  }

  ...

}

现在,让我们看看导航栏是如何在每个路由中保持一致的:

Navigator.NavigationBar

就这样!重新加载并查看静态导航栏是如何在每个路由中保持一致的:

Navigator.NavigationBar

要获取关于 Navigator 的更详细指南,请查看 React Native 文档facebook.github.io/react-native/docs/navigator.html。我们现在有了适当的基础设施,可以开始向我们的应用程序添加创建笔记的功能。

NoteScreen – 第一次尝试

现在我们有了NoteScreen并且可以导航到它,让我们开始让它变得有用。我们需要添加一些TextInput组件,一个用于笔记的标题,一个用于捕获正文。我们希望自动将焦点设置在标题的TextInput上,以便用户可以立即开始输入。我们需要监听TextInput组件上的事件,以便通过更新状态来跟踪用户输入的内容。我们还希望知道用户何时完成了笔记标题的编辑,这样我们就可以自动将焦点设置在正文的TextInput上。

首先,让我们将TextInput组件添加到我们的依赖列表中,并移除不再需要的Text组件:

import React, {
  StyleSheet,
  TextInput,
  View
}from 'react-native';

在我们将TextInput组件添加到View之前,让我们先处理一些样式更新:

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 64
  },
  title: {
    height: 40
  },
  body: {
    flex: 1
  }
});

注意,我们在容器中添加了marginTop: 64。这很重要,因为我们想确保NavigationBar不会意外地拦截我们希望TextInput接收的onPress事件。我们还为即将添加的每个TextInput添加了样式。我们将在第四章中更详细地讨论样式,使用样式和布局

现在,在我们的渲染函数中,让我们将Text组件替换为两个TextInput组件,例如:

render () {
  return (
    <View style={styles.container}>
      <TextInput placeholder="Untitled"
        style={styles.title}/>
      <TextInput multiline={true}placeholder="Start typing" style={styles.body}/></View>
  );
}

在我们尝试之前,请注意 TextInput 组件有一个占位符属性,它允许我们告诉用户 TextInput 的用途,而无需通过标签我们的表单字段来占用额外的屏幕空间。我还在第二个 TextInput 上指定了 multiline={true},以便用户可以添加他们想要的任意多的文本。

现在,让我们在模拟器中刷新应用程序,你应该会看到类似这样的内容:

NoteScreen – 第一次尝试

您应该能够点击进入 TextInput 并开始输入。如果您想在模拟器中使用可用的屏幕键盘,可以按 Cmd+K / Ctrl+K

让我们通过使标题 TextInput 自动聚焦并显示键盘,当用户导航到 NoteScreen 时来稍微提升用户体验:

<TextInput
  ref="title"
  autoFocus={true}
  placeholder="Untitled"
 style={styles.title}
/>

为了更加用户友好,让我们监听一个事件,告诉我们用户已经完成了标题的编辑,并自动将焦点设置在主体 TextInput 上。为此,我们需要对主体 TextInput 进行一些小的修改,以便我们可以在事件处理器中引用它:

<TextInput
  ref="body"
  multiline={true}
  placeholder="Start typing"
  style={styles.body}
/>

注意 ref="body"。任何 React 组件都可以被赋予一个 ref,以便在您的 javascript 代码中引用。现在,在标题 TextInput 中,我们可以添加一个 onEndEditing 事件处理器,以便将焦点设置在 TextInput 的主体上:

<TextInput
  autoFocus={true}
  placeholder="Untitled"
  style={styles.title}
  onEndEditing={(text) => {this.refs.body.focus()}}
  />

注意

避免在组件上使用 refs 来设置和获取值!那是 state 的用途,我们将在第五章 显示和保存数据中学习所有关于 state 的内容。

现在,当您在模拟器中刷新应用程序并导航到 NoteScreen 时,您将看到标题 TextInput 有焦点,并且您应该能够输入一些内容。按 Enter 键,您会看到焦点自动切换到主体,并开始在那里输入。如果您在尝试此操作时没有看到屏幕键盘,请按 Cmd + K / Ctrl + K 并再次尝试。

摘要

在本章中,我们创建了 ReactNotes 应用程序的骨架,向您介绍了如何创建新项目,创建了 Views 和自定义组件,在 HomeScreenNoteScreen 之间导航,并调试了您的应用程序。

您现在已经为我们将在本书的其余部分介绍的所有主题打下了坚实的基础。然而,这个应用程序有两个大问题,它不够美观,而且没有任何功能!在接下来的两个章节中,我们将解决这两个问题,您将朝着掌握 React Native 的道路迈进!

第四章:使用样式和布局

到目前为止,你可能觉得应用程序缺少一定的吸引力。任何应用程序的成功在很大程度上取决于用户界面的外观。就像 React Native 从 Web 上的 React 中借鉴一样,样式也是如此。在本章中,你将学习 React Native 如何使用 React CSS 样式化和布局组件。

我们将涵盖以下主题:

  • 什么是 React CSS?

  • 创建样式表

  • 扩展SimpleButton以包含自定义样式

  • Flexbox 布局简介

  • 样式化NavigationBar

  • 样式化NoteScreen

React CSS

如果你有任何为浏览器编写 CSS 的经验,那么你将感到 React Native 中的样式很舒适。尽管如此,Facebook 在 JavaScript 中开发了一个 CSS 子集版本,而不是浏览器中层叠样式的实现。这种方法的优点是设计师可以充分利用 JavaScript 中的功能,如变量和条件,这是 CSS 原生不支持的功能。

样式表

样式表是 React Native 使用对象表示法声明样式的抽象。组件可以使用任何样式,所以如果你发现你无法获得正确的外观,请参考该组件在 React Native 文档中的样式部分。

在插入样式时,通常只包括特定组件所需的样式。这类似于 JSX 如何将 JavaScript 逻辑和标记合并到一个组件中;我们也将定义我们的样式在同一文件中。

要创建样式表,请使用Stylesheet.create({..})方法,传入一个对象的对象:

var styles = StyleSheet.create({
  myStyle: {
    backgroundColor: '#EEEEEE'
    color: 'black'
  }
});

这看起来与 CSS 类似,但它使用逗号而不是分号。

使用style属性在组件上声明样式为内联

// Using StyleSheet
<Component style={styles.myStyle} />
// Object
<Component style={{color: 'white'}} />

也可以将普通的 JavaScript 对象传递给style属性。这通常不推荐,因为样式表确保每个样式是不可变的,并且在整个生命周期中只创建一次。

样式化SimpleButton组件

让我们进一步扩展我们的SimpleButton组件,使其能够接受按钮背景和文本的自定义样式。在render方法中,让我们从props中设置ViewText组件的style属性:

export default class SimpleButton extends React.Component {
  render () {
    return (
      <TouchableOpacity onPress={this.props.onPress}>
        <View style={this.props.style}>
          <Text style={this.props.textStyle}>{this.props.customText || 'Simple Button'}
          </Text>
        </View>
      </TouchableOpacity>
    );
  }
}

SimpleButton.propTypes = {
    onPress: React.PropTypes.func.isRequired,
    customText: React.PropTypes.string,
    style: View.propTypes.style,
    textStyle: Text.propTypes.style
};

小贴士

重新审视 PropTypes

为了验证,将ViewText样式传递到你的组件中时,使用View.propTypes.styleText.propTypes.style

HomeScreen上,我们将对simpleButton组件进行样式设计,以便在没有笔记时吸引用户的注意力到NoteScreen。我们将首先将其添加到StyleSheet中,并定义一些文本样式:

var styles = StyleSheet.create({
  ...

  simpleButtonText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 16
  }
});

在这里,我们希望按钮上的文本是粗体的,颜色为白色,大小为 16。为了样式化按钮,我们需要向StyleSheet中添加另一个名为simpleButton的对象,并定义一个背景颜色;simpleButton的代码如下:

var styles = StyleSheet.create({
  ...

  simpleButton: {
    backgroundColor: '#5B29C1',
  },
  simpleButtonText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 16
  }
});

让我们看看上一个命令的输出:

SimpleButton 组件的样式

它目前还不够吸引人;让我们添加一些填充,以便用户更容易按下按钮:

paddingHorizontal: 20,
paddingVertical: 15,

注意

paddingVerticalpaddingToppaddingBottom的简写。paddingHorizontalpaddingLeftpaddingRight的简写。

React CSS 没有简写概念,例如border: 1px solid #000。相反,每个项目都是单独声明的:

borderColor: '#48209A',
borderWidth: 1,
borderRadius: 4,

要添加阴影,我们定义每个属性类似于边框:

shadowColor: 'darkgrey',
    shadowOffset: {
        width: 1,
        height: 1
    },
    shadowOpacity: 0.8,
    shadowRadius: 1,

注意阴影偏移需要具有widthheight属性的对象。由于我们处理的是 JavaScript 对象,这种表示法是完全可以接受的。现在,我们将SimpleButton组件包含在我们的HomeScreen渲染方法中:

...
import SimpleButton from './SimpleButton';

export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <Text style={styles.noNotesText}>You haven't created any notes!</Text>

        <SimpleButton
          onPress={() => this.props.navigator.push({
            name: 'createNote'
          })}
          customText="Create Note"
          style={styles.simpleButton}
          textStyle={styles.simpleButtonText}
        />
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  noNotesText: {
    color: '#48209A',
    marginBottom: 10
  },
  simpleButton: {
    backgroundColor: '#5B29C1',
    borderColor: '#48209A',
    borderWidth: 1,
    borderRadius: 4,
    paddingHorizontal: 20,
    paddingVertical: 15,
    shadowColor: 'darkgrey',
    shadowOffset: {
        width: 1,
        height: 1
    },
    shadowOpacity: 0.8,
    shadowRadius: 1,
  },
  simpleButtonText: {
    color: 'white',
    fontWeight: 'bold',
    fontSize: 16
  }
});

更新ReactNotesindex.ios.jsindex.android.js中的renderScene函数,通过props将导航器传递给HomeScreen

class ReactNotes extends React.Component {
  renderScene (route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen navigator={navigator} />
        );
      case 'createNote':
        return (
          <NoteScreen />
        );
    }
  }

  ...

}

让我们看看前面命令的输出:

SimpleButton 组件的样式

对于一个典型的调用操作按钮来说,这已经很不错了。如果你在模拟器中重新加载并按下按钮,它仍然会因为TouchableOpacity的反馈而渐变。有关 React CSS 的更多信息或要贡献,请访问开源 CSS 布局仓库github.com/facebook/css-layout

布局和 Flexbox

由于 Flexbox 是 React Native 布局的基础,我们将深入探讨它。如果你已经熟悉 Flexbox 的复杂性,请随意跳转到样式化 NavigationBar 组件部分。在那里,我们将更多地关注我们在上一章中制作的组件的样式。

Flex 容器

Flex 容器是描述子元素或 Flex 项目布局的父元素。容器的flexDirection属性指定了main-axis;项目渲染的主要方向。与main-axis垂直的线称为cross-axis。容器上的不同 flex 属性会影响项目在每个轴上的对齐方式。flexDirection属性有两个可能的值;row用于水平布局(从左到右)和column用于垂直布局(从上到下)。以下图显示了flexDirectionrow项目从左到右对齐:

Flex 容器

下一图显示了当设置为flexDirectioncolumn时从上到下排列的项目:

Flex 容器

我们可以使用justifyContent帮助在容器中沿着已建立的main-axis移动项目。以下图显示了main-axis上的不同选项:

Flex 容器

注意

注意到space-between不包括左和右边缘的空白,而space-around则包括,但宽度是项目之间空白宽度的一半。

要沿cross-axis移动项目,我们使用alignItems:

弹性容器

项目包装也是可能的,但默认情况下是禁用的。容器内的所有项目都将尝试沿main-axis排列。如果项目太多或太拥挤,你可以应用flexWrap。容器将计算是否需要将项目放置在新的一行或列上。

弹性项目

默认情况下,flex项目将只有其内部内容的宽度。flex属性决定了项目应占用的剩余空间量。可用空间根据每个项目的flex值比例进行划分:

弹性项目

注意第二行的项目都是相同宽度,因为它们的flex值是1。第三行中flex值为2的项目比其他项目多占两倍的空间。

alignItems类似,一个flex项目可以使用alignSelf沿cross-axis对齐自己。

水平和垂直居中

让我们快速看一下 Flexbox 如何使布局更简单。CSS 中最大的挑战之一是水平和垂直居中的元素(花五分钟尝试用常规 CSS 完成这个任务)。我们将首先创建我们的Center组件,并定义一个包含三个flex项目的flex容器:

class Center extends React.Component {
    render () {
        return (
            <View style={styles.container}>
                <View style={[styles.item, styles.one]}>
                    <Text style={styles.itemText}>1</Text>
                </View>
                <View style={[styles.item, styles.two]}>
                    <Text style={styles.itemText}>2</Text>
                </View>
                <View style={[styles.item, styles.three]}>
                    <Text style={styles.itemText}>3</Text>
                </View>
            </View>
        );
    }
}

初始化一个新的StyleSheet并为项目定义一些简单的样式:

var styles = StyleSheet.create({
  item: {
    backgroundColor: '#EEE',
    padding: 25
  },
  one: {
    backgroundColor: 'red'
  },
  two: {
    backgroundColor: 'green'
  },
  three: {
    backgroundColor: 'blue'
  },
  itemText: {
    color: 'white',
    fontSize: 40,
  }
});

现在,我们想要通过justifyContentalignItems来控制项目沿main-axiscross-axis的对齐位置。创建一个容器样式,并将justifyContentalignItems设置为center:

var styles = StyleSheet.create({
  container: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center'
  },

  ...
});

水平和垂直居中

这似乎没有指定behaviour。项目沿main-axis的中心对齐,但没有沿cross-axis对齐。让我们在容器周围添加一个边框来可视化它:

var styles = StyleSheet.create({
  container: {
    borderWidth: 10,
    borderColor: 'purple',
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center'
  },

  ...
});

水平和垂直居中

现在我们可以看到容器的长度并没有覆盖整个屏幕。由于根View容器中的默认flexDirectioncolumn,内容将只覆盖内容的高度。幸运的是,我们现在知道如何填充剩余空间。给我们的容器添加flex 1将使其在垂直方向上覆盖屏幕长度,这给我们以下结果:

var styles = StyleSheet.create({
  container: {
    borderWidth: 10,
    borderColor: 'purple',
    flex: 1,
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center'
  },

  ...
});

水平和垂直居中

这就完成了我们对 Flexbox 布局的概述!有关支持的 Flexbox 属性的完整列表,请查看 React Native 文档facebook.github.io/react-native/docs/flexbox.html#content

绝对定位

此外,React Native 还提供了将屏幕上的项目定位的选项。这通过定义topleftrightbottom属性与浏览器中的方式相同。我们建议你在求助于绝对定位之前,尝试使用 Flexbox 创建你的布局。

设计 NavigationBar 组件

是时候给我们的NavigationBar添加 iOS 和 Android 的样式处理了。两者之间只有细微的差别,除了字体大小和填充的渲染方式。我们将首先为NavigationBar添加背景颜色和底部边框。将以下内容添加到index.ios.jsindex.android.js中的StyleSheet,并定义navbar样式:

var styles = StyleSheet.create({
    navContainer: {
      flex: 1
    },
    navBar: {
      backgroundColor: '#5B29C1',
      borderBottomColor: '#48209A',
      borderBottomWidth: 1
    }
});

接下来,使用样式属性更新Navigator.NavigatorBar

class ReactNotes extends React.Component {
  ...
  render () {
    return (
      <Navigator
        initialRoute={{name: 'home'}}
        renderScene={this.renderScene}
        navigationBar={
          <Navigator.NavigationBar
            routeMapper={NavigationBarRouteMapper}
            style={styles.navBar}
          />
        }
      />
    );
  }
}

最后要更新的是我们的navbar标题和SimpleButton样式。我们希望文本在垂直方向上居中,同时给屏幕的左右两侧按钮一些填充:

var styles = StyleSheet.create({
    navBar: {
      backgroundColor: '#5B29C1',
      borderBottomColor: '#48209A',
      borderBottomWidth: 1
    },
    navBarTitleText: {
      color: 'white',
      fontSize: 16,
      fontWeight: '500',
      marginVertical: 9  // iOS
   // marginVertical: 16 // Android
    },
    navBarLeftButton: {
      paddingLeft: 10
    },
    navBarRightButton: {
      paddingRight: 10
    },
    navBarButtonText: {
      color: '#EEE',
      fontSize: 16,
      marginVertical: 10 // iOS
   // marginVertical: 16 // Android
    }
});

注意

正如我们之前提到的,iOS 的marginVertical与 Android 版本不同,以产生相同的视觉效果。

最后,更新NavigationBarRouteMapper以包含标题和按钮的样式:

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'createNote':
        return (
          <SimpleButton
            onPress={() => navigator.pop()}
            customText='Back'
            style={styles.navBarLeftButton}
            textStyle={styles.navBarButtonText}
           />
        );
      default:
        return null;
    }
  },
  RightButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => {
              navigator.push({
                name: 'createNote'
              });
            }}
            customText='Create Note'
            style={styles.navBarRightButton}
            textStyle={styles.navBarButtonText}
          />
        );
      default:
         return null;
    }
  },

  Title: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <Text style={styles.navBarTitleText}>React Notes</Text>
        );
      case 'createNote':
        return (
          <Text style={styles.navBarTitleText}>Create Note</Text>
        );
    }
  }
};

设计 NavigationBar 组件

请注意,我们更改了 iOS 版本的状态栏文本颜色为白色。React Native 提供了一个 API 来与 iOS 中的状态栏交互。在我们的index.ios.js中,我们可以在ReactNotes构造函数中将它切换为白色:

class ReactNotes extends React.Component {
  constructor (props) {
    super(props);
    StatusBarIOS.setStyle('light-content');
  }
  ...
}

StatusBarIOS的文档可以在 React Native 文档中找到,链接为facebook.github.io/react-native/docs/statusbarios.html

更改 Android Material 主题

我们 Android 应用程序的状态栏和导航栏颜色看起来是纯黑色。目前,React Native 没有提供从 JavaScript 中样式化这些元素的支持系统,就像 iOS 上的StatusBarIOS API 所提供的那样。我们仍然可以使用 Android 5.0 及以上版本中可用的 Material Theme(位于ReactNotes/android/app/src/6main/res/values/styles.xml),来应用我们想要的颜色。将styles.xml的内容更改为以下内容:

<resources>
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:colorPrimaryDark">#48209A</item>
        <item name="android:navigationBarColor">#48209A</item>
    </style>
</resources>

colorPrimaryDark指的是状态栏的颜色,而navigationBarColor是底部导航容器的颜色。当你重新启动应用程序时,你应该能够看到状态栏和导航栏正确着色。

更改 Android Material 主题

想要了解更多关于使用 Material 主题的信息,请参考 Android 开发者文档中的developer.android.com/training/material/theme.html

设计 NoteScreen

我们的 NoteScreen 有两个没有任何样式的 TextInput。到目前为止,很难看到每个输入在屏幕上的位置。在 iOS 和 Android 上,通常会在每个输入下添加下划线。为了实现这一点,我们将 TextInput 包裹在 View 中,并对其应用 borderBottom

var styles = StyleSheet.create({
  ...

  inputContainer: {
    borderBottomColor: '#9E7CE3',
    borderBottomWidth: 1,
    flexDirection: 'row',
    marginBottom: 10
  }
});

Apply the inputContainer style to Views:
export default class NoteScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={styles.title}

            onEndEditing={(text) => {this.refs.body.focus()}}
          />
        </View>
        <View style={styles.inputContainer}>
          <TextInput
            ref="body"
            multiline={true}
            placeholder="Start typing"
            style={styles.body}

            textAlignVertical="top"
            underlineColorAndroid="transparent"
          />
        </View>
      </View>
    );
  }
}

现有的标题和正文样式定义了每个 TextInput 的高度。由于每个输入都将共享 flex 属性和文本大小,我们可以定义一个共享样式:

var styles = StyleSheet.create({
  ...
  textInput: {
    flex: 1,
    fontSize: 16,
  },

});

然后,在输入样式中,我们可以传递一个数组来包含这两种样式:

class NoteScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={[styles.textInput, styles.title]}
            onEndEditing={(text) => {this.refs.body.focus()}}
          />
        </View>
        <View style={styles.inputContainer}>
          <TextInput
            ref="body"
            multiline={true}
            placeholder="Start typing"
            style={[styles.textInput, styles.body]}
          />
        </View>
      </View>
    );
  }
}

对 NoteScreen 进行样式设计

在 Android 上这看起来还不正确。Android 上的 TextInput 有一个默认的下划线,并且它们在多行输入中垂直居中文本。有两个仅适用于 Android 的属性可以添加以匹配 iOS 应用程序的外观。在每个 TextInput 上设置 underlineColorAndroidtransparent,并在正文中将 textAlignVertical 设置为 TextInput

export default class NoteScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={[styles.textInput, styles.title]}
            onEndEditing={(text) => {this.refs.body.focus()}}
            underlineColorAndroid="transparent"
          />
        </View>
        <View style={styles.inputContainer}>
          <TextInput
            ref="body"
            multiline={true}
            placeholder="Start typing"
            style={[styles.textInput, styles.body]}
            textAlignVertical="top"
            underlineColorAndroid="transparent"
          />
        </View>
      </View>
    );
  }
}

对 NoteScreen 进行样式设计

使用这种方法,我们可以在两个设备上获得相同的外观!这完成了我们在上一章中创建的组件的样式设计。从现在开始,每当我们向我们的应用程序添加新组件时,我们就会立即进行样式设计。

摘要

React Native 中的样式与浏览器中 CSS 的工作方式非常相似。在本章中,你学习了如何创建和管理样式表并将它们添加到你的组件中。如果你在布局方面感到沮丧,请将 Flexbox 部分作为指南。确保回顾你的 main-axiscross-axis 的定义位置,以及 flex 项目沿着它们对齐的位置。在继续下一章之前,你可以自由地回到我们的组件中并尝试任何样式。

第五章。显示和保存数据

现在我们已经知道了如何为 React Native 应用程序设置样式,让我们来看看如何让它真正做些事情。在本章中,我们将开始将笔记保存到设备上,用我们保存的笔记填充列表,并从列表中选择笔记以查看和编辑。

在本章中,我们将涵盖以下主题:

  • 使用 ListView 显示数据行

  • 状态管理

  • 使用 props 将数据和回调传递到组件中

  • 使用 AsyncStorage 在 iOS 和 Android 设备上存储数据

本章的策略是首先使用虚拟数据构建基本功能,这样我们就可以在学习如何使用 AsyncStorage API 保存和加载数据之前学习一些基本技能。到本章结束时,您将拥有一个完全功能的笔记应用程序!

列表

我们应用程序的 HomeScreen 将显示我们已保存的笔记列表。为此,我们将引入 ListView 组件。让我们首先在我们的 Components 目录中创建一个名为 NoteList 的新文件,并添加以下代码:

import React, {
  StyleSheet,
  Text,
  View,
  ListView
  } from 'react-native';

export default class NoteList extends React.Component {

  constructor (props) {
    super(props);
    this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  }

  render() {
    return (
      <ListView
        dataSource={
          this.ds.cloneWithRows( [
              {title:"Note 1", body:"Body 1", id:1}, 
              {title:"Note 2", body:"Body 2", id:2}
            ])
        }
        renderRow={(rowData) => {
              return (
                 <Text>{rowData.title}</Text>
              )
            }
          }/>
      )
  }
}

ListView 组件使用起来相当简单。您必须提供两块信息,一个是提供所有行数据的 dataSource,另一个是 renderRow 函数,它是一个简单的函数,接受每一行的数据(一个单独的笔记)并返回一个 React 组件。在上面的示例中,这个函数返回一个 <Text/> 组件,用于显示笔记的标题。

我们实例化一个 ListViewDataSource 在构造函数中,因为我们只想创建一次。DataSource 构造函数接受一个 params 对象来配置 DataSource;然而,唯一必需的参数是一个 rowHasChanged 函数。当 DataSource 收到新数据时,该函数被 DataSource 使用,以便它可以有效地确定哪些行需要重新渲染。如果 r1r2 指向同一个对象,则行没有变化。

您还会注意到我们没有直接将 DataSource 引用传递给我们的 ListView。相反,我们使用 cloneWithRows(),将其传递给我们要使用的 rowData。目前我们正在硬编码行数据,但到本章结束时,您将知道如何使用新数据更新 ListView

接下来,让我们将 NoteList 组件添加到 HomeScreen 中,并学习如何响应对每一行的触摸事件。打开 HomeScreen 组件,并添加以下行以导入您的新 NoteList 组件:

import NoteList from './NoteList';

此外,让我们将 NoteList 组件放入 HomeScreenrender 方法中,在 View 组件内,在 <Text/> 组件之前:

render () {
    return (
      <View style={styles.container}>
        <NoteList/>
        <Text style={styles.noNotesText}>You haven't created any notes!</Text>

        <SimpleButton
          onPress={() => this.props.navigator.push({
            name: 'createNote'
          })}
          customText="Create Note"
          style={styles.simpleButton}
          textStyle={styles.simpleButtonText}
        />
      </View>
    );
  }

在我们尝试 NoteList 之前,让我们修改我们的样式以确保列表内容不会被 NavigationBar 遮挡:

  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 60
  }

现在,当您重新加载应用程序时,您应该看到以下截图:

列表

屏幕底部仍然显示着您还没有创建任何笔记的消息,但我们将学习如何在章节的后面处理这个问题。

现在我们有一个项目列表,我们希望当用户触摸其中一个项目时能够做出响应。为了做到这一点,我们将使用TouchableHighlight组件包裹我们的renderRow函数中的<Text/>组件。首先,让我们将TouchableHighlight添加到我们的导入列表中:

import React, {
  StyleSheet,
  Text,
  View,
  ListView,
  TouchableHighlight
  } from 'react-native';

然后更新我们的ListView中的renderRow函数:

renderRow={
  (rowData) => {
    return (
      <TouchableHighlight onPress={() => console.log(rowData)}>
         <Text>{rowData.title}</Text>
      </TouchableHighlight>
    )
}

现在,你可以重新加载应用程序并触摸每一行,以查看rowData已经被记录到控制台。

我们的目的是能够触摸一行,导航到NoteScreen,并使用该行的数据填充标题和正文。让我们给我们的NoteList组件添加一个_onPress事件处理程序,如下所示:

_onPress (rowData) {
  this.props.navigator.push(
    {
      name: 'createNote',
      note: {
        id: rowData.id,
        title: rowData.title,
        body: rowData.body
      }
    });
  }

我们将从这个TouchableHighlight函数中调用这个函数,如下所示:

                <TouchableHighlight onPress={() => this._onPress(rowData)}>
                  <Text>{rowData.title}</Text>
                </TouchableHighlight>

在我们尝试这个功能之前,看看_onPress处理程序,并注意我们正在引用this.props.navigator。这是我们用来在HomeScreenNoteScreen之间来回导航的导航器,但这个 props 业务是什么意思呢?

理解 props

如果你看看NoteList的构造函数,你会注意到它接受一个名为props的参数:

export default class NoteList extends React.Component {
  constructor (props) {
    super(props);
    this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  }

Props 是我们用来向 React 组件传递数据的机制。在我们的例子中,我们想从HomeScreen组件传递一个导航器引用到NoteList,所以让我们快速修改一下我们的NoteList声明,如下所示:

export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <NoteList navigator={this.props.navigator}/>
       ...
      </View>
    );
  }
}

当你在NoteList中触摸一行时,你将把与该行相关的笔记数据推送到导航器,然后触发renderScene,将笔记传递给NoteScreen。那么我们如何在NoteScreen中使用这个笔记呢?我们之前了解到 props 被传递到组件的构造函数中,但我们如何实际上让我们的TextInput组件显示笔记的标题和正文呢?让我们看看如果我们将每个输入的值属性绑定到传入的笔记会发生什么,如下所示:

        <View style={styles.inputContainer}>
          <TextInput
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={[styles.textInput, styles.title]}
            onEndEditing={(text) => {this.refs.body.focus()}}
            underlineColorAndroid="transparent"
            value={this.props.note.title}
          />
        </View>
        <View style={styles.inputContainer}>
          <TextInput
            ref="body"
            multiline={true}
            placeholder="Start typing"
            style={[styles.textInput, styles.body]}
            textAlignVertical="top"
            underlineColorAndroid="transparent"
            value={this.props.note.body}
          />
        </View>

现在我们重新加载应用程序并触摸列表中的第一个笔记时,我们将看到以下截图:

理解 props

但当你尝试编辑标题或正文时会发生什么?什么都没有发生!在我们诊断出问题之前,让我们点击返回按钮并触摸NoteList中的第二个笔记。你会看到它被显示出来,如下所示:

理解 props

好吧,所以我们的 NoteScreen 确实更新了,但只是在从外部传递新的 props 时,而不是在我们尝试编辑 TextInputs 时。Props 只能从组件外部传递。虽然这样做可能很有吸引力,但在每个 TextInput 的值改变时尝试在 NoteScreen 内部修改 this.props.note 是一个坏主意。我们需要的是一种方式来管理用户更改 TextInputs 时对 NoteScreen 内部状态的更改。为此,每个 React 组件都有一个叫做 状态 的东西。

使用状态

React 组件有一个内置的变量叫做 state,你可以用它来跟踪组件的 state。在上面的例子中,我们知道我们正在传递一个我们想要显示的笔记,因此组件的初始状态由那个笔记表示。让我们做一些完全疯狂的事情,修改 NoteScreen 构造函数,如下所示:

  constructor (props) {
    super(props)
    this.state = {note:this.props.note};
  }

因此,this.state 是一个具有标题和正文属性的初始设置为传递的笔记标题和正文的对象。为什么会有对 super (props) 的调用?我们的 NoteScreen 的超类是 React.Component,它将 props 作为参数并设置 this.props。如果我们省略 NoteScreen 中的 super(props),那么 this.props 将是未定义的。

你可能已经猜到了,我们将更新 TextInputs 以分别绑定到 this.state.titlethis.state.body,但我们还将监听每个输入的 onChangeText 事件:

        <View style={styles.inputContainer}>
          <TextInput
            ref="title"
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={[styles.textInput, styles.title]}
            onEndEditing={(text) => {this.refs.body.focus()}}
            underlineColorAndroid="transparent"
            value={this.state.note.title}
            onChangeText={(title) => {this.setState({title})}}
          />
        </View>
        <View style={styles.inputContainer}>
          <TextInput
            ref="body"
            multiline={true}
            placeholder="Start typing"
            style={[styles.textInput, styles.body]}
            textAlignVertical="top"
            underlineColorAndroid="transparent"
            value={this.state.body}
            onChangeText={(body) => {this.setState({body})}}
          />
        </View>

注意,我们用来处理 onChangeText 事件的箭头函数正在调用 this.setState(...) 而不是直接设置 this.state.title。这是一个重要的事情要记住。每次修改状态时,你必须使用 this.setState(),这样 React 就知道你的组件需要重新渲染。出于性能原因,调用 setState() 并不会立即更新 this.state,所以不要因此困惑!

重新加载应用程序,触摸列表中的 笔记 1,然后将标题更改为 我的笔记

使用状态

TextInput 属性现在在每次调用 render() 时都会反映 this.state.title 的值,这发生在每次调用 this.setState({title}) 之后。到目前为止一切顺利,但当你导航回 HomeScreen 时,你认为我们会看到什么?点击 返回 按钮看看——第一个笔记的标题仍然是 笔记 1 而不是 我的笔记。现在,当你点击 笔记 1 返回到 NoteScreen 时,你会看到你的更改已经消失了。让我们来修复这个问题!

我们刚刚确定了在笔记更改时需要更新我们的 ListView。我们知道当我们在 TextInput 组件中输入时,NoteScreen 的内部状态会发生变化,但我们如何将这些更改传达给应用程序的其他部分?

在 props 中传递回调

在 React 中,一个常见的模式是通过 props 将回调传递给组件。在我们的情况下,我们想要将一个回调传递给我们的 NoteScreen,以便它能够让我们知道笔记何时被更改。让我们回到我们的 index.ios.jsindex.android.js 文件中的 ReactNotes 组件,并更新我们的 renderScene 函数,如下所示:

  renderScene (route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen navigator={navigator} />
        );
      case 'createNote':
        return (
          <NoteScreen 
            note={route.note} 
            onChangeNote={(note) => console.log("note changed", note)}/>
        );
    }
  }

在这里,我们定义了一个名为 onChangeNote 的属性,并将其值设置为当我们在 NoteScreen 组件内部调用 onChangeNote 时将被调用的箭头函数。所以,在我们的 NoteScreen 代码内部,我们将添加以下行:

this.props.onChangeNote(note);

让我们回顾一下 NoteScreen 和一个用于更新笔记的函数:

class NoteScreen extends React.Component {
  …
  updateNote(title, body) {
    var note = Object.assign(this.state.note, {title:title, body:body});
    this.props.onChangeNote(note);
    this.setState(note);
  }
  …
}

在我们的标题 TextInput 中,更新 onChangeText 函数,如下所示:

onChangeText={(title) => this.updateNote(title, this.state.note.body)}

并且在 TextInput 的主体中:

onChangeText={(body) => this.updateNote(this.state.note.title, body)}

现在,让我们重新加载我们的应用程序,触摸笔记 1,并开始进行更改。如果你查看控制台,你应该会看到每次更改都被记录下来:

在 props 中传递回调

仅通过通知笔记的更改,我们只完成了更新 ListView 目标的一半。回想一下,我们的 NoteList 组件的 dataSource 目前只是一个硬编码的笔记数组:

  <ListView
        dataSource={
          this.ds.cloneWithRows([
            { title:"Note 1", body:"body", id:1}, 
            {title:"Note 2", body:"body", id:2}
          ])
        }
        renderRow={(rowData) => {
              return (
                <TouchableHighlight onPress={() => this._onPress(rowData)}>
                  <Text>{rowData.title}</Text>
                </TouchableHighlight>
              )
            }
          }
/>

我们需要能够将笔记列表传递给 NoteList 组件而不是硬编码它们。现在,由于你已经熟悉了 props,你知道我们可以从 HomeScreen 中传递列表,如下所示:

export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <NoteList 
          navigator={this.props.navigator} 
           notes={[{title:"Note 1", body:"body", id:1}, {title:"Note 2", body:"body", id:2}]}
       />
  …
}

然后修改 NoteList 组件以在 dataSource 中使用 this.props.notes

export default class NoteList extends React.Component {
...
  render() {
    return (
      <ListView
        dataSource={this.ds.cloneWithRows(this.props.notes)}
        ...
        />
      )
  }
}

让我们把重构再进一步。我们并不想让 HomeScreen 负责管理笔记列表的状态,这是一个顶级组件 ReactNotes 的工作。我们可以重复我们刚刚使用的技巧,并用 this.props.notes 替换 HomeScreen 中的硬编码的笔记数组:

export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <NoteList navigator={this.props.navigator} notes={this.props.notes}/>
      ...
     </View>
    );
  }
}

在我们的 ReactNotes 组件中,我们可以使用 props 将笔记传递给 HomeScreen

class ReactNotes extends React.Component {
  renderScene (route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen navigator={navigator} 
          notes={[{title:"Note 1", body:"body", id:1}, {title:"Note 2", body:"body", id:2}]}/>
        );
      case 'createNote':
        return (
          <NoteScreen note={route.note} onChangeNote={(note) => console.log("note changed", note)}/>
        );
    }
  }
  …
}

你可能会感觉到我们正越来越接近我们的目标,即能够修改笔记并在 ListView 中看到更改。我们笔记的来源现在与知道用户在 NoteScreen 上修改了笔记的事件处理程序非常接近。我们真正讨论的是管理我们应用程序的状态。

ReactNotes 组件是负责管理应用程序状态的最高级组件,该状态完全由笔记组成。所以,让我们正式一下,将笔记数组移动到组件的初始状态中:

class ReactNotes extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      notes: [{title: "Note 1", body: "body", id: 1}, {title: "Note 2", body: "body", id: 2}]};
  }
  renderScene(route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen navigator={navigator} notes={this.state.notes}/>
        );
      case 'createNote':
        return (
          <NoteScreen note={route.note} onChangeNote={(note) => console.log("note changed", note)}/>
        );
    }
  }

  ...
}

将笔记存储在数组中使得更新特定笔记变得有些棘手;让我们快速重构,使用对象而不是数组,如下所示:

class ReactNotes extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      selectedNote: {title:"", body:""},
      notes: {
        1: {title: "Note 1", body: "body", id: 1},
        2: {title: "Note 2", body: "body", id: 2}
      }
    }
  }
…
}

现在,notes 是一个对象,其中的键对应于笔记的 ids。由于 NoteList 组件仍然期望一个数组,让我们使用 underscore.js 来进行转换:

<HomeScreen navigator={navigator} notes={_(this.state.notes).toArray()} />

NoteList 应该继续以之前的方式工作;我们只是以不同的方式跟踪我们的笔记。

这里是我们需要为onChangeNote处理程序所做的更改,以便实际上通过状态更新笔记:

class ReactNotes extends React.Component {
  ...
  updateNote(note) {
    var newNotes = Object.assign({}, this.state.notes);
    newNotes[note.id] = note;
    this.setState({notes:newNotes});
  }

  renderScene(route, navigator) {
    switch (route.name) {
      case 'createNote':

        return (
          <NoteScreen note={this.state.selectedNote} onChangeNote={(note) => this.updateNote(note)}/>
        );
    }
  }
...
}

让我们逐步分析updateNote函数,以了解发生了什么。首先,我们使用Object.assign()创建this.state.notes的一个副本。每次你在状态对象中处理嵌套数据时,我们都建议创建这样的副本,以避免意外的行为。React 通过比较两个对象来确定组件的状态是否已更改,需要重新渲染;因此,使用这样的副本确保旧状态和新状态指向不同的对象。然后,我们使用note.id作为键将我们的修改后的笔记放入newNotes中。最后,我们调用setState()来用新的副本替换整个笔记对象。

在我们可以尝试我们的手艺之前,我们还有一些重构要做。现在我们知道如何通过 props 将回调传递给我们的组件,我们可以消除将导航器传递给HomeScreenNoteList组件的需要,而是传递一个回调,这样NoteList就可以告诉我们用户是否已选择了一个笔记:

class ReactNotes extends React.Component {
  renderScene(route, navigator) {
    switch (route.name) {
      case 'home':
        return (<HomeScreen navigator={navigator} notes={_(this.state.notes).toArray()} onSelectNote={(note) => navigator.push({name:"createNote", note: note})}/>);
      case 'createNote':
      return (
          <NoteScreen note={route.note} onChangeNote={(note) => this.updateNote(note)}/>
        );
    }
  }

这意味着我们必须更新我们的HomeScreen,将onSelectNote回调传递给NoteList

export default class HomeScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <NoteList notes={this.props.notes} onSelectNote={this.props.onSelectNote}/>
        <Text style={styles.noNotesText}>You haven't created any notes!</Text>
        <SimpleButton
          onPress={() => this.props.navigator.push({
            name: 'createNote'
          })}
          customText="Create Note"
          style={styles.simpleButton}
          textStyle={styles.simpleButtonText}
        />
      </View>
    );
  }
}

此外,我们还需要更新NoteList。我们不再需要_onPress处理程序或导航器的引用,我们只需使用rowData调用提供的回调即可:

export default class NoteList extends React.Component {

  constructor (props) {
    super(props);
    this.ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
  }
  render() {
    return (
      <ListView
        dataSource={this.ds.cloneWithRows(this.props.notes)}
        renderRow={(rowData) => {
              return (
            <TouchableHighlight
              onPress={() => this.props.onSelectNote(rowData)}
              style={styles.rowStyle}
              underlayColor="#9E7CE3"
            >
              <Text style={styles.rowText}>{rowData.title}</Text>
            </TouchableHighlight>              )
            }
          }/>
      )
  }
}

var styles = StyleSheet.create({
  rowStyle: {
    borderBottomColor: '#9E7CE3',
    borderBottomWidth: 1,
    padding: 20,
  },
  rowText: {
    fontWeight: '600'
  }
});

现在,你应该能够重新加载应用程序,触摸一个笔记,更改标题,返回,并看到更新的标题出现在NoteList中,如下面的截图所示:

通过 props 传递回调

当你选择一个笔记并导航到NoteScreen时,在NavigationBar中显示的标题仍然是创建笔记。让我们修改它,这样即使我们从列表中选择一个现有的笔记,我们也使用笔记的标题而不是创建笔记

  Title: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <Text style={styles.navBarTitleText}>React Notes</Text>
        );
      case 'createNote':
        return (
          <Text style={styles.navBarTitleText}>{route.note ? route.note.title : 'Create Note'}</Text>
        );
    }
  }

当你重新加载应用程序时,NoteScreen应该反映所选笔记的标题:

通过 props 传递回调

创建新笔记

到目前为止,我们一直在更新现有的笔记。我们如何添加新的笔记?实际上,这非常简单。我们只需要更新NavigationBar中的创建笔记按钮,如下所示:

   RightButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => {
              navigator.push({
                name: 'createNote',
                note: {
                  id: new Date().getTime(),
                  title: '',
                  body: ''
                }
              });
            }}
            customText='Create Note'
            style={styles.navBarRightButton}
            textStyle={styles.navBarButtonText}
          />
        );
      default:
         return null;
    }
  }

如你所见,我们现在传递了一个带有生成 id 的空笔记。 (生成 id 的一个更好的方法是将uuid生成器用于,但我们将把这个作为读者的练习!)

就这样!我们终于拥有了一个完整的、端到端的笔记应用!然而,我们的笔记只存在于内存中。我们需要能够将笔记保存到设备上,所以让我们认识我们的新朋友,AsyncStorage

使用 AsyncStorage

React Native 提供了一个本地存储机制的抽象,这样你就不必担心 iOS 和 Android 在设备上保存数据时的底层差异。

它的使用非常简单,所以让我们更新我们的ReactNotes组件以使用AsyncStorage。首先,让我们将AsyncStorage添加到我们的导入列表中:

import React, {
  AppRegistry,
  Navigator,
  StyleSheet,
  Text,
  AsyncStorage
} from 'react-native';

接下来,让我们添加一个saveNotes()函数:

  async saveNotes(notes) {
    try {
      await AsyncStorage.setItem("@ReactNotes:notes", JSON.stringify(notes));
    } catch (error) {
      console.log('AsyncStorage error: ' + error.message);
    }
  }

你可能想知道 JavaScript 中的asyncawait关键字在做什么!这些是 ES7 中的新关键字,它们简化了与 promises 的工作。AsyncStorage方法实际上是异步的,并且返回 promises。不深入细节,函数前面的async关键字允许我们在函数体内使用await关键字。await关键字将解析 promise,如果出现问题,它将抛出一个错误。

让我们修改我们的updateNote函数来调用我们新的saveNotes函数:

  updateNote(note) {
    var newNotes = Object.assign({}, this.state.notes);
    newNotes[note.id] = note;
    this.setState({notes:newNotes});
    this.saveNotes(newNotes);
  }

我们还需要一个函数来从AsyncStorageloadNotes

  async loadNotes() {
    try {
      var notes = await AsyncStorage.getItem("@ReactNotes:notes");
      if (notes !== null) {
        this.setState({notes:JSON.parse(notes)})
      }
    } catch (error) {
      console.log('AsyncStorage error: ' + error.message);
    }
  }

我们想在构造函数中从设备加载我们的已保存笔记:

  constructor(props) {
    super(props);
    this.state = {
      notes: {
        1: {title: "Note 1", body: "body", id: 1},
        2: {title: "Note 2", body: "body", id: 2}
      }
    }
    this.loadNotes();
  }

重新加载你的应用程序,并保存对笔记所做的更改或创建一个新的笔记。然后再次重新加载应用程序。你的更改已经保存!我们只剩下一个任务要做,那就是删除笔记!

删除笔记

在我们拥有一个完全功能的笔记应用之前,我们需要做的最后一件事是在我们的NoteScreen中添加一个删除按钮。为了实现这一点,我们将更新我们的NavigationBarRouteMapper,在路由名为createNote时添加一个RightButton

  RightButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => {
              navigator.push({
                name: 'createNote',
                note: {
                  id: new Date().getTime(),
                  title: '',
                  body: '',
                  isSaved: false
                }
              });
            }}
            customText='Create Note'
            style={styles.navBarRightButton}
            textStyle={styles.navBarButtonText}
          />
        );
      case 'createNote':
        if (route.note.isSaved) {
          return (
            <SimpleButton
              onPress={
                () => {
                  navigator.props.onDeleteNote(route.note);
                  navigator.pop();
                }
              }
              customText='Delete'
              style={styles.navBarRightButton}
              textStyle={styles.navBarButtonText}
              />
          );
        } else {
          return null;
        }
      default:
         return null;
    }
  },

首先要注意的是,我添加了一个条件来检查笔记是否已经被保存(我们需要调整我们的updateNote函数来设置这个)。这是为了确保删除按钮不会出现在新笔记上。当按下按钮时,Create NoteonPress处理程序已被更新,将isSaved设置为false,在传递给NoteScreen的空笔记中。

现在,让我们看看删除按钮的onPress处理程序:

              onPress={
                () => {
                  navigator.props.onDeleteNote(route.note);
                  navigator.pop();
                }
              }

我们之前见过navigator.pop(),但我们还调用了一个新的回调函数onDeleteNote。我们需要通过ReactNotesrender函数中的 props 传递这个回调函数:

  render () {
    return (
      <Navigator
        initialRoute={{name: 'home'}}
        renderScene={this.renderScene.bind(this)}
        navigationBar={
          <Navigator.NavigationBar
            routeMapper={NavigationBarRouteMapper}
            style={styles.navBar}
          />
        }
        onDeleteNote={(note) => this.deleteNote(note)}
      />
    );
  }

接下来,我们需要修改我们的updateNote函数来标记已保存的笔记:

  updateNote(note) {
    var newNotes = Object.assign({}, this.state.notes);
    note.isSaved = true;
    newNotes[note.id] = note;
    this.setState({notes:newNotes});
    this.saveNotes(newNotes);
  }

在那下面,我们将添加deleteNote函数:

  deleteNote(note) {
    var newNotes = Object.assign({}, this.state.notes);
    delete newNotes[note.id];
    this.setState({notes:newNotes});
    this.saveNotes(newNotes);
  }

就这样!重新加载应用程序并创建一个新的笔记。注意在NavigationBar中没有删除按钮。按下返回按钮查看列表中的笔记,然后点击列表中的该项来查看。你应该能在右上角看到删除按钮,如图所示:

删除笔记

按下删除按钮,你将被返回到HomeScreen,在那里被删除的笔记将从列表中消失!

概述

在本章中,我们已经创建了一个完整的笔记应用。你学习了如何使用ListView来显示数据,通过 props 将数据传递到组件中,跟踪组件的状态,以及使用 AsyncStorage 将数据保存到设备上。此外,你完成所有这些操作而没有编写任何平台特定的代码!

第六章:使用地理位置和地图

到目前为止,你已经看到 React Native 简化了原生 UI 组件的创建,例如列表、文本字段和按钮,并且它提供了简单的抽象,例如 AsyncStorage,以便与底层原生 API 一起工作。很快,你将看到你还可以访问更高级的组件,例如使用MapView组件的地图,以及你可以访问更高级的原生功能,例如使用 React Native 的地理位置 API 进行地理位置定位。我们将通过添加捕获和保存每个新笔记的当前 GPS 坐标的能力来展示这些功能。请注意,接下来的两个章节将专注于 iOS 开发,因为 Android 的功能集尚不完整。

在本章中,我们将涵盖以下主题:

  • 学习如何获取当前地理位置

  • 监听用户位置的变化

  • 确保我们的应用程序需要适当的权限

  • 将位置数据与每个笔记一起保存

  • MapView上显示所有笔记的原始位置

让我们开始吧!

介绍地理位置 API

React Native 在原生地理位置 API 上提供了一个易于使用的抽象。它遵循MDNMozilla 开发者网络)规范,该规范建议以下地理位置接口:

navigator.geolocation.getCurrentPosition(success, error, options)

此方法异步请求设备的当前位置,如果成功,将使用Position对象调用success回调,如果失败(通常是由于您的应用程序中权限配置不当或用户明确拒绝允许您的应用程序知道他们的位置),将调用error回调。options参数允许您请求更高的位置精度,定义您愿意等待响应的时间长度,并指定您愿意接受的缓存数据的最大年龄:

navigator.geolocation.watchPosition(success, error, options)

此功能允许您注册一个函数,每次位置变化时都会调用该函数。此函数返回一个整数,表示您注册的回调的id。这允许您通过调用以下代码来停止监听更新:

navigator.geolocation.clearWatch(id);

iOS 中的位置权限

在我们将地理位置集成到我们的笔记之前,我们需要配置一个权限来请求用户的位置。从 Xcode 中打开info.plist,确保NSLocationWhenInUseUsageDescription键位于文件中(默认情况下应该已启用):

iOS 中的位置权限

一旦应用程序启动,你应该会在屏幕中央自动弹出一个权限模态:

iOS 中的位置权限

使用地理位置标记笔记

让我们尝试地理位置,并在用户保存新笔记时开始捕获用户的位置。由于我们将在保存笔记时使用位置数据,我们将把我们的代码添加到index.ios.jsindex.android.js中的ReactNotes组件中。让我们首先添加一个名为trackLocation()的函数:

class ReactNotes extends React.Component {
  trackLocation() {
    navigator.geolocation.getCurrentPosition(
      (initialPosition) => this.setState({initialPosition}),
      (error) => alert(error.message)
    );
    this.watchID = navigator.geolocation.watchPosition((lastPosition) => {
      this.setState({lastPosition});
    });
  }

 …
}

在这里,我们调用getCurrentPosition并提供一个回调,该回调将使用设备返回的位置信息更新当前状态。如果出现问题,我们还提供了一个错误处理器。

接下来,我们使用watchPosition()来注册一个事件处理器,当用户的位置发生变化时将被调用。我们还保存从这次调用返回的watchId,这样我们就可以在组件卸载时停止监听。通常,在componentWillUnmount函数中清除您在构造函数或componentDidMount方法中最初设置的任何监听器是良好的实践:

class ReactNotes extends React.Component {
  componentWillUnmount() {
    navigator.geolocation.clearWatch(this.watchID);
  }
  trackLocation() {
    …
  }
 …
}

然后,我们将从构造函数中调用我们的trackLocation()函数,并添加一些带有位置数据的笔记到我们的初始状态中:

class ReactNotes extends React.Component {
  constructor (props) {
    super(props);
    StatusBarIOS.setStyle('light-content');

    this.state = {
      notes: {
        1: {
          title: "Note 1",
          body: "body",
          id: 1,
          location: {
            coords: {
              latitude: 33.987,
              longitude: -118.47
            }
          }
        },
        2: {
          title: "Note 2",
          body: "body",
          id: 2,
          location: {
            coords: {
              latitude: 33.986,
              longitude: -118.46
            }
          }
        }
      }
    };

    this.loadNotes();
    this.trackLocation();
  }

将位置数据与笔记一起保存需要对我们的updateNote()函数进行一些小的调整:

  updateNote(note) {
    var newNotes = Object.assign({}, this.state.notes);

    if (!note.isSaved) {
      note.location = this.state.lastPosition;
    }

    note.isSaved = true;
    newNotes[note.id] = note;
    this.setState({notes:newNotes});
    this.saveNotes(newNotes);
  }

这就是全部内容!重新加载应用,创建一个新的笔记,当笔记第一次保存时,GPS 坐标将被存储。但是,我们如何可视化与每个笔记关联的位置数据呢?让我们创建一个MapView来显示每个笔记的标记!

地理定位的完整文档可以在 React Native 文档中找到,网址为facebook.github.io/react-native/docs/geolocation.html

NoteLocationScreen

现在,由于我们在创建笔记时捕获了用户的位置,我们希望以有用的方式显示此信息。位置数据与在地图 UI 上显示笔记完美匹配。这样用户可以直观地看到他们创建的所有笔记。我们将创建一个新的组件NoteLocationScreen来存放我们的笔记位置,但在编写此屏幕的代码之前,让我们先添加导航。

在主页面上,我们希望在navbar中有一个地图按钮以过渡到NoteLocationScreen。更新NavigationBarRouteMapper中的LeftButtonTitle如下:

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => navigator.push({name: 'noteLocations'})}
            customText='Map'
            style={styles.navBarLeftButton}
            textStyle={styles.navBarButtonText}
           />
        );
      case 'createNote':
      case 'noteLocations':
        return (
          <SimpleButton
            onPress={() => navigator.pop()}
            customText='Back'
            style={styles.navBarLeftButton}
            textStyle={styles.navBarButtonText}
           />
        );
      default:
        return null;
    }
  },
  ...

  Title: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <Text style={styles.navBarTitleText}>React Notes</Text>
        );
      case 'createNote':
        return (
          <Text style={styles.navBarTitleText}>{route.note ? route.note.title : 'Create Note'}</Text>
        );
      case 'noteLocations':
        return (
          <Text style={styles.navBarTitleText}>Note Locations</Text>
        );
    }
  }
}

在这里,我们正在定义一个新的路由,称为noteLocations。请注意,我们还想在noteLocation路由上显示返回按钮,因此我们包括与createNote路由相同的案例。

如果您还没有这样做,请向App/Components/添加一个新的NoteLocationScreen.js文件,并将其导入到ReactNotes中。我们最后需要做的是将其包含在我们的renderScene函数中。我们将将其传递给NoteLocationScreen的笔记列表和相同的onSelectNote函数:

import NoteLocationScreen from './App/Components/NoteLocationScreen';

...

class ReactNotes extends React.Component {
  ...

  renderScene(route, navigator) {
    switch (route.name) {
      case 'home':
        return (
          <HomeScreen navigator={navigator} notes={_(this.state.notes).toArray()} onSelectNote={(note) => navigator.push({name:"createNote", note: note})} />
        );
      case 'createNote':
        return (
          <NoteScreen note={route.note} onChangeNote={(note) => this.updateNote(note)} />
        );
      case 'noteLocations':
        return (
          <NoteLocationScreen notes={this.state.notes} onSelectNote={(note) => navigator.push({name:"createNote", note: note})} />
        );
    }
  }

  ...

}

MapView

MapView 是 React Native 提供的另一个组件,用于显示每个平台对应的地图:iOS 上的 Apple Maps 和 Android 上的 Google Maps。您可以从将MapView添加到NoteLocationScreen开始:

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

export default class NoteLocationScreen extends React.Component {
  render () {
    return (
      <MapView
        showsUserLocation={true}
        style={styles.map}
      />
    );
  }
}

var styles = StyleSheet.create({
  map: {
    flex: 1,
    marginTop: 64
  }
});

MapView

Note

如果在 iOS 上地图没有显示您的位置,您可能需要在模拟器中启用位置。通过导航到调试 | 位置 | 自定义位置来设置自定义位置。

showsUserLocation函数将放大并显示用户在地图上的位置;默认情况下,此值是false。接下来,我们想要收集所有笔记的位置,以便使用注释在地图上显示它们。注释格式接受一个包含longitudelatitude、一些title信息以及on press属性的对象。我们将遍历通过 props 传入的笔记列表并提取位置数据。然后,将注释列表传递给 MapView 的annotations属性:

export default class NoteLocationScreen extends React.Component {
  render () {
    var locations = _.values(this.props.notes).map((note) => {
      return {
        latitude: note.location.coords.latitude,
        longitude: note.location.coords.longitude,
        title: note.title
      };
    });

    return (
      <MapView
        annotations={locations}
        showsUserLocation={true}
        style={styles.map}
      />
    );
  }
}

MapView

我们还可以通过向注释中添加一个按下时调用函数来添加查看笔记的功能。按下时调用方法将调用我们传入的onNoteSelect函数,并过渡到NoteScreen。在这里,我们添加一个左侧调用

export default class NoteLocationScreen extends React.Component {
  render () {
    var locations = _.values(this.props.notes).map((note) => {
      return {
        latitude: note.location.coords.latitude,
        longitude: note.location.coords.longitude,
        hasLeftCallout: true,
        onLeftCalloutPress: this.props.onSelectNote.bind(this, note),
        title: note.title
      };
    });

  ...

}

MapView

查阅 React Native 文档以获取有关MapView的更多详细信息,请访问facebook.github.io/react-native/docs/mapview.html

摘要

在本章中,我们探索了更多 React Native 的内置组件和模块,以捕获特定于设备的地理位置数据。地理位置 API 为我们提供了机制,该机制挂钩到现有的组件生命周期以跟踪用户位置。通过将其纳入我们现有的保存数据中,我们可以使用经纬度值来显示所有笔记记录的位置地图。

第七章。集成原生模块

到目前为止,你已经看到了 React Native 提供了大量开箱即用的功能。它通过 JavaScript 为你提供了一种简单的方法来使用各种原生功能,但有时你可能需要一些内置 React Native 组件尚未覆盖的功能。幸运的是,React Native 通过原生模块完全可扩展。多亏了一个非常活跃的社区,有一长串自定义组件正在填补这些空白。在本章中,我们将使用这些第三方原生模块之一来为我们的 React Notes 应用程序添加相机支持。

在本章中,我们将涵盖以下主题:

  • 使用 npm 安装自定义 React Native 相机模块

  • 添加 CameraScreen 和相机组件

  • 将捕获的图像保存到磁盘

  • NoteImageScreen 中显示捕获的图像

将图像添加到笔记

我们的笔记应用正在逐步完善,但一张图片胜过千言万语,所以如果我们能拍照并和笔记一起保存,那岂不是很好?由于 React Native 没有内置相机组件,我们需要使用由 Lochlan Wansbrough 创建的一个非常流行的组件。源代码可以在以下位置找到:github.com/lwansbrough/react-native-camera

到目前为止,你很可能已经熟悉了向我们的导航中添加新屏幕。在我们包含原生模块之前,让我们快速编写 CameraScreen 的导航代码。在 NavigationBarRouteMapper 中,将 camera 路由添加到 LeftButtonTitle 属性:

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <SimpleButton
            onPress={() => navigator.push({name: 'noteLocations'})}
            customText='Map'
            style={styles.navBarLeftButton}
            textStyle={styles.navBarButtonText}
           />
        );
      case 'createNote':
      case 'noteLocations':
      case 'camera':
        return (
          <SimpleButton
            onPress={() => navigator.pop()}
            customText='Back'
            style={styles.navBarLeftButton}
            textStyle={styles.navBarButtonText}
           />
        );
      default:
        return null;
    }
  },

  ...

  Title: function(route, navigator, index, navState) {
    switch (route.name) {
      case 'home':
        return (
          <Text style={styles.navBarTitleText}>React Notes</Text>
        );
      case 'createNote':
        return (
          <Text style={styles.navBarTitleText}>{route.note ? route.note.title : 'Create Note'}</Text>
        );
      case 'noteLocations':
        return (
          <Text style={styles.navBarTitleText}>Note Locations</Text>
        );
      case 'camera':
        return (
          <Text style={styles.navBarTitleText}>Take Picture</Text>
        );
    }
  }
};

然后,在 ReactNotes 组件中更新 renderScene 方法:

class ReactNotes extends React.Component {
  ...

  renderScene(route, navigator) {
    switch (route.name) {
      ...

      case 'createNote':
        return (
          <NoteScreen navigator={navigator} note={route.note} onChangeNote={(note) => this.updateNote(note)} showCameraButton={true} />
        );

      case 'camera':
        return (
          <CameraScreen />
        );
    }
  }

  ...

}

我们传递另一个名为 showCameraButton 的属性到 NoteScreen,我们将在稍后使用它来隐藏 Android 版本的相机按钮。

注意

与 Android 版本的 ReactNotes 中的 showCameraButton 属性相同,除了值为 false,应从 renderScene 方法传递给 Android 版本:showCameraButton={false}

在 iOS 上安装 react-native-camera

安装 react-native-camera 并将其包含在 CameraScreen 中有三个步骤。从命令行导航到 ReactNotes 目录,并运行以下命令:

npm install react-native-camera@0.3.8 --save

如果你查看 ReactNotes 项目的 node_modules 目录,你会看到一个名为 react-native-camera 的新目录,其中包含模块的 JavaScript 和原生源代码。在 ios 子目录中,你会注意到一个名为 RCTCamera.xcodeproj 的文件,如下面的截图所示:

在 iOS 上安装 react-native-camera

我们需要将此文件添加到我们的 Xcode 项目的库中。在 Xcode 项目导航器中,右键单击 并选择 将文件添加到 ReactNotes

在 iOS 上安装 react-native-camera

在出现的 Finder 窗口中,导航到ReactNotes | node_modules | react-native-camera | ios,选择RCTCamera.xcodeproj并点击Add

在 iOS 上安装 react-native-camera

查看项目导航器中的文件夹,您应该在列表中看到RCTCamera.xcodeproj

接下来,在项目导航器中选择ReactNotes,点击构建阶段并展开链接二进制库部分:

在 iOS 上安装 react-native-camera

点击链接二进制库部分底部的加号,从列表中选择libRCTCamera.a,然后点击Add

在 iOS 上安装 react-native-camera

我们现在可以开始在应用程序中使用相机组件了。

搜索原生模块

在我们开始使用相机组件之前,简要说明一下如何自己找到这些模块。寻找开源原生模块的两个最佳位置是 GitHub (github.com) 或 NPM (www.npmjs.com)。在这两个网站上的搜索将为您提供大量由 React Native 社区创建的第三方模块,您可以在项目中使用。

使用相机组件

困难的部分已经结束!导入相机模块就像包含任何其他 React 组件一样简单:

import Camera from 'react-native-camera';

使用相机组件相当简单。以下是CameraScreenrender函数:

  render () {
    return (
      <Camera
        captureTarget={Camera.constants.CaptureTarget.disk}
        ref="cam"
        style={styles.container}
      >
        <View style={styles.cameraButtonContainer}>
          <SimpleButton
            onPress={this._takePicture.bind(this)}
            customText="Capture"
            style={styles.cameraButton}
            textStyle={styles.cameraButtonText}
          />
        </View>
      </Camera>
    );
  }

相机模块公开了一些 props,您可以使用它们来自定义其行为,但大多数默认值对我们的目的来说都很适用。然而,您会注意到我们设置了captureTarget属性为Camera.constants.CaptureTarget.disk。此设置将保存的图像放置在设备上的一个目录中,只有我们的ReactNotes应用程序可以访问。captureTarget属性的默认值是Camera.constants.CaptureTarget.cameraRoll,这将图像放置在您拍照时原生相机使用的共享位置。虽然这通常是可以接受的,但在撰写本文时,存在一个阻止 ReactNative 从该位置加载图像的 bug。

看看上面的代码列表。注意我们已经向相机组件添加了子组件。它表现得就像一个View组件;你现在熟悉使用Flexbox属性来布局子组件。在我们的例子中,我们添加了一个View和一个带有onPress处理器的SimpleButton,该处理器将捕获图像:

_takePicture () {
  this.refs.cam.capture((err, data) => {
    if (err) return;
    this.props.onPicture(data);
  });
}

请记住,我们在相机组件声明中添加了ref="cam";因此,我们可以通过处理器来引用它。当我们调用capture()函数时,我们传递一个回调函数,该函数接受两个参数,err(除非用户不允许ReactNotes使用相机,否则应为 null)和数据,其中将包括图像保存到磁盘后的完整路径。

为了将图片路径与笔记一起保存,我们需要使用this.props.onPicture(data)将数据向上传递。我们需要更新我们的顶级ReactNotes组件,但在我们这样做之前,这里是CameraScreen的完整代码:

import React, {
  StyleSheet,
  Text,
  View
} from 'react-native';

import Camera from 'react-native-camera';
import SimpleButton from './SimpleButton';

export default class CameraScreen extends React.Component {
  _takePicture () {
    this.refs.cam.capture((err, data) => {
      if (err) return;
      this.props.onPicture(data);
    });
  }

  render () {
    return (
      <Camera
        captureTarget={Camera.constants.CaptureTarget.disk}
        ref="cam"
        style={styles.container}
      >
        <View style={styles.cameraButtonContainer}>
          <SimpleButton
            onPress={this._takePicture.bind(this)}
            customText="Capture"
            style={styles.cameraButton}
            textStyle={styles.cameraButtonText}
          />
        </View>
      </Camera>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 64
  },
  cameraButtonContainer: {
    position: 'absolute',
    bottom: 20,
    left: 20,
    right: 20
  },
  cameraButton: {
    backgroundColor: '#5B29C1',
    borderRadius: 4,
    paddingHorizontal: 20,
    paddingVertical: 15
  },
  cameraButtonText: {
    color: 'white',
    textAlign: 'center'
  }
});

返回到index.ios.js并在CameraScreen的 props 中添加onPicture回调:

renderScene(route, navigator) {
    switch (route.name) {
      case 'home':
        return (
      …
      case 'camera':
        return (
          <CameraScreen onPicture={(imagePath) => this.saveNoteImage(imagePath, route.note)}/>
        );    
   …
    }
  }
}

我们传递一个回调函数,该函数接受一个imagePath并调用this.saveNoteImage(imagePath, route.note)。让我们在renderScene上方添加该函数:

saveNoteImage(imagePath, note) {
  note.imagePath = imagePath;
  this.updateNote(note);
}

此函数只是接受imagePath,将其添加到笔记对象中,并将修改后的笔记传递给我们的updateNote()函数。

现在,你可以在模拟器中运行应用程序,点击拍照按钮,屏幕会变黑!别担心,你的代码没有问题;iOS 模拟器无法访问相机,因此显示一个黑色屏幕。然而,如果你点击捕获按钮,图片将被保存到你的文件系统中,当你返回查看图片时,你实际上会看到一个白色屏幕。

为了验证这行是否有效,你可以使用console.log输出imagePath,导航到图片,修改图片,然后返回到NoteImageScreen查看你的更改。

查看图片

使用图片时,重要的是它们被正确地保存到imagePath属性中,我们希望能够再次查看它们。我们将添加另一个名为NoteImageScreen的屏幕,用于显示相机组件捕获的图片。在App/Components/目录中创建NoteImageScreen.js文件。和之前一样,我们将将其包含在导航中,如下所示:

import NoteImageScreen from './App/Components/NoteImageScreen';

var NavigationBarRouteMapper = {
  LeftButton: function(route, navigator, index, navState) {
    switch (route.name) {

      ...

      case 'createNote':
      case 'noteLocations':
      case 'camera':
      case 'noteImage':
        ...
    }
  },

  ...

  Title: function(route, navigator, index, navState) {
    switch (route.name) {

      ...

      case 'noteImage':
        return (
          <Text style={styles.navBarTitleText}>{`Image: ${route.note.title}`}</Text>
        );
    }
  }
};

class ReactNotes extends React.Component {

  ...

  renderScene(route, navigator) {
    switch (route.name) {

      ...

      case 'noteImage':
        return (
          <NoteImageScreen note={route.note} />
        );
    }
  }

  ...

}

你可能会注意到,在noteImage路由的标题代码中,我们使用了另一个 ES6 特性,称为字符串插值。这允许我们在反引号`${variable}`之间直接格式化字符串,其中变量的值是route.note.title

图片组件

Image 组件由 React Native 提供,用于显示来自各种来源的图片,如本地磁盘或通过网络。要渲染我们的图片,我们只需将笔记中的imagePath传递给 source 属性。在ImageNoteScreen中添加:

import React, {
  Image,
  View,
  StyleSheet
} from 'react-native';

export default class NoteImageScreen extends React.Component {
  render () {
    return (
      <View style={styles.container}>
        <Image
          source={{uri: this.props.note.imagePath}}
          style={styles.image}
        />
      </View>
    );
  }
}

var styles = StyleSheet.create({
  container: {
    flex: 1,
    marginTop: 64
  },
  image: {
    flex: 1
  }
});

在这里,我们指定一个带有uri属性的对象来传递路径。你也可以使用互联网上的url以这种方式渲染图片:

source={{uri: https://example.com/example.png}}

要在本地要求图片,只需指定图片的路径:

source={require('./example.png')} 

关于 Image 组件的更多信息,请参阅 React Native 文档facebook.github.io/react-native/docs/image.html

删除图片

如果用户拍错了照片,我们需要一种方法来从笔记中删除图片。类似于NoteScreen的导航,我们将在右侧添加一个delete按钮。在ReactNotes组件中,我们将添加deleteNoteImage方法来从笔记中移除imagePath属性:

class ReactNotes extends React.Component {

  ...

  deleteNoteImage (note) {
    note.imagePath = null;
    this.updateNote(note);
  }

  saveNoteImage(imagePath, note) {
    note.imagePath = imagePath;
    this.updateNote(note);
  }

  ...

}

这看起来与我们的 saveNoteImage 函数类似,除了我们将值设置为 null。接下来,为了添加按钮,我们再次将 noteImage 属性添加到 NavigationBarRouteMapper 中的 RightButton 函数,并将 deleteNoteImage 函数传递给 Navigator 组件:

var NavigationBarRouteMapper = {

  ...

  RightButton: function(route, navigator, index, navState) {
    switch (route.name) {

      ...

      case 'noteImage':
        return (
            <SimpleButton
              onPress={() => {
                navigator.props.onDeleteNoteImage(route.note);
                navigator.pop();
              }}
              customText='Delete'
              style={styles.navBarRightButton}
              textStyle={styles.navBarButtonText}
            />
          );
      default:
         return null;
    }
  },

  ...

}

class ReactNotes extends React.Component {

  ...

   render () {
    return (
      <Navigator
        initialRoute={{name: 'home'}}
        renderScene={this.renderScene.bind(this)}
        navigationBar={
          <Navigator.NavigationBar
            routeMapper={NavigationBarRouteMapper}
            style={styles.navBar}
          />
        }
        onDeleteNote={(note) => this.deleteNote(note)}
        onDeleteNoteImage={(note) => this.deleteNoteImage(note)}
      />
    );
  }
}

连接最终部分

现在我们有了 CameraScreenImageScreen,我们需要能够通过 NoteScreen 导航到它们。我们将添加一个按钮,该按钮将根据笔记的 imagePath 更改状态。如果它不存在,则希望用户在它存在时过渡到 CameraScreenImageScreen。在视觉上,我们将按钮放置在标题输入旁边:

import SimpleButton = from './SimpleButton';

export default class NoteScreen extends React.Component {

  ...

  blurInputs () {
    this.refs.body.blur();
    this.refs.title.blur();
  }

  render () {
    var pictureButton = null;
    if (this.props.showCameraButton) {
      pictureButton = (this.state.note.imagePath) ? (
        <SimpleButton
          onPress={() => {
            this.blurInputs();
            this.props.navigator.push({
              name: 'noteImage',
              note: this.state.note
            });
          }}
          customText="View Picture"
          style={styles.takePictureButton}
          textStyle={styles.takePictureButtonText}
        />
      ) : (
        <SimpleButton
          onPress={() => {
            this.blurInputs();
            this.props.navigator.push({
              name: 'camera',
              note: this.state.note
            });
          }}
          customText="Take Picture"
          style={styles.takePictureButton}
          textStyle={styles.takePictureButtonText}
        />
      );
    }

    return (
      <View style={styles.container}>
        <View style={styles.inputContainer}>
          <TextInput
            ref="title"
            autoFocus={true}
            autoCapitalize="sentences"
            placeholder="Untitled"
            style={[styles.textInput, styles.title]}
            onEndEditing={(text) => {this.refs.body.focus()}}
            underlineColorAndroid="transparent"
            value={this.state.note.title}
            onChangeText={(title) => this.updateNote(title, this.state.note.body)}
          />

          {pictureButton}
        </View>
        ...

      </View>
    );
  }
}

注意,如果 showCameraButton 属性被启用,我们将根据 imagePath 的存在渲染不同的按钮,以向用户指示下一步。SimpleButtons 上的每个相应函数都将将相机或 noteImage 路由推送到导航器堆栈。

备注

blurInputs 是我们定义的一个函数,用于在切换到下一屏幕时禁用 TextInputs 的焦点并隐藏键盘。

按钮的样式与我们之前使用的类似。主要区别在于文本周围的填充:

var styles = StyleSheet.create({

  ...

  takePictureButton: {
    backgroundColor: '#5B29C1',
    borderColor: '#48209A',
    borderWidth: 1,
    borderRadius: 4,
    paddingHorizontal: 10,
    paddingVertical: 5,
    shadowColor: 'darkgrey',
    shadowOffset: {
        width: 1,
        height: 1
    },
    shadowOpacity: 0.8,
    shadowRadius: 1
  },
  takePictureButtonText: {
    color: 'white'
  }
});

由于我们之前定义的 inputContainer 样式具有 flexDirection 为 row,因此我们可以将按钮放置在与 TextInput 相同的行上,如下所示:

连接最终部分

摘要

在本章中,你了解到即使 React Native 缺少你需要的功能,你也能找到适合你需求的 Native Module。在我们的案例中,我们需要为我们的笔记应用提供相机支持,我们展示了如何通过 npm 安装一个出色的第三方模块。我们为我们的相机组件创建了一个新屏幕,并将其连接到我们的笔记保存机制以存储捕获的图像路径。然后我们创建了一个 NoteImage 屏幕来查看捕获的图像,并添加了一种删除我们捕获的图像的方法。

Facebook 以与 react-native-camera 完全相同的方式公开原生设备功能。如果你好奇,可以查看随 React Native 一起提供的非常简单的振动模块:github.com/facebook/react-native/tree/master/Libraries/Vibration。即使你不认为自己是一位 Objective-C、Swift 或 Java 程序员,也不要害怕尝试自己创建 Native Module——你可能会惊讶于它是多么简单!

第八章。发布应用程序

我们应用程序的第一个版本已完成,这意味着我们已准备好通过创建生产构建的过程。在本章中,我们将首先向您展示如何从静态 JavaScript 包生成和运行应用程序。然后,为了准备 App Store,我们将使用 Xcode 构建我们的 iOS 版本。最后,对于 Android,我们将介绍 React Native 提供的命令行工具和脚本来构建最终的 APK。

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

  • 为 iOS 生成静态包

  • 使用静态包代替react-native start

  • 在 Xcode 中构建发布版本

  • 签名和构建 Android 版本 APK

在 iOS 中生成静态包

到目前为止,我们一直在从由 Xcode 或终端使用react-native start启动的节点服务器上提供应用程序的静态包(其中包含所有我们的 JavaScript 代码)。在我们为 iOS 和 Android 创建发布版本之前,我们需要生成应用程序将加载的静态 JS 包。我们将首先在 iOS 中创建发布版本;对于 Android,请跳转到生成 Android APK部分。

再次使用react-native-cli并执行bundle命令。bundle命令需要三个标志:cplatformbundle-outputentry-file指定根组件的路径,平台是 iOS 或 Android,而bundle-output是放置生成的包的路径。

在根目录的终端中,运行react-native bundle,指定entry-fileindex.ios.js,平台为iOS,并将bundle-output路径指向ios/main.jsbundle

$ react-native bundle --entry-file index.ios.js --platform ios --bundle-output ios/main.jsbundle
bundle: Created ReactPackager
bundle: Closing client
bundle: start
bundle: finish
bundle: Writing bundle output to: ios/main.jsbundle
bundle: Done writing bundle output

资产目标文件夹未设置,跳过...

关于 iOS 中react-native bundle的更多详细信息,可以在 React Native 文档中找到,网址为facebook.github.io/react-native/docs/running-on-device-ios.html#using-offline-bundle

测试 iOS 中的静态包

首先,我们需要测试静态包是否可以在模拟器中由我们的 iOS 应用程序加载。在 Xcode 中打开AppDelegate.m,查看以下代码和注释:

   * Loading JavaScript code - uncomment the one you want.
   *
   * OPTION 1
   * Load from development server. Start the server from the repository root:
   *
   * $ npm start
   *
   * To run on device, change `localhost` to the IP address of your computer
   * (you can get this by typing `ifconfig` into the terminal and selecting the
   * `inet` value under `en0:`) and make sure your computer and iOS device are
   * on the same Wi-Fi network.
   */

  jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];

  /**
   * OPTION 2
   * Load from pre-bundled file on disk. To re-generate the static bundle
   * from the root of your project directory, run
   *
   * $ react-native bundle --minify
   *
   * see http://facebook.github.io/react-native/docs/runningondevice.html
   */

//   jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

这里概述了加载 JavaScript 包的多种方法。我们感兴趣的是选项 2,从磁盘加载预打包的文件。从选项 1中注释掉jsCodeLocation语句,并在选项 2中取消注释第二个:

// jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
...

jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];

确保没有正在运行的react-native start终端会话,然后从 Xcode 中构建并运行应用程序(Cmd + R)。你应该位于模拟器的顶部,以表明它正在从预打包的文件加载:

测试 iOS 中的静态包

在 Xcode 中创建 iOS 版本

为了提交到 AppStore,我们需要为分发构建我们的应用程序。幸运的是,我们最初使用 react-native init 创建的 Xcode 项目已经为我们预配置了一些内容。首先,我们想要将 构建配置 改为 禁用 功能,例如我们在调试时获得的开发者菜单。

让我们配置 iOS 发布:

  1. 在 Xcode 中,导航到 产品 | 方案 | 编辑方案… 并选择 运行,然后在 信息 选项卡下将 构建配置调试 更改为 发布在 Xcode 中创建 iOS 发布版本

  2. 将目标设置为 iOS 设备 而不是模拟器:在 Xcode 中创建 iOS 发布版本

  3. 最后,从 产品 | 存档 运行构建。组织者 窗口将打开一个包含您项目存档的列表。您可以通过从顶部菜单选择 窗口 | 组织者 来稍后返回此屏幕:在 Xcode 中创建 iOS 发布版本

  4. 在未来,当您创建多个发布版本时,您应该在 目标 | ReactNotes | 常规 中增加版本号。对于我们的第一个发布版本,这可以忽略:在 Xcode 中创建 iOS 发布版本

    一旦您的构建被存档,它就准备好提交到苹果应用商店。本书不涵盖应用商店的申请,但下一步将在苹果开发者网站上提供,网址为 developer.apple.com

生成 Android APK

构建 Android 应用程序包(APK)比 iOS 发布要复杂一些。在生成静态包之前,我们需要遵循一些步骤,就像我们在 iOS 中做的那样:

  1. 首先,我们需要生成一个密钥,我们可以使用 keytool 来签署我们的应用程序。在终端中导航到 android/app 文件夹并运行以下命令:

    $ keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
    [Storing my-release-key.keystore]
    
    

    注意

    注意这是一个私有文件,永远不应该与任何人共享。请将其保存在安全的地方!

  2. 接下来,我们需要更新几个配置文件。在 android/ 目录中向上一个级别打开 gradle.properties 并添加以下四行,将 YOUR_KEY_PASSWORD 替换为您为 keytool 使用的密码:

    MYAPP_RELEASE_STORE_FILE=my-release-key.keystore MYAPP_RELEASE_KEY_ALIAS=my-key-alias MYAPP_RELEASE_STORE_PASSWORD=YOUR_KEY_PASSWORD
    MYAPP_RELEASE_KEY_PASSWORD= YOUR_KEY_PASSWORD
    
  3. android/app/build.gradle 中添加以下内容:

    android {
        ...
    
        signingConfigs {
            release {
                storeFile file(MYAPP_RELEASE_STORE_FILE)
                storePassword MYAPP_RELEASE_STORE_PASSWORD
                keyAlias MYAPP_RELEASE_KEY_ALIAS
                keyPassword MYAPP_RELEASE_KEY_PASSWORD
            }
        }
        buildTypes {
            release {
                ...
                signingConfig signingConfigs.release
            }
        }
    }
    
  4. 现在,我们可以为 Android 生成静态包。创建一个新的目录 android/app/src/main/assets/ 并运行以下修改后的 react-native bundle 命令:

    react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
    
    

    这将产生以下输出:

    $ react-native bundle --platform android --dev false --entry-file index.android.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
    Building package...
    transforming [========================================] 100% 326/326
    Build complete
    Successfully saved bundle to android/app/src/main/assets/index.android.bundle
    
    
  5. 使用 gradle 命令在 android/ 目录下构建最终的 APK:

    ./gradlew assembleRelease
    
    

    如果您已正确设置密钥签名,您可以使用以下方式在模拟器或设备上测试您的发布版本:

    ./gradlew installRelease
    
    
  6. 有了这个,我们就有了最终的发布 APK(可以在android/app/build/outputs/apk/app-release.apk中找到)。查看 Android 开发者上的启动清单,了解更多关于 Play Store 提交流程的信息,请访问developer.android.com/distribute/tools/launch-checklist.html

摘要

在本章中,你学习了如何构建我们的应用程序的发布版本,以便提交到 App Store 或 Google Play Store。iOS 在 Xcode 中有一个预配置方案来禁用开发者功能。然后,我们通过针对 iOS 设备创建了一个存档。在 Android 上,我们使用keytool创建了一个私有发布密钥,并通过命令行和gradle构建了发布 APK。在提交之前,跟进并测试这两个发布构建是否都能正常工作,以降低被拒绝的可能性。

我们希望这本书能给你提供开始使用 React Native 创建移动应用所需的基础知识。尽管 React 和 React Native 在开发方面还非常早期,但你可以期待这本书中讨论的核心概念在未来一段时间内仍然适用。当 Android 最终达到与 iOS 的功能对等时,两个平台之间将开启更多快速发展的机会。祝你好运,我们迫不及待地想看到你的应用在 App 和 Google Play Stores 上发布!

posted @ 2025-09-07 09:18  绝不原创的飞龙  阅读(12)  评论(0)    收藏  举报