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

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

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

3.1 搭建环境

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

有两种通用的方式可以建立 React Native 的开发环境。第一种是使用 Create React Native App 工具,这款工具可以让你便捷地进行安装,但是只支持纯 JavaScript 应用。第二种更加传统的方式是完整安装 React Native 和它的所有依赖。可以把 Create React Native App 当作一种快捷的方式,用来轻松进行测试和原型制作。

在附录 C 中可以找到关于如何从 Create React Native App 迁移到完整的 React Native 项目的内容。

 应该选择哪种方式?我建议初学者选择 Create React Native App,这种方式更加适合教学和快速原型。

到最后,如果你要专业开发一款 React Native 应用,或者编写一款同时使用 JavaScript 及原生 Java、Objective-C 或者 Swift 代码的混合应用,你就需要安装完整的 React Native 项目。

这两种方式接下来都会介绍。后续章节中的示例代码通常是可以在其中任何一种方式中工作的。当某些代码不兼容 Create React Native App 并且需要完整的 React Native 项目时,会加以说明。

3.2 使用Create React Native App进行开发配置

Create React Native App(https://github.com/react-community/create-react-native-app)是一个可以快速创建并运行 React Native 应用的命令行工具,无须安装 Xcode 或者 Android Studio 即可运行。

如果你想要快速上手运行,那么使用 Create React Native App 就是正确的选择。

 Create React Native App 这款工具很棒,但是正如前面提到的,它只支持纯 JavaScript 的应用。在本书后面,我们将讨论如何将 React Native 应用和使用 Java 或者 Objective-C 编写的原生代码集成到一起。别担心,如果你从 Create React Native App 开始,依然可以转变成一个完整的 React Native 应用。

首先,我们要从 npm 安装 create-react-native-app 包。React Native 使用 npm 来管理依赖,npm 是 Node.js 的包管理器。不仅是 Node 环境,在 npm registry 中还包含了各种 JavaScript 项目包。

npm install -g create-react-native-app

3.2.1 使用create-react-native-app创建你的第一个应用

要使用 Create React Native App 创建一个新项目,只需要运行下列命令:

create-react-native-app first-project

这条命令会安装一些 JavaScript 依赖,以及应用的模板代码。项目目录看起来应该如下所示:

.
├── App.js
├── App.test.js
├── README.md
├── app.json
├── node_modules
├── package.json
└── yarn.lock

这个结构看起来像是一个简单的 JavaScript 项目。其中 package.json 包含了项目的元数据,以及项目的依赖。README.md 文件包含了运行项目相关的信息。App.test.js 中包含了一个简单的测试文件。应用代码位于 App.js。要修改这个项目,并构建你自己的应用,你需要从 App.js 入手。

在 3.5 节中,我们会开始构建天气应用,届时会更加详细地介绍这段代码所做的事情。

3.2.2 在iOS或者Android中预览应用

太棒了——现在你的应用已经准备好测试了。要运行应用,在命令行中输入:

cd first-project
npm start

你应该就能够看到图 3-1 所示的屏幕。

图 3-1:使用二维码预览 Create React Native App

要查看你的应用,你需要先安装 Expo 应用(https://expo.io/)的 iOS 或者 Android 版本。安装之后,将手机摄像头对准二维码,React Native 应用就能够加载出来。注意,手机和电脑需要在同一个网络中,并且能够互相通信。

恭喜你!现在你已经创建了第一个 React Native 应用,进行编译,并运行在真实的设备上。

在下一节中,我们将会介绍如何进行 React Native 完整、传统的安装。如果你想要直接开始编程,可以直接跳到 3.4 节。

3.3 使用传统方式进行开发配置

在 React Native 官方文档中,可以查看安装 React Native 及其所有依赖的指引。

你可以使用 Windows、macOS 或者 Linux 来开发 React Native 应用。然而,开发 iOS 应用只能使用 macOS。Linux 和 Windows 用户依然可以使用 React Native 来编写 Android 应用。

因为安装指令随着平台和 React Native 版本更新而变化,所以在这里我们不会详细介绍它们,但是你需要安装以下内容:

  • node.js
  • React Native
  • iOS 开发环境(Xcode)
  • Android 开发环境(JDK、Android SDK、Android Studio)

如果你不想同时安装 iOS 和 Android 开发者工具,那也可以,只要确保已经安装其中一种平台就足够了。

3.3.1 使用react-native创建第一个应用

你可以使用 React Native 命令行工具创建一个新应用。运行以下命令,即可安装这个命令行工具:

npm install -g react-native-cli

现在,我们可以通过运行以下命令,生成一个新的项目,同时包括 React Native、iOS 和 Android 的模板代码:

react-native init FirstProject

生成的目录结构应该类似于这样:

.
├── __tests__
├── android
├── app.json
├── index.android.js
├── index.ios.js
├── ios
├── node_modules
├── package.json
└── yarn.lock

ios/ 和 android/ 目录包含了这些平台对应的模板。你的 React 代码放置在 index.ios.js 和 index.android.js 文件之中,分别是 React 应用不同平台的入口点。通过 npm 安装的依赖文件通常会被放在 node_modules/ 目录下。

3.3.2 在iOS平台运行React Native应用

要在 iOS 平台上运行应用,首先我们要进入新创建的项目目录。随后,你可以像这样运行 React Native 应用:

cd FirstProject
react-native run-ios

或者你可以在 Xcode 中打开应用,并从这里运行 iOS 模拟器:

open ios/FirstProject.xcodeproj

你还可以使用 Xcode,将应用上传到真实设备用户测试。要实现这一点,你需要一个免费的 Apple ID,用来配置代码签名。

要配置代码签名,可以在 Xcode 的项目导航中选择你的主要目标,也就是与项目同名的那一项。下一步,点击 General 标签。在 Signing 菜单下面的 Team 下拉菜单中,选择你的 Apple 开发者账号(见图 3-2)。随后你还需要为测试目标重复同样的步骤。

图 3-2:在 XCode 中选择团队,让你可以在物理设备上测试应用

当你第一次尝试在某台设备上运行应用时,XCode 会提示你登录 Apple 账号,并注册你的设备用于开发。

要了解更多关于如何在真实 iOS 设备上运行应用的细节,可以参考 Apple 的官方文档(https://help.apple.com/xcode/mac/current/#/dev60b6fbbc7)。

请注意,你的 iOS 设备和电脑必须处于同一网络,才能运行你的应用。

3.3.3 在Android平台运行React Native应用

为了在 Android 平台运行 React Native 应用,需要安装完整的 Android 开发环境,其中包括 Android Studio 以及 Android SDK。这份入门文档(https://facebook.github.io/react-native/docs/getting-started.html)中可以看到 Android 依赖的列表。

要在 Android 上运行 React Native,可以输入:

react-native run-android

你还可以在 Android Studio 中打开应用,并从那里运行。

此外,你还可以在 Android 模拟器或者 USB 连接的物理设备上运行应用。要在物理设备上运行,你需要在设备的开发者选项中启用 USB 调试。更多的细节指引,请参考 Android Studio 文档(https://developer.android.com/studio/debug/dev-options.html)。

3.4 探索示例代码

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

如果你用的是 Create React Native App,打开 App.js 文件(见例 3-1)。如果你使用的是完整的 React Native 项目,打开 index.ios.js 或者 index.android.js(见例 3-2)。

例 3-1 为 Create React Native App 项目创建的 App.js 初始代码

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

export default class App extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Hello, world!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center"
  }
});

例 3-2 为 React Native 完整项目创建的 index.ios.js 和 index.android.js 初始代码

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

export default class FirstProject extends Component {
  render() {
    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>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

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

不管你用的是哪一种,让我们来谈谈这里发生的事情。

正如你在例 3-3 中所看到的那样,import 语句的使用方式,和基于 Web 的 React 项目相比,可能会有一点不同。

例 3-3 在 React Native 中导入 UI 元素

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

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

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

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

接下来看看例 3-4 中的组件类。这些代码看起来亲切而熟悉,因为它就是普通的 React 组件。主要的区别是,它使用了 <Text><View> 组件代替了 <div><span>,并使用了样式对象。

例 3-4 带有样式的 FirstProject 组件

export default class FirstProject extends Component {
  render() {
    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>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#F5FCFF',
  },
  welcome: {
    fontSize: 20,
    textAlign: 'center',
    margin: 10,
  },
  instructions: {
    textAlign: 'center',
    color: '#333333',
    marginBottom: 5,
  },
});

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

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

3.5 开发天气应用

本节中,我们将使用示例程序来开发天气应用。这个项目包括如何利用和结合样式表、flexbox、网络通信、用户输入和图像显示等知识来开发一个实用的应用程序,然后将其部署到 Android 或 iOS 设备上。

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

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

图 3-3:天气应用成品

我们要做的第一件事就是在示例应用中替换默认的代码。将初始组件的代码移动到 WeatherProject.js 中。

如果你创建的是完整的 React Native 项目,请修改 index.ios.js 和 index.android.js 文件的内容,如例 3-5 所示。

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

import { AppRegistry } from "react-native";
import WeatherProject from "./WeatherProject";
AppRegistry.registerComponent("WeatherProject", () => WeatherProject);

类似地,如果你使用 Create React Native App 创建了 React Native 应用,就需要替换 App.js 文件中的内容,如例 3-6 所示。

例 3-6 精简后的 App.js 内容,用于 Create React Native App 项目

import WeatherProject from "./WeatherProject";
export default WeatherProject;

3.5.1 处理用户输入

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

例 3-7 在 render 函数前加入这段邮编信息代码

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

如果你已经习惯了使用 React.createClass() 来创建组件,而不是 JavaScript 类,可能会觉得上述代码很奇怪。在创建组件类时,我们可以通过在 constructor 方法中改变 this.state 变量,并设置 React 组件的初始 state 状态值。如果你想要查看组件的生命周期,可以参考 React 文档(https://reactjs.org/docs/react-component.html)。

接着,修改其中一个 <Text> 组件的内容为 this.state.zip,如例 3-8 所示。

例 3-8 添加 <Text> 组件,显示当前的邮编

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

我们用同样的方式添加一个 <TextInput> 组件,如例 3-9 所示。这是一个允许用户输入文本的基础组件。

例 3-9 添加 <Text> 组件,用于输入文本

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

这个 <TextInput> 组件的文档和属性可以在 React Native 官网查看。为了监听一些事件,你可以往 <TextInput> 中传入回调函数,如 onChangeonFocus,但现在暂时不需要这么做。

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

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

我们通过 onSubmitEditing 属性传递的回调,应当作为函数添加到组件中,如例 3-10 所示。

例 3-10 <TextInput>handleText 回调

_handleTextChange = event => {
  this.setState({zip: event.nativeEvent.text})
}

通过使用箭头函数语法,我们可以确保回调函数能够正确绑定到函数实例中。React 会自动绑定 render 这样的生命周期方法,但是对于其他的方法,我们就需要注意 this 绑定的问题。箭头函数会在例 A-8 中介绍。

你还需要更新 import 语句,如例 3-11 所示。

例 3-11 在 React Native 中导入 UI 元素

import {
  ...
  TextInput
  ...
} from "react-native";

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

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

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

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

import React, { Component } from "react";

import { StyleSheet, Text, View, TextInput } from "react-native";

class WeatherProject extends Component {
  constructor(props) {
    super(props);
    this.state = { zip: "" };
  }

  _handleTextChange = event => {
    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>
    );
  }
}

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

export default WeatherProject;

3.5.2 展现数据

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

constructor(props) {
  super(props);
  this.state = { zip: "", forecast: null };
}

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

例 3-13 Forecast.js 中的 <Forecast> 组件

import React, { Component } from "react";

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

class Forecast extends Component {
  render() {
    return (
      <View style={styles.container}>
        <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>
    );
  }
}

const styles = StyleSheet.create({
  container: { height: 130 },
  bigText: {
    flex: 2,
    fontSize: 20,
    textAlign: "center",
    margin: 10,
    color: "#FFFFFF"
  },
  mainText: { flex: 1, fontSize: 16, textAlign: "center", color: "#FFFFFF" }
});

export default Forecast;

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

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

例 3-14 更新后的 WeatherProject.js,包含了 <Forecast> 组件

import React, { Component } from "react";

import { StyleSheet, Text, View, TextInput } from "react-native";
import Forecast from "./Forecast";

class WeatherProject extends Component {
  constructor(props) {
    super(props);
    this.state = { zip: "", forecast: null };
  }

  _handleTextChange = event => {
    this.setState({ zip: event.nativeEvent.text });
  };

  render() {
    let 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}>
        <Text style={styles.welcome}>
          You input {this.state.zip}.
        </Text>
        {content}
        <TextInput
          style={styles.input}
          onSubmitEditing={this._handleTextChange}
        />
      </View>
    );
  }
}

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

export default WeatherProject;

因为我们现在还没有拿到需要渲染的天气数据,所以从视觉上看,应该还没有发生变化。

3.5.3 从Web获取数据

现在,让我们来探索使用 React Native 中可用的 API。你不需要使用 jQuery 在移动设备上发送 AJAX 请求,只需要使用 React Native 实现的 Fetch API 即可。如例 3-15 所示,这个 API 的语法基于 promise,用法相当简单。

例 3-15 使用 React Native Fetch API

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

如果你还不习惯 promise 式的语法,请参见附录 A 中的 A.8 节。

我们会使用 OpenWeatherMap API,它提供了简单的端点,返回给定邮编的当前天气。这个 API 作为一个轻量库封装在 open_weather_map.js 中,如例 3-16 所示。

例 3-16 OpenWeatherMap 库,来自 src/weather/open_weather_map.js

const WEATHER_API_KEY = "bbeb34ebf60ad50f7893e7440a1e2b0b";
const API_STEM = "http://api.openweathermap.org/data/2.5/weather?";

function zipUrl(zip) {
  return `${API_STEM}q=${zip}&units=imperial&APPID=${WEATHER_API_KEY}`;
}

function fetchForecast(zip) {
  return fetch(zipUrl(zip))
    .then(response => response.json())
    .then(responseJSON => {
      return {
        main: responseJSON.weather[0].main,
        description: responseJSON.weather[0].description,
        temp: responseJSON.main.temp
      };
    })
    .catch(error => {
      console.error(error);
    });
}

export default { fetchForecast: fetchForecast };

现在,我们可以这样导入:

import OpenWeatherMap from "./open_weather_map";

为了继承这个 API,我们可以修改 <TextInput> 组件中的回调函数,让其查询 OpenWeatherMap API,如例 3-17 所示。

例 3-17 向 OpenWeatherMap API 请求数据

_handleTextChange = event => {
  let zip = event.nativeEvent.text;
  OpenWeatherMap.fetchForecast(zip).then(forecast => {
    console.log(forecast);
    this.setState({ forecast: forecast });
  });
};

这里将天气预报的信息打印到控制到,这对于我们来说非常有用,要了解更多关于如何查看控制台输出的内容,请参见 9.1.2 节。

最后,我们还要更新 container 的样式,才能看到天气预报文本的渲染。

container: {
  flex: 1,
  justifyContent: "center",
  alignItems: "center",
  backgroundColor: "#666666"
}

现在,当你输入邮编时,应该就能看到天气预报信息渲染出来(见图 3-4)。

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

更新后的 WeatherProject.js 代码,展示在例 3-18 中。

例 3-18 现在 WeatherProject.js 中是真实数据了

import React, { Component } from "react";

import { StyleSheet, Text, View, TextInput } from "react-native";
import OpenWeatherMap from "./open_weather_map";
import Forecast from "./Forecast";

class WeatherProject extends Component {
  constructor(props) {
    super(props);
    this.state = { zip: "", forecast: null };
  }

  _handleTextChange = event => {
    let zip = event.nativeEvent.text;
    OpenWeatherMap.fetchForecast(zip).then(forecast => {
      this.setState({ forecast: forecast });
    });
  };

  render() {
    let 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}>
        <Text style={styles.welcome}>
          You input {this.state.zip}.
        </Text>
        {content}
        <TextInput
          style={styles.input}
          onSubmitEditing={this._handleTextChange}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#666666"
  },
  welcome: { fontSize: 20, textAlign: "center", margin: 10 },
  input: {
    fontSize: 20,
    borderWidth: 2,
    padding: 2,
    height: 40,
    width: 100,
    textAlign: "center"
  }
});

