第 3 章 构建你的第一个应用

第 3 章 构建你的第一个应用

本章将会讲解如何搭建 React Native 开发环境以及如何构建一个简单的应用,并将其部署到自己的 iOS 或 Android 移动设备上。

3.1 搭建环境

搭建开发环境让你可以跟着本书的例子一起学习并开发你自己的应用。

关于安装 React Native 的说明可以查看 React Native 官方文档(http://facebook.github.io/react-native/)。官方网站会提供最新的安装参考,不过在此我们也会讲解这些步骤。

你将会用到 Homebrew(http://brew.sh/),一个 OS X 系统的通用包管理工具,用来安装 React Native 的相关依赖。本书假设你使用 OS X 操作系统,因此可以同时开发 iOS 和 Android 应用。

安装好 Homebrew 之后,运行以下命令:

brew install node
brew install watchman
brew install flow

React Native 包管理器同时使用了 nodewatchman, 如果在今后的开发过程中遇到问题,建议你更新这些依赖。flow 是 Facebook 公司出品的一个类型检查库,它同样被 React Native 所采用(如果你想让 React Native 项目支持类型检查,可以使用 flow)。

如果安装过程中遇到问题,你可能需要更新 brew 和相关依赖包(以下命令可能比较耗时)。

brew update
brew upgrade

如果出现错误,你需要修复本地的 brew 安装程序,brew doctor 可以帮助你找到问题所在。

3.1.1 安装React Native

现在你已经安装好了 node,然后就可以通过 npm(Node 包管理器)来安装 React Native 命令行工具了:

npm install -g react-native-cli

这个步骤将会在你的系统全局安装 React Native 命令行工具。完成之后,祝贺你,此时 React Native 已经安装成功了!

接下来,你需要处理特定平台的安装。为了开发特定平台的移动应用,你需要安装平台开发的依赖。本章将继续讲解相关内容,包括 iOS 和 Android 两个版本。

3.1.2 iOS依赖

为了开发和发布 iOS 应用,你需要获得一个 iOS 开发者账号。申请这个账号是免费的,足够用来开发使用了。如果需要部署到 iOS 应用商店,你需要获得一个许可,价格是每年 99 美元。

如果你还没有完成这一步的话,需要下载并安装 Xcode,它包含了 Xcode 集成开发环境、iOS 模拟器以及 iOS SDK(软件开发工具包)。你可以从应用商店或 Xcode 网站(https://developer.apple.com/xcode/download/)下载。

Xcode 成功安装之后,接受许可,一切就准备就绪了。

3.1.3 Android依赖

Android 依赖的安装需要较多的步骤,应查看官方文档(https://facebook.github.io/react-native/docs/android-setup.html)中最新的安装说明。需要注意的是,这些安装说明都假设你没有安装过 Android 开发环境。总体而言,安装分为三个主要阶段:安装 SDK、安装模拟器工具、创建模拟器。

首先,你需要安装 JDK(Java 开发工具包)和 Android SDK。

(1) 安装最新版本的 JDK(http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html)。

(2) 通过 brew install android-sdk 安装 Android SDK。

(3) 在 shell 配置文件中正确导出 ANDROID_HOME 环境变量(~/.bashrc、~/.zshrc 或其他 shell)。

export ANDROID_HOME=/usr/local/opt/android-sdk

许多 Android 相关的开发任务都使用这个环境变量。需要确保添加环境变量之后执行 source 命令使得配置可以立即生效。

接下来,在命令行执行 android 命令,从而打开 Android SDK 管理器。如图 3-1 所示,管理器将会显示出开发包的安装情况。

{%}

图 3-1:Android SDK 管理器允许你选择开发包进行安装

等待 SDK 管理更新并下载开发包列表。部分开发包会被默认选中,另外要确保选中了以下选项:

  • Android SDK Build-tools version 23.0.1

  • Android 6.0 (API 23)

  • Android Support Repository

然后,点击 Install Packages 并接受合适的许可。等待安装完成可能会花费一些时间。

接下来,你将要安装模拟器和相关的工具。

启动一个新的 shell,然后再次运行 android 来启动 Android SDK 管理器。我们将安装一些其他的包。

  • Intel x86 Atom System Image (for Android 5.1.1–API 22)

  • Intel x86 Emulator Accelerator (HAXM installer)

再次点击 Install Packages,接受合适的许可。

这些依赖包使我们能够创建 Android 虚拟设备(Android Virtual Devices,AVDs)或模拟器,但实际上我们还未创建任何模拟器。让我们来创建它,运行如下命令启动 AVD 管理器(如图 3-2 所示):

android avd

{%}

图 3-2:通过 AVD 管理器创建和运行模拟器

之后,点击 Create 按钮并填写创建模拟器的相关信息(如图 3-3 所示)。对于模拟器选项,记得勾选 Use Host GPU(如图 3-4 所示)。

{%}

图 3-3:创建任何你喜欢的模拟器(此处创建了一个 Galaxy Nexus 模拟器)

{%}

图 3-4:确保已经勾选了 Use Host GPU,否则模拟器会非常慢

如果愿意的话,你可以创建许多 AVD。由于 Android 设备种类繁多,有不同的屏幕尺寸、分辨率和功能,因此使用不同的模拟器通常能为测试带来帮助。当然,出于学习的目的,我们只需要安装一个即可。

3.2 创建一个新的应用

你可以使用 React Native 命令行工具来创建一个新的应用,它会为你生成一个包含 React Native、iOS 和 Android 的全新模板工程:

react-native init FirstProject

成功创建之后的项目结构如图 3-5 所示。

{%}

图 3-5:默认工程的文件结构

图中 ios/ 和 android/ 目录包含了平台相关的开发模板。你的 React 代码被放在 index.ios.js 和 index.android.js 文件中,它们分别是各自平台的入口文件。通过 npm 安装的依赖文件通常会被放在 node_modules/ 目录下。

如果需要,可以从 GitHub 仓库中下载本书的示例工程(https://github.com/bonniee/learning-react-native)。

3.2.1 在iOS平台运行React Native应用

作为初学者,我们将分别尝试在模拟器和物理设备上运行 iOS 版本的 React Native 应用。

使用 Xcode 打开 ios/ 目录下的 FirstProject.xcodeproj 文件。你会注意到左上方有一个“运行”按钮,如图 3-6 所示。点击“运行”按钮,程序将会在编译之后启动。你也可以选择不同的 iOS 模拟器作为部署目标。

{%}

图 3-6:“运行”按钮和部署目标的切换

点击“运行”按钮之后,React 包管理器将会自动运行在新的终端窗口中,如果运行失败或输出错误,请在 FirstProject 目录下重新运行 npm installnpm start 命令。

终端窗口如图 3-7 所示。

{%}

图 3-7:React 包管理器

包管理器就绪之后,iOS 模拟器将会运行默认的应用程序。不出意外的话,结果应如图 3-8 所示。

图 3-8:默认应用的截图

为了让代码能在模拟器上实时更新,需要保证包管理器一直处于运行状态。如果包管理器不幸崩溃退出,可切换到工程目录,然后运行 npm start 命令来重启它。

3.2.2 部署到iOS设备

为了将你的 React Native 应用上传至物理 iOS 设备中,你需要一个 Apple 开发者账号。然后,需要生成证书并注册你的设备。最后,打开 Xcode 偏好设置,添加你的账号即可(如图 3-9 所示)。

{%}

图 3-9:在 Xcode 偏好设置面板添加你的账户

接下来看如何为你的账号生成证书。最简单的办法就是在 Xcode 中打开通用面板(General),如图 3-10 所示,你会看到一个警告的符号。点击 Fix Issue(修复问题)按钮来解除警告。为了从 Apple 公司获取证书,Xcode 将会一步步引导你。

{%}

图 3-10:Xcode 通用面板

成功获取证书之后,工作基本就完成了。最后一步是登录到 Apple 开发者中心(http://developer.apple.com),然后注册你的设备(如图 3-11 所示)。

{%}

图 3-11:在 iOS 开发者中心注册你的 iOS 设备

设备的 UDID 很容易获取。将你的设备连接到电脑上,打开 iTunes,选择你的设备,然后单击序列号,就可以看到 UDID 显示出来并且复制到剪贴板中了。

一旦将你的设备注册到 Apple 公司之后,构建列表中就会出现许可的设备了。

如果你只想发布到测试设备上,那么这个注册过程也可以在今后进行。另外,Apple 公司每年会通过开发者计划为独立开发者分配 100 个设备用于测试。

最后,我们在部署之前需要对代码作一些改动。你需要在 AppDelegate.m 文件中将 localhost 改成你的 Mac 的 IP 地址。假如你不知道如何查看自己电脑的 IP 地址,可以在终端运行 ifconfig,在 en0 下的 inet 即为 IP 地址。

例如,你的 IP 地址为 10.10.12.345,那么应该把 jsCodeLocation 修改为:

jsCodeLocation =
[NSURL URLWithString:@"http://10.10.12.345:8081/index.ios.bundle"];

呀!一路的配置终于完成了,现在我们可以在 Xcode 左上方选择部署的物理设备了(图 3-12)。

{%}

图 3-12:选择你的 iOS 设备作为部署平台

选好之后,单击“运行”按钮,应用程序就安装到你的设备中了,就像在模拟器上一样。你会发现关闭应用程序之后,它已经被安装在主菜单中了。

3.2.3 在Android平台运行React Native应用

为了在 Android 平台运行 React Native 应用,需要做两件事情:首先打开模拟器,然后运行程序。

之前我们介绍过了运行 AVD 管理器的方法(如图 3-2 所示):

android avd

选择希望运行的模拟器版本,然后点击 Start... 按钮。

另外,也可以通过命令行来运行模拟器。通过以下命令显示出所有可用的模拟器类型:

emulator -list-avds

然后通过名字和 @ 前缀来运行它们,例如,我有一个名为 galaxy 的模拟器,我可以这样来运行它:

emulator @galaxy

无论采用何种方式来启动模拟器,一旦启动成功,只需要在工程的根目录运行如下命令即可加载 React Native 应用:

react-native run-android

3.2.4 小结:创建并运行项目

以上文字涉及很多知识,因为我们需要为 React Native 安装 iOS 或 Android 设备的各种依赖,看上去比较麻烦。

好消息是,现在你已经完成了初始阶段的这些琐碎的工作,接下来就会变得更轻松。在 React Native 中创建“Hello World”,只需要在命令行运行 react-native init HelloWorld 即可。

3.3 探索示例代码

我们已经部署并运行了默认的应用程序,接下来看看它是如何工作的。在这一节中,我们将深入到默认应用的源代码中去探索 React Native 项目的结构。

3.3.1 添加组件到视图中

当 React Native 应用启动之后,React 组件是如何被添加到视图中的呢?是什么决定了组件的渲染情况?

问题的答案取决于平台。我们先来看项目的 iOS 版本。

我们可以在 AppDelegate.m 文件中找到答案。例 3-1 尤其要注意。

例 3-1:在 ios/AppDelegate.m 中声明根视图

RCTRootView *rootView =
  [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
  moduleName:@"FirstProject"
  launchOptions:launchOptions];

React Native 库将其所有的类名使用 RCT 作为前缀,也就是说 RCTRootView 就是一个 React Native 类。所以,RCTRootView 代表 React Native 的根视图。AppDelegate.m 中其他的代码则将视图添加到 UIViewController 中并渲染到屏幕上。这个步骤与使用 React.render 方法挂载 React 组件到 DOM 节点上有着异曲同工之妙。

眼下,AppDelegate.m 文件有两处应该修改。

第一处需要修改的地方是 jsCodeLocation 这一行,之前为了把应用部署到物理设备上,我们修改过此处。正如代码中的注释所示,第一种方式作为开发使用,第二种方式用来将预打包文件部署到硬盘上。现在我们采用第一种方式,今后一旦需要部署到应用商店,我们会更加详细地讨论这两种方式。

另一处需要修改的地方是 moduleName,它被传递给 RCTRootView 以决定哪个组件将被挂载到视图中。这里你可以指定哪些组件需要被程序渲染。

为了使用 FirstProject 组件, 你需要在 React 中注册一个相同名字的组件。如果打开 index.ios.js,你会看到代码的最后一行已经完成了这项工作(例 3-2)。

例 3-2:注册顶层组件

AppRegistry.registerComponent('FirstProject', () => FirstProject);

以上代码暴露了 FirstProject 组件,使得我们能够在 AppDelegate.m 文件中使用它。大多数情况下,你都不需要去修改这个模板代码,但是我们应该对此有一些了解。

那么,Android 平台是怎样的呢?原理也很类似。如果你查看 MainActivity.java 文件,会注意到这一行代码(例 3-3)。

例 3-3:MainActivity.java 中 Android 的 React 入口

mReactRootView.startReactApplication(mReactInstanceManager, "FirstProject", null);

正如 AppDelegate.m 之于 iOS,Android 的 MainActivity.java 也会查看 AppRegistry 中绑定到 FirstProject 的 React 组件。

3.3.2 React Native中的模块导入

让我们进一步观察 index.ios.js 文件。如例 3-4 所示,require 语句跟通常的用法有一些区别。

例 3-4:React Native 中的 require 语句,导入 UI 元素

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

上面的语法挺有趣的。我们通过 require 语句导入了 React,但是下一行代码发生了什么呢?

React Native 的使用方面有一点比较奇特,那就是你要导入所需的每一个组件或模块。诸如 <div> 之类的标签是不存在的,如果你需要使用 <View><Text> 等组件,就要逐一导入。像 StylesheetAppRegistry 这样的库函数也需要使用以上语法进行导入。一旦开始开发自己的应用,我们将会探索 React Native 提供的其他库函数,同样也是需要导入才能使用。

如果你不熟悉这些语法,可以在附录 A 中查看例 A-2,它解释了 ES6 的解构特性。

3.3.3 FirstProject组件

让我们看看 <FirstProject> 组件(例 3-5),代码存在于 index.ios.js 和 index.android.js 文件中(它们是相同的)。

这些代码看起来亲切而熟悉,因为 <FirstProject> 仅仅是一个普通的 React 组件,主要的不同在于用 <Text><View> 代替了 <div><span>,并且用对象来表示样式。

例 3-5:包含样式的 FirstProject 组件

var FirstProject = 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,
  },
});

正如之前所提到的,React Native 中所有的样式都采用样式对象来代替传统的样式表,标准的做法就是利用 StyleSheet 库进行样式的编写。你可以在文件的底部看到样式对象是如何定义的。需要注意的是,<Text> 组件可以使用文本特有的属性,如 fontSize,所有的布局样式都使用 flexbox。我们将在第 5 章用更多篇幅介绍布局的内容。

该示例应用很好地介绍了创建 React Native 应用的一些基本函数。它挂载了 React 组件用于渲染,并介绍了 React Native 的基本样式和渲染逻辑,同时也可以检验我们的开发环境是否被正确安装,我们还尝试将应用部署到真实设备上。但它依然是一个极其基础的缺乏用户交互的应用,接下来让我们尝试开发一个有更多功能的应用吧。

3.4 开发天气应用

我们将使用示例程序来开发天气应用(你也可以通过 react-native init WeatherProject 创建一个新的示例工程)。这个项目包括如何利用和结合样式表、flexbox、网络通信、用户输入和图像显示等知识来开发一个实用的应用程序,然后将其部署到 Android 或 iOS 设备上。

这部分内容可能会有些含糊不清,因为我们主要把精力集中在这些特性的用法上,而不是深入分析它们。不用担心进度太快,在后续的章节中,我们会将这个天气应用作为参考并深入讨论这些特性。

天气应用最终的界面如图 3-13 所示,用户可以通过文本框输入邮编进行查询。该应用利用 OpenWeatherMap 的接口获取数据并展现当前天气情况。

图 3-13:天气应用成品

我们要做的第一件事就是替换默认的代码。将初始组件的代码移动到 WeatherProject.js 中,并修改 index.ios.js 和 index.android.js 文件的内容(例 3-6)。

例 3-6:精简后的 index.ios.js 和 index.android.js 代码(二者保持一致)

var React = require('react-native');
var { AppRegistry } = React;
var WeatherProject = require('./WeatherProject');
AppRegistry.registerComponent('WeatherProject', () => WeatherProject);

3.4.1 处理用户输入

我们希望用户通过输入邮编获取该地区的天气预报,因此需要添加一个输入框提供给用户。首先,添加默认邮编信息至组件的初始状态(state)中(例 3-7)。

例 3-7:在 render 函数前加入这段代码

getInitialState: function() {
  return {
    zip: ''
  };
}

记 住,getInitialState 可以让我们创建 React 组件的初始状态(state)。如果你需要复习 React 组件的生命周期,可以查看 React 文档(https://facebook.github.io/react/docs/component-specs.html)。

接着,修改其中一个 <Text> 组件的内容为 this.state.zip

<Text style={styles.welcome}>
  You input {this.state.zip}.
</Text>

我们用同样的方式添加一个 <TextInput> 组件(这是一个允许用户输入文本的基础组件)。

<TextInput
  style={styles.input}
  onSubmitEditing={this._handleTextChange}/>

这个 <TextInput> 组件的文档和属性可以在 React Native 官网查看(http://facebook.github.io/react-native/docs/textinput.html)。为了监听一些事件,你可以往 <TextInput> 中传入回调函数,如 onChangeonFocus,但现在暂时不需要这么做。

注意,我们已经为其添加了样式,input 样式表如下:

var styles = StyleSheet.create({
  ...
  input: {
    fontSize: 20,
    borderWidth: 2,
    height: 40
    }
  ...
});

<TextInput> 组件的属性(prop)中监听了 onSubmitEditing 事件,事件回调需要作为组件中的一个函数:

_handleTextChange(event) {
 console.log(event.nativeEvent.text);
 this.setState({zip: event.nativeEvent.text})
}

图中 console 语句是外部的,如果需要的话,可以在调试工具里使用该语句进行调试。

同时需要更新导入语句:

var React = require('react-native');
var {
  ...
  TextInput
  ...
} = React;

现在,尝试使用 iOS 模拟器运行你的应用。它可能不是很美观,但你应该可以成功地输入一个邮编并显示在 <Text> 组件上。

如果需要的话,也可以对用户的输入做一个验证来确保正确输入了五位数字,现在暂时略过。

例 3-8 展示了 WeatherProject.js 组件的完整代码。

例 3-8:WeatherProject.js:这个版本简单地接收并记录用户的输入

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

var WeatherProject = React.createClass({
  // 如果你需要一个默认的邮编, 你可以在这里添加一个。
  getInitialState() {
    return ({
      zip: ''
    });
  },
  // 我们将添加这个回调函数到<TextInput>属性中。
  _handleTextChange(event) {

    // log语句输出结果在Xcode或Chrome调试工具中可见。
    console.log(event.nativeEvent.text);

    this.setState({
      zip: event.nativeEvent.text
    });
  },
  render() {
    return (
      <View style={styles.container}>
       <Text style={styles.welcome}>
         You input {this.state.zip}.
       </Text>
       <TextInput
             style={styles.input}
             onSubmitEditing={this._handleTextChange}/>
     </View>
   );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  input: {
    fontSize: 20,
    borderWidth: 2,
    height: 40
    }
});

module.exports = WeatherProject;

3.4.2 展现数据

现在,我们来开发根据邮编查询天气预报的功能。首先添加一些 mock 数据(虚拟的数据)到 WeatherProject.js 文件的 getInitialState 方法中。

getInitialState() {
  return {
    zip: '',
    forecast: {
      main: 'Clouds',
      description: 'few clouds',
      temp: 45.7
    }
  }
}

为了让程序更加清晰,我们把天气预报独立成一个单独的组件,新建一个名为 Forecast.js 的文件(例 3-9)。

例 3-9:Forecast.js 中的 Forecast 组件

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

var Forecast = React.createClass({
  render: function() {
    return (
      <View>
        <Text style={styles.bigText}>
          {this.props.main}
        </Text>
        <Text style={styles.mainText}>
          Current conditions: {this.props.description}
        </Text>
        <Text style={styles.bigText}>
          {this.props.temp}°F
        </Text>
      </View>
    );
  }
});


var styles = StyleSheet.create({
  bigText: {
    flex: 2,
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
    color: '#FFFFFF'
  },
  mainText: {
    flex: 1,
    fontSize: 16,
    textAlign: 'center',
    color: '#FFFFFF'
  }
})

module.exports = Forecast;

<Forecast> 组件只能基于它的属性渲染一些 <Text> 文本,我们也会在文件的末尾添加一些简单的文本颜色之类的样式。

导入 <Forecast> 组件并添加到 render 方法中,通过 this.state.forecast 向它的属性传入数据(例 3-10)。我们稍后会解决布局和样式问题。你能在图 3-14 看到 <Forecast> 组件最终显示的效果。

例 3-10:WeatherProject.js 中应该加入新的 state 和 Forecast 组件

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

var Forecast = require('./Forecast');

var WeatherProject = React.createClass({
  getInitialState() {
    return {
      zip: '',
      forecast: {
        main: 'Clouds',
        description: 'few clouds',
        temp: 45.7
      }
    }
  },
  _handleTextChange(event) {
    console.log(event.nativeEvent.text);
    this.setState({
      zip: event.nativeEvent.text
    });
  },
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          You input {this.state.zip}.
        </Text>
        <Forecast
          main={this.state.forecast.main}
          description={this.state.forecast.description}
          temp={this.state.forecast.temp}/>
        <TextInput
          style={styles.input}
          returnKeyType='go'
          onSubmitEditing={this._handleTextChange}/>
      </View>
    );
  }
});

var styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#4D4D4D',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  input: {
    fontSize: 20,
    borderWidth: 2,
    height: 40
    }
});

module.exports = WeatherProject;

图 3-14:目前的天气应用

3.4.3 添加背景图片

单纯的背景颜色略显单调。我们接下来为其添加一个背景图片。1

1React Native 0.14 版本之后,提供了图片统一的管理方式,详见 http://facebook.github.io/react-native/docs/images.html。以下篇幅介绍的是 React Native 0.14 之前版本的做法,如使用最新版本可略过该部分。——译者注

资源导入的方法是平台特定的

Android 和 iOS 添加资源的方法是不同的。这里我们讲解两种方法。

像图片这样的资源需要根据平台分别导入到你的项目工程中。我们先看看 Xcode 如何处理。

选择 Images.xcassets/ 目录,然后选择 New Image Set 选项,如图 3-15 所示。然后,你可以拖放一张图片到这个图片集中,图 3-16 展示了最终的效果。要确保图片集的名字与文件名保持一致,否则 React Native 可能无法导入。

{%}

图 3-15:新建一个图片集

{%}

图 3-16:拖放图片至图片集中

@2x 和 @3x 修饰符分别表明图片显示在基准分辨率两倍或三倍的屏幕中。由于我们的天气应用是一个通用的软件(意味着一个程序可以同时运行在 iPhone 或 iPad 中),Xcode 赋予了我们针对不同分辨率添加不同图片的能力。

对于 Android 系统,我们需要将图片文件作为可绘制位图资源(bitmap drawable resources)(http://developer.android.com/guide/topics/resources/drawable-resource.html#Bitmap) 添加到目录 WeatherProject/android/app/src/main/res 中。你需要将 .png 图片添加到下列特定分辨率的目录中(如图 3-17 所示):

  • drawable-mdpi/ (1x)

  • drawable-hdpi/ (1.5x)

  • drawable-xhdpi/ (2x)

  • drawable-xxhdpi/ (3x)

{%}

图 3-17:添加图片文件到 Android 中

之后,图片就可以在 Android 应用中使用了。

这个工作流程可能让你感觉不舒服,这就是它的现状。不过它很可能在之后版本的 React Native 中进行改进。

既然文件已经被导入到 Android 和 iOS 项目中,让我们回到 React 代码吧。添加一背景图片时,我们不能像在 Web 环境一样为 <div> 标签设置属性,而是将 <Image> 组件作为容器使用:

<Image source={require('image!flowers')}
       resizeMode='cover'
       style={styles.backdrop}>
  // 这里放置内容。
</Image>

<Image> 组件期望一个图片源 prop,我们通过 require 进行引入。 require(image!flowers) 语句将会触发 React Native 查询名为 flowers 的文件。

别忘了为其样式添加 flexDirection 属性,使得它的子元素渲染成我们期望的样子:

backdrop: {
  flex: 1,
  flexDirection: 'column'
}

现在为 <Image> 添加一些子元素。更新 <Weather Project> 组件的 render 方法为:

<Image source={require('image!flowers')}
       resizeMode='cover'
         style={styles.backdrop}>
    <View style={styles.overlay}>
      <View style={styles.row}>
        <Text style={styles.mainText}>
          Current weather for
        </Text>
        <View style={styles.zipContainer}>
          <TextInput
            style={[styles.zipCode, styles.mainText]}
            returnKeyType='go'
            onSubmitEditing={this._handleTextChange}/>
        </View>
      </View>
      <Forecast
         main={this.state.forecast.main}
         description={this.state.forecast.description}
         temp={this.state.forecast.temp}/>
    </View>
   </Image>

你会发现我使用了一些还没有讨论过的样式,例如:rowoverlayzipContainerzipCode 等样式。你可以跳到这部分的末尾去查看完整的样式表。

3.4.4 从Web获取数据

下一步,我们将探索 React Native 中网络接口的用法。你不能在移动设备中使用 jQuery 发送 AJAX 请求。然而,React Native 实现了 Fetch 接口。基于 Promise 的语法非常简洁:

fetch('http://www.somesite.com')
  .then((response) => response.text())
  .then((responseText) => {
    console.log(responseText);
  });

我们将会使用 OpenWeatherMap 的接口,它提供给我们一个可以根据邮编返回当前天气情况的简单的端点。

为了集成这个接口,我们可以修改 <TextInput> 组件的回调函数,从而使用 OpenWeatherMap 的接口进行查询。

_handleTextChange: function(event) {
  var zip = event.nativeEvent.text;
  this.setState({zip: zip});
  fetch('http://api.openweathermap.org/data/2.5/weather?q=' +
  zip + '&units=imperial')
    .then((response) => response.json())
    .then((responseJSON) => {
      // 如果你愿意,可以看看这个格式。
      console.log(responseJSON);
      this.setState({
        forecast: {
          main: responseJSON.weather[0].main,
          description: responseJSON.weather[0].description,
          temp: responseJSON.main.temp
        }
      });
    })
    .catch((error) => {
      console.warn(error);
    });
}

注意,我们需要从返回结果中获取 JSON。使用 Fetch 接口是非常直观的,以上就是我们需要做的全部工作了。

另一件我们要做的事是移除占位的数据,确保在没有数据的时候,天气预报组件不会被渲染。

首先,从 getInitialState 中清除 mock 数据:

getInitialState: function() {
  return {
    zip: '',
    forecast: null
  };
}

然后,在 render 函数中更新渲染逻辑:

var content = null;
if (this.state.forecast !== null) {
  content = <Forecast
              main={this.state.forecast.main}
              description={this.state.forecast.description}
              temp={this.state.forecast.temp}/>;
}

最后,在 render 函数中使用 {content} 替换 <Forecast> 组件。

3.4.5 整合

对于这个应用的最后一个版本,我重新组织了 <WeatherProject> 组件的 render 函数并且调整了样式。最大的改变是布局逻辑,如图 3-18 所示。

{%}

图 3-18:天气应用最终的布局

好,准备好查看整体代码了吗?例 3-11 展示了完成之后的 <WeatherProject> 组件包括样式表在内的完整代码。<Forecast> 组件仍然与例 3-9 一致。

例 3-11:WeatherProject.js 完整代码

var React = require('react-native');
var {
  StyleSheet,
  Text,
  View,
  TextInput,
  Image
} = React;
var Forecast = require('./Forecast');

var WeatherProject = React.createClass({
  getInitialState: function() {
    return {
      zip: '',
      forecast: null
    };
  },

  _handleTextChange: function(event) {
    var zip = event.nativeEvent.text;
    this.setState({zip: zip});
    fetch('http://api.openweathermap.org/data/2.5/weather?q='
      + zip + '&units=imperial')
      .then((response) => response.json())
      .then((responseJSON) => {
        this.setState({
          forecast: {
            main: responseJSON.weather[0].main,
            description: responseJSON.weather[0].description,
            temp: responseJSON.main.temp
          }
        });
      })
      .catch((error) => {
        console.warn(error);
      });
  },

  render: function() {
    var content = null;
    if (this.state.forecast !== null) {
      content = <Forecast
                  main={this.state.forecast.main}
                  description={this.state.forecast.description}
                  temp={this.state.forecast.temp}/>;
    }
    return (
      <View style={styles.container}>
        <Image source={require('image!flowers')}
               resizeMode='cover'
               style={styles.backdrop}>
          <View style={styles.overlay}>
           <View style={styles.row}>
             <Text style={styles.mainText}>
               Current weather for
             </Text>
             <View style={styles.zipContainer}>
               <TextInput
                 style={[styles.zipCode, styles.mainText]}
                 returnKeyType='go'
                 onSubmitEditing={this._handleTextChange}/>
             </View>
           </View>
           {content}
         </View>
        </Image>
      </View>
    );
  }
});

var baseFontSize = 16;
var styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    paddingTop: 30
  },
  backdrop: {
    flex: 1,
    flexDirection: 'column'
  },
  overlay: {
    paddingTop: 5,
    backgroundColor: '#000000',
    opacity: 0.5,
    flexDirection: 'column',
    alignItems: 'center'
  },
  row: {
    flex: 1,
    flexDirection: 'row',
    flexWrap: 'nowrap',
    alignItems: 'flex-start',
    padding: 30
  },
  zipContainer: {
    flex: 1,
    borderBottomColor: '#DDDDDD',
    borderBottomWidth: 1,
    marginLeft: 5,
    marginTop: 3
  },
  zipCode: {
    width: 50,
    height: baseFontSize,
  },
  mainText: {
    flex: 1,
    fontSize: baseFontSize,
    color: '#FFFFFF'
  }
});