export default WeatherProject;

3.5.4 添加背景图片

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

图片资源的管理和任何其他的代码资源非常类似:你可以通过 require 调用来导入图片。例如要使用 flowers.png 作为背景图片的话,我们可以这样导入:

<Image source={require('./flowers.png')}/>

这个图片文件可以在 GitHub(https://github.com/bonniee/learning-react-native/blob/2.0.0/src/weather/flowers.png)中获取。

和 JavaScript 资源类似,如果你同时拥有 flowers.ios.png 和 flowers.android.png 两个文件的话,React Native 打包器会根据平台加载对应的图片。同样,你可以使用 @2x@3x 后缀,为不同的屏幕密度提供不同的图片。所以,假设我们可以像这样构造我们的项目目录:

.
├── flowers.png
├── flowers@2x.png
├── flowers@3x.png
...

要添加背景图到 <View> 中,我们不能像 Web 那样,在 <div> 上设置 background 属性。取而代之的做法是,我们要使用一个 <Image> 组件作为容器:

<Image source={require('./flowers.png')}
       resizeMode='cover'
       style={styles.backdrop}>
  // 内容区
</Image>

<Image> 组件预期接收一个 source 属性,我们在这里使用 require 来获取值。

在样式中,别忘了使用 flexDirection,让其子节点可以按照我们期望的方式进行渲染:

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

现在,我们在 <Image> 里面添加一些子节点。在 <WeatherProject> 组件中修改 render 方法,让其返回以下内容:

<View style={styles.container}>
  <Image
    source={require("./flowers.png")}
    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]}
            onSubmitEditing={event => this._handleTextChange(event)}
          />
        </View>
      </View>
      {content}
    </View>
  </Image>
</View>

你会注意到,我使用了一些还没有讨论过的样式,例如 rowoverlayzipContainerzipCode。你可以直接跳到例 3-19,查看完整的样式表。

3.5.5 整合

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

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

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

例 3-19 WeatherProject.js 完整代码

import React, { Component } from "react";

import { StyleSheet, Text, View, TextInput, Image } from "react-native";

import Forecast from "./Forecast";
import OpenWeatherMap from "./open_weather_map";

class WeatherProject extends Component {
  constructor(props) {
    super(props);
    this.state = { zip: "", forecast: null };
  }

  _handleTextChange = event => {
    let zip = event.nativeEvent.text;
    OpenWeatherMap.fetchForecast(zip).then(forecast => {
      this.setState({ forecast: forecast });
    });
  };

  render() {
    let 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("./flowers.png")}
          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]}
                  onSubmitEditing={this._handleTextChange}
                  underlineColorAndroid="transparent"
                />
              </View>
            </View>
            {content}
          </View>
        </Image>
      </View>
    );
  }
}

const baseFontSize = 16;

const 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: {
    flexDirection: "row",
    flexWrap: "nowrap",
    alignItems: "flex-start",
    padding: 30
  },
  zipContainer: {
    height: baseFontSize + 10,
    borderBottomColor: "#DDDDDD",
    borderBottomWidth: 1,
    marginLeft: 5,
    marginTop: 3
  },
  zipCode: { flex: 1, flexBasis: 1, width: 50, height: baseFontSize },
  mainText: { fontSize: baseFontSize, color: "#FFFFFF" }
});

export default WeatherProject;

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

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

3.6 小结

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

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

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

  • 添加更多的图片,并根据天气预报更换图片;
  • 对邮编进行有效性验证;
  • 切换更便捷的小型键盘进行邮编输入;
  • 展示最近 5 天的天气预报。

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

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

目录