module.exports = WeatherProject;


我们已经完成了所有的工作,现在尝试运行这个应用。不出意外的话,它将可以同时运行在 Android 和 iOS 设备上,无论是模拟器还是真实物理设备皆可。想对它进行修改或者完善吗?

你可以在 GitHub 仓库查看完整的代码(https://github.com/bonniee/learning-react-native)。

3.5 小结

我们开发的第一个应用涉及了很多知识。本章介绍了新的 UI 组件——<TextInput>,以及如何从中获取用户输入的信息。接下来本章解释了如何在 React Native 中编写基本样式,以及如何在应用中加载并使用图片。最后本章讨论了如何使用 React Native 网络接口从外部网络源中请求数据。对于我们的第一个应用,这已经相当不错了!

幸运的是,这足以证明你可以使用 React Native 快速地开发出具有原生体验的、功能丰富的移动应用。

如果你想继续扩展这个应用,可以尝试:

  • 添加更多的图片,并根据天气预报更换图片;

  • 对邮编进行有效性验证;

  • 切换更便捷的小型键盘进行邮编输入;

  • 展示最近 5 天的天气预报。

随着我们学习更多的知识,例如地理位置,你将可以为天气应用扩展更多的功能。

当然,这只是一个快速的概览。在后面几章中,我们将更加深入地了解 React Native 的最佳实践,并学习使用更多的特性!

目录