React Navtive框架教程

泡在网上的日子 / 文 发表于2015-09-10 10:11 次阅读 React

几个月前Facebook推出了React Native框架,允许开发者用JavaScript编写本地iOS App——今天,官方代码库的beta版本终于放出!

早在几年前,开发者就已经在使用JavaScript和HTML5加上PhoneGap编译器来编写iOS App了,因此React Native框架是不是有点多余?

但React Native确实是一个很了不起的东东,开发者为之欢欣鼓舞,这是因为:
1. 通过React Native框架,你可以用JavaScript来编写和运行应用程序逻辑,而UI却可以是真正的本地代码编写的;因此,你完全不需要一个HTML5编写的UI。
2. React框架采用了一种新颖的、激进的和高度函数式的方式来构建UI。简单说,应用程序的UI可以简单地用一个函数来表示应用程序当前的状态。

React Native的重点是把React编程模型引进到移动App的开发中去。它的目的并不是跨平台,一次编写到处运行。它真正的目标是“一次学习多处编写”。这是一个重大的区别。本教程只涉及iOS,但一旦你学会了它的思想,你就可以快速将同样的知识应用到Android App的编写上。

如果你过去只使用O-C或Swift编写过iOS应用程序,你可能不会因为可以用JavaScript编写iOS App而激动不已。但是,作为一个Swift开发者,上面所说的第二点原因肯定会激起你的兴趣!

通过Swift,你毫无疑问已经学习到那些新颖的、函数式的编写代码的方法;以及一些和过去不同或相同的技术。但是,你构建UI的方式仍然和O-C时代没有太大的不同:仍然要离不开UIKit。

通过一些有趣的概念,比如虚拟DOM和reconciliation,React直接将函数式编程的理念用到了UI层面。
这个教程带你一起构建一个搜索英国房产信息的应用:

blob.png

如果你之前从未写过任何 JavaScript ,别担心;这篇教程带着你一点一点编写代码。React 使用 CSS 属性来定义样式,这些样式通常都很易于阅读和理解,但是如果你想进一步了解,可以参考 Mozilla Developer Network reference。
要想学习更多内容,请往下看!

开始

React Native 框架托管在GitHub。要获得这个框架,你可以使用git命令克隆项目到本地,或者直接下载zip包。 如果你不想使用源代码,也可以用命令行界面(CLI)创建React Native项目,本文将使用这种方式。

React Native 使用了 Node.js,如果你的机器上没有安装Node.js,请先安装它。
首先需要安装 Homebrew,安装指南请参考Homebrew网站。然后用brew命令来安装Node.js:

brew install node

然后安装 watchman(Facebook推出的文件改动监听器):

brew install watchman

React Native通过watchman来监视代码文件的改动并适时进行编译。这就好比Xcode,它会在每次文件被保存时对文件进行编译。
然后用npm命令安装React Native 的CLI工具:

npm install -g react-native-cli

这个命令通过Node Package Manager来下载和安装CLI工具,npm是一个类似CocoPods或Carthage工具。
定位到要创建React Native 项目的文件夹,使用CLI工具创建一个新的React Native项目:

react-native init PropertyFinder

这将创建一个默认的React Native项目,其中包含有能够让React Native项目编译运行的必要内容。
在React Native项目文件夹中,有一个node_modules文件夹,它包含React Native 框架文件。此外还有一个 index.ios.js 文件,这是CLI创建的脚手架代码。最后,还有一个Xcode项目文件及一个iOS文件夹,后者会有一些iOS代码用于引导React Navtive App。
打开Xcode项目文件,build&run。模拟器启动并显示一句问候语:

这里写图片描述

与此同时Xcode还会打开一个终端窗口,并显示如下信息:
> ===============================================================
| Running packager on port 8081.
| Keep this packager running while developing on any JS
| projects. Feel free to close this tab and run your own
| packager instance if you prefer.
|
| https://github.com/facebook/react-native
|
===============================================================
>
> Looking for JS files in /Users/colineberhardt/Temp/TestProject
> React packager ready.

这是React Navtive Packager,它在node容器中运行。你待会就会发现它的用处。
千万不要关闭这个窗口,让它一直运行在后面。如果你意外关闭它,可以在Xcode中先停止程序,再重新运行程序。

> 注意:
> 在开始接触具体的代码之前(在本教程中,主要是js代码),我们将推荐 Sublime
> Text这个文本编辑工具,因为Xcode并不适合用于编写js代码的。当然,你也可以使用 atom, brackets
> 等其他轻量级的工具来替代。

你好, React Native

在开始编写这个房产搜索App之前,我们先来创建一个简单的Hello World项目。我们将通过这个例子来演示React Native的各个组件和概念。
用你喜欢的文本编辑器(例如Sublime Text)打开index.ios.js ,删除所有内容。然后加入以下语句:

'use strict';

这将开启严谨模式,这会改进错误的处理并禁用某些js语法特性,这将让JavaScript表现得更好。

> 注意: 关于严谨模式,读者可以参考 Jon Resig的文章:“ECMAScript 5 Strict Mode, JSON, and More”。

然后加入这一句:

var React = require('react-native');

这将加载 react-native 模块,并将其保存为React变量。React Native 使用和Node.js 一样的 require 函数来加载模块,类似于Swift中的import语句。

> 注意:
> 关于JavaScript 模块的概念,请参考 Addy Osmani的这篇文章

然后加入如下语句:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  }
});

这将定义一个css样式,我们将在显示“Hello World”字符串时应用这个样式。
在React Native 中,我们可以使用 Cascading Style Sheets (CSS) 语法来格式化UI样式。
接下来敲入如下代码:

class PropertyFinderApp extends React.Component {
  render() {
    return React.createElement(React.Text, {style: styles.text}, "Hello World!");
  }
}

这里我们定义了一个JavaScript 类。JavaScript类的概念出现自ECMAScript 6。由于JavaScript是一门不断演变的语言,因此web开发者必须保持与浏览器的向下兼容。由于React Native基于JavaScriptCore,因此我们完全可以放心使用它的现代语法特性,而不需要操心与老版本浏览器兼容的问题。

> Note: If you are a web developer, I’d thoroughly encourage you to use
> modern JavaScript, and then convert to older JavaScript using tools
> such as Babel to maintain support for older and incompatible browsers.
>
> 注意:如果你是Web开发人员,我建议你使用新的JavaScript语法。有一些工具比如 Babel,可以将现代JavaScript语法转变为传统JavaScript语法,这样就能和老式浏览器进行兼容。

PropertyFinderApp 类继承自 React.Componen,后者是React UI中的基础。Components包含了一些不可变属性、可变属性和一些渲染方法。当然,这个简单App中,我们就用到一个render方法。
React Native 的Components不同于UIKit 类,它们是轻量级的对象。框架会将React Components转换为对应的本地UI对象。
最后敲入如下代码:

React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });

AppRegistry 代表了App的入口以及根组件。保存文件,返回Xcode。确保当前Scheme为PropertyFinder ,然后在模拟器运行App。你将看到如下效果:

看到了吧,模拟器将JavaScript代码渲染为本地UI组件,你不会看到任何浏览器的痕迹。
你可以这样来确认一下:
在Xcode中,选中 Debug\View Debugging\Capture View Hierarchy,查看本地视图树。你将找不到任何UIWebView实例。
这里写图片描述

你一定会惊奇这一切是怎么发生的。在 Xcode 中打开 AppDelegate.m,找到application:didFinishLaunchingWithOptions:方法。
在这个方法中,创建了一个RCTRootView,该对象负责加载JavaScript App并渲染相关视图。
App一启动,RCTRootView会加载如下URL的内容:

http://localhost:8081/index.ios.bundle

还记得App启动时弹出的终端窗口吗?终端窗口中运行的packager和server会处理上述请求。
你可以用Safari来打开上述URL,你将会看到一些JavaScript代码。在React Native 框架代码中你会找到“Hello World”相关代码。
当App打开时,这些代码会被加载并执行。以我们的App来说,PropertyFinderApp组件会被加载,然后创建相应的本地UI组件。

你好, JSX

前面我们用React.createElement构建了一个简单的UI ,React 会将之转换为对应的本地对象。但对于复杂UI来说(比如那些组件嵌套的UI),代码会变得非常难看。
确保App保持运行,回到文本编辑器,修改index.ios.js中的return语句为:

return Hello World (Again);

这里使用了JSX语法,即JavaScript 语法扩展,它基本上是将JavaScript代码混合了HTML风格。如果你是一个web开发人员,对此你应该不会陌生。 在本文中,JSX随处可见。

保存 index.ios.js回到iPhone模拟器,按下快捷键 Cmd+R,你会看到App的显示变成了 “Hello World (Again)”。

重新运行React Navtive App如同刷新Web页面一样简单。

因为你实际上是在和JavaScript打交道,所以只需修改并保存index.ios.js,即可让App内容得到更新,同时不中断App的运行。

> 注意:
> 如果你还有疑问,你可以用浏览器在看一下你的“Bundle”内容,它应该也发生了变化。

好了,“Hello World” 的演示就到此为止;接下来我们要编写一个真正的React App了!

实现导航

这个demo使用了标准的UIKit中的导航控制器来提供”栈式导航体验“。接下来我们就来实现这个功能。

在 index.ios.js, 将 PropertyFinderApp 类修改为 HelloWorld:

class HelloWorld extends React.Component {

我们仍然要显示“Hello World”字样,但不再将它作为App的根视图。
然后为HelloWorld加入以下代码:

class PropertyFinderApp extends React.Component {
  render() {
    return (
 
    );
  }
}

这将创建一个导航控制器,并指定了它的外观样式和初始route(相对于HelloWorld视图)。在web开发中,routing是一种技术,用于表示应用程序的导航方式,即哪个一页面(或route)对应哪一个URL。
然后修改css样式定义,在其中增加一个container样式:

var styles = React.StyleSheet.create({
  text: {
    color: 'black',
    backgroundColor: 'white',
    fontSize: 30,
    margin: 80
  },
  container: {
    flex: 1
  }
});

flex: 1的意思稍后解释。
回到模拟器,按 Cmd+R 查看效果:

这个导航控制器有一个根视图,即图中显示的”Hello World“文本。非常好——我们的App已经具备了基本的导航功能。是时候显示一些”真正的“UI了!

实现查找

新建一个 SearchPage.js 文件,保存在 index.ios.js同一目录。在这个文件中加入代码:

'use strict';

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

这里使用了一个解构赋值(destructuring assignment),可以将多个对象属性一次性赋给多个变量。这样,在后面的代码中,我们就可以省略掉React前缀,比如用StyleSheet 来代替 React.StyleSheet。解构赋值对于数组操作来说尤其方便,请参考well worth learning more about.
然后定义如下Css样式:

var styles = StyleSheet.create({
description: {
marginBottom: 20,
fontSize: 18,
textAlign: ‘center’,
color: ‘#656565′
},
container: {
padding: 30,
marginTop: 65,
alignItems: ‘center’
}
});

这里,再次使用了标准的 CSS 属性。虽然用CSS设置样式在可视化方面比起在IB中要差一些,但总比在viewDidLoad()方法中用代码写要好一些。
然后加入以下代码:

class SearchPage extends Component {
  render() {
    return (


          Search for houses to buy!


          Search by place-name, postcode or search near your location.


    );
  }
}

在render方法中使用了大量的JSX语法来构造UI组件。通过这种方式,你可以非常容易地构造出如下组件:在一个Container View中包含了两个label。
在源文件的最后加入这一句:

module.exports = SearchPage;

This exports the SearchPage class, which permits its use in other files.
这一句将使 SearchPage 类可被其他js文件引用。

然后需要修改App的导航。

打开 index.ios.js 在文件头部、现有的require 语句后加入 require 语句:

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

在 PropertyFinderApp 类的 render 函数中,修改 initialRoute 为:

component: SearchPage

这里我们可以将HelloWorld类和它对应的样式移除了,我们不再需要它。

回到模拟器,按下 Cmd+R 查看效果:

你新创建的组件SearchPage显示在屏幕中。

弹性盒子模型

一直以来,我们都在用原始的CSS属性来设置外边距、内边距和颜色。但是,最新的CSS规范中增加了弹性盒子的概念,非常利于我们对App的UI进行布局,虽然你可能还不太熟悉它。

React Native 使用了 css-layout 库,在这个库中实现了弹性盒子,而这种模型无论对iOS还是Android来说都很好理解。

更幸运的是,Facebook针对许多语言单独实现了这个项目,这就引申出了许多新颖的用法,比如在SVG中应用弹性盒子布局(是的,这篇文章也是我写的,为此我不得不熬到深夜)。

在这个App中,采用了默认的垂直流式布局,即容器中的子元素按照从上到下的顺序进行布局。比如:

这被称作主轴, 主轴可能是水平方向,也可能是垂直方向。

每个子元素的纵向位置由它们的边距(margin)、间距(padding)和高决定。容器的alignItems属性会被设置为居中(center),这决定了子元素在交叉轴上的位置。在本例里,将导致子元素水平居中对齐。

接下来我们添加一些文本输入框和按钮。打开SearchPage.js 在第二个 Text 元素后添加:

<View style={styles.flowRight}>
  <TextInput
    style={styles.searchInput}
    placeholder='Search via name or postcode'/>
  <TouchableHighlight style={styles.button}
      underlayColor='#99d9f4'>
    <Text style={styles.buttonText}>Go</Text>
  </TouchableHighlight>
</View>
<TouchableHighlight style={styles.button}
    underlayColor='#99d9f4'>
  <Text style={styles.buttonText}>Location</Text>
</TouchableHighlight>

这段代码添加了两个顶级的视图:一个文本输入框外加一个按钮,以及一个单独的按钮。它们所使用的样式待会我们再介绍。

接着,在styles中增加如下样式:

flowRight: {
  flexDirection: 'row',
  alignItems: 'center',
  alignSelf: 'stretch'
},
buttonText: {
  fontSize: 18,
  color: 'white',
  alignSelf: 'center'
},
button: {
  height: 36,
  flex: 1,
  flexDirection: 'row',
  backgroundColor: '#48BBEC',
  borderColor: '#48BBEC',
  borderWidth: 1,
  borderRadius: 8,
  marginBottom: 10,
  alignSelf: 'stretch',
  justifyContent: 'center'
},
searchInput: {
  height: 36,
  padding: 4,
  marginRight: 5,
  flex: 4,
  fontSize: 18,
  borderWidth: 1,
  borderColor: '#48BBEC',
  borderRadius: 8,
  color: '#48BBEC'
}

不同样式属性间以逗号分隔,这样你在container选择器后必须以一个逗号结尾。

这些样式将被文本输入框和按钮所用。

回到模拟器,按下Cmd+R ,你将看到如下效果:

Go按钮和其紧随的文本框在同一行,因此我们将它们用一个容器装在一起,同时容器的flexDirection: 样式属性设置为’row’ 。我们没有显式指定文本框和按钮的宽度,而是分别指定它们的flex样式属性为4和1。也就是说,它们的宽度在整个宽度(屏幕宽度)中所占的份额分别为4和1。

而且,视图中的两个按钮都不是真正的按钮。对于UIKit,按钮不过是可以点击的标签而已,因此React Native开发团队能够用JavaScript以一种简单的方式构建按钮:TouchableHighlight是一种React Native组件,当它被点击时,它的前景会变得透明,从而显示其隐藏在底部的背景色。

最后我们还要在视图中添加一张图片。这些图片可以在此处下载。下载后解压缩zip文件。

在 Xcode 打开 Images.xcassets 文件,点击加号按钮,添加一个新的image set。然后将需要用到的图片拖到image set右边窗口对应的位置。

要让这些图片显示,必须停止你的 React Native App并重新启动。

在location按钮对应的 TouchableHighlight 组件下加入:

<Image source={require('image!house')} style={styles.image}/>


然后,为图片添加适当的样式定义,记得在上一个样式之后添加一个逗号结尾:

image: {
  width: 217,
  height: 138
}

由于我们将图片添加到了Images.xcasset资源包中,我们需要用require(‘image!house’)语句获得图片在App包中的正确路径。在Xcode中,打开Images.xcassets ,你可以找到名为house的image set。

回到模拟器,按下Cmd+R 查看运行效果:


> 注意: 如果图片没有显示,却看到一个““image!house” cannot be
> found”的提示,则可以重启packager(在终端中输入npm start命令)。

到目前为止,我们的App看上去有模有样,但它还缺少很多实际的功能。接下来的任务就是为App增加一些状态,执行一些动作。

添加组件状态

每个React组件都有一个state对象,它是一个键值存储对象。在组件被渲染之前,我们可以设置组件的state对象。

打开SearchPage.js,在 SearchPage 类的 render()方法前,加入以下代码:

constructor(props) {
  super(props);
  this.state = {
    searchString: 'london'
  };
}

现在组件就有一个state变量了,同时我们在state中存放了一个 searchString:’london’ 的键值对象。

然后我们来使用这个state变量。在render方法中,修改TextInput元素为:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  placeholder='Search via name or postcode'/>

这将改变 TextInput 的value 属性,即在TextInput中显示一个london的文本——即state变量中的searchString。这个值在我们初始化state时指定的,但如果用户修改了文本框中文本,那又怎么办?

首先创建一个事件处理方法。在 SearchPage 类中增加一个方法:

onSearchTextChanged(event) {
  console.log('onSearchTextChanged');
  this.setState({ searchString: event.nativeEvent.text });
  console.log(this.state.searchString);
}

首先从事件参数event中获得text属性,然后将它保存到组件的state中,并用控制台输出一些感兴趣的内容。

为了让文本内容改变时这个方法能被调用,我们需要回到TextInput的onChange事件属性中,绑定这个方法,即新加一个onChange属性,如以下代码所示:

<TextInput
  style={styles.searchInput}
  value={this.state.searchString}
  onChange={this.onSearchTextChanged.bind(this)}
  placeholder='Search via name or postcode'/>

一旦用户改变了文本框中的文本,这个函数立即就会被调用。

> 注意: bind(this) 的使用有点特殊。JavaScript 中 this
> 关键字的含义其实和大部分语言都不相同,它就好比Swift语言中的self。bind方法的调用使得onSearchTextChanged
> 方法中能够引用到this,并通过this引用到组件实例。更多内容请参考 MDN page on this。

然后,我们在render方法顶部、return语句之前加一条Log语句:

console.log('SearchPage.render');

通过这些log语句,你应该能明白大致发生了什么事情!

返回模拟器,按下Cmd+R,我们将看到文本框中一开始就有了一个london的字样,当你编辑这段文本后,控制台中的内容将显示:

查看上面的截屏,log语句输出的顺序似乎有点问题:
1. 组件初始化后调用 render() 方法
2. 当文本被改变, onSearchTextChanged() 被调用
3. 我们在代码中改变了组件的state 属性,因此render()方法会被调用
4. onSearchTextChanged() 打印新的search string.

当React 组件的状态被改变时,都会导致整个UI被重新渲染——所有组件的render方法都会被调用。这样做的目的,是为了将渲染逻辑和组件状态的改变完全进行分离。

在其他所有的UI框架中,要么程序员在状态改变时自己手动刷新UI,要么使用一种绑定机制在程序状态和UI之间进行联系。就像我另一篇文章 MVVM pattern with ReactiveCocoa所讲。

而在React中,我们不再操心状态的改变会导致那一部分UI需要刷新,因为当状态改变所有的UI都会刷新。

当然,你也许会担心性能问题。

难道每次状态改变时,整个UI都会被舍弃然后重新创建吗?
这就是React真正智能的地方。每当UI要进行渲染时,它会遍历整个视图树并计算render方法,对比与当前UIKit视图是否一致,并将需要改变的地方列出一张列表,然后在此基础上刷新视图。也就是说,只有真正发生变化的东西才会被重新渲染。

ReactJS将一些新奇的概念应用到了iOS App中,比如虚拟DOM(Document Object Modal,web文档可视树)和一致性。这些概念我们可以稍后再讨论,先来看下这个App接下来要做的工作。删除上面添加的Log语句。

开始搜索

为了实现搜索功能,我们需要处理Go按钮点击事件,创建对应的API请求,显示网络请求的状态。
打开SearchPage.js, 在constructor方法中修改state的初始化代码:

this.state = {
  searchString: 'london',
  isLoading: false
};

isLoading 属性用于表示查询是否正在进行。
在render方法最上面增加:

var spinner = this.state.isLoading ?
  ( <ActivityIndicatorIOS
      hidden='true'
      size='large'/> ) :
  ( <View/>);

这里用了一个三目运算,这是一个if语句的简化形式。如果isLoading为true,显示一个网络指示器,否则显示一个空的view。
在return语句中,在Image下增加:

{spinner}

在Go按钮对应的 TouchableHighlight 标签中增加如下属性:

onPress={this.onSearchPressed.bind(this)}

在 SearchPage 类中新增如下方法:

_executeQuery(query) {
  console.log(query);
  this.setState({ isLoading: true });
}

onSearchPressed() {
  var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
  this._executeQuery(query);
}

_executeQuery() 目前仅仅是在控制台中输出一些信息,同时设置isLoading属性为true,剩下的功能我们留到后面完成。

> 注意: JavaScript 类没有访问器,因此也就没有私有的概念。因此我们会在方法名前加一个下划线,以表示该方法视同为私有方法。

当Go按钮被点击, onSearchPressed() 即被调用。
然后,在 SearchPage 类声明之前,声明如下实用函数:

function urlForQueryAndPage(key, value, pageNumber) {
  var data = {
      country: 'uk',
      pretty: '1',
      encoding: 'json',
      listing_type: 'buy',
      action: 'search_listings',
      page: pageNumber
  };
  data[key] = value;

  var querystring = Object.keys(data)
    .map(key =&gt; key + '=' + encodeURIComponent(data[key]))
    .join('&amp;');

  return 'http://api.nestoria.co.uk/api?' + querystring;
};

这个函数不依赖SearchPage类,因此被定义为函数而不是方法。它首先将key\value参数以键值对形式放到了data集合中,然后将data集合转换成以&符分隔的“键=值”形式。=>语法是箭头函数的写法,一种创建匿名函数简洁写法,具体请参考recent addition to the JavaScript language 。

回到模拟器,按下 Cmd+R 重启App,然后点击‘Go’ 按钮。你将看到网络指示器开始转动。同时控制台将输出:

网络指示器显示,同时要请求的URL也打印出来了。拷贝并粘贴URL到Safari,查看搜索结果。你将看到一堆JSON对象。我们将用代码解析这些JSON对象。

> 注意: 这个App使用 Nestoria API 来查找房子。查找结果以JSON格式返回。官方文档中列出了所有请求的URL规范及响应格式。

发送请求

打开 SearchPage.js,在初始化状态过程中增加一个message属性:

this.state = {
  searchString: 'london',
  isLoading: false,
  message: ''
};

在render方法中,在UI元素的最后加入:

{this.state.message}

这个Text用于向用户显示一些文本。

在 SearchPage 类中,在 _executeQuery()方法最后加入:

fetch(query)
  .then(response =&gt; response.json())
  .then(json =&gt; this._handleResponse(json.response))
  .catch(error =&gt; 
     this.setState({
      isLoading: false,
      message: 'Something bad happened ' + error
   }));

fetch 函数在 Fetch API中定义,这个新的JavaScript规范被Firefox 39(Nightly版)以及Chrome 42(开发版)支持,它在XMLHttpRequest的基础上进行了极大的改进。结果是异步返回的,同时使用了 promise规范,如果response中包含有效的JSON对象则将JSON对象的response成员(另一个JSON)传到_handleResponse方法(后面实现)。

然后在 SearchPage类中增加方法:

_handleResponse(response) {
  this.setState({ isLoading: false , message: '' });
  if (response.application_response_code.substr(0, 1) === '1') {
    console.log('Properties found: ' + response.listings.length);
  } else {
    this.setState({ message: 'Location not recognized; please try again.'});
  }
}

如果查询结果成功返回,我们重置 isLoading 属性为false,然后打印结果集的总行数。

> 注意: Nestoria 有 不以1开头的响应码, 这些代码都非常有用。例如202 和 200表示返回一个推荐位置的列表。当完成这个实例后,你可以尝试处理这些返回码,并将列表提供给用户选择。

保存,返回模拟器,按下Cmd+R ,然后搜索 ‘london’你将在控制台看到一条消息,表示搜索到20条房子信息。尝试输入一个不存在的地名,比如 ‘narnia’ 你将看到如下信息:

接下来我们在伦敦或者别的什么城市搜索20座房子。

显示搜索结果

新建一个文件: SearchResults.js, 编写如下代码:

'use strict';

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

你注意到了吗?一切都是老样子,一条requires语句和一个结构赋值。

然后定义一个Componet子类:

class SearchResults extends Component {

  constructor(props) {
    super(props);
    var dataSource = new ListView.DataSource(
      {rowHasChanged: (r1, r2) =&gt; r1.guid !== r2.guid});
    this.state = {
      dataSource: dataSource.cloneWithRows(this.props.listings)
    };
  }

  renderRow(rowData, sectionID, rowID) {
    return (


          {rowData.title}


    );
  }

  render() {
    return (

    );
  }

}

上述代码中使用了一个专门的组件——ListView ——该组件非常像ITableView。通过ListView.DataSource, 我们可以向ListView提供数据。renderRow函数则用于为每个行提供UI。

在构建数据源的时候,我们使用箭头函数对不同的行进行识别。这个函数在ListView进行“一致化”的时候被调用,以便判断列表中的数据是否被改变。在本例中,Nestoria API有一个guid属性,刚好可以用来作为判断的标准。

最后,加入一条模块输出语句:

module.exports = SearchResults;

在SearchPage.js 头部,require 下方加入:

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

这样我们就可以 SearchPage 类中使用SearchResults类了。
在_handleResponse 方法,将console.log 一句替换为:

this.props.navigator.push({
  title: 'Results',
  component: SearchResults,
  passProps: {listings: response.listings}
});

上述代码将导航至SearchResults 页面,并将请求到的列表数据传递给它。Push方法可以将页面添加到导航控制器的ViewController堆栈中,同时你的导航栏上将出现一个Back按钮,点击它可以返回到上一页面。

回到模拟器, 按下Cmd+R ,进行一个查找动作。你将看到搜索结果如下:

好了,房子清单已经列出来了,不过列表有一点丑陋。接下来我们会让它变得漂亮一点。

表格样式

现在,React Native的代码对我们来说已经不陌生了,接下来我们的教程可以稍微加快一点节奏了。

在 SearchResults.js文件的解构赋值语句之后,添加样式定义:

var styles = StyleSheet.create({
  thumb: {
    width: 80,
    height: 80,
    marginRight: 10
  },
  textContainer: {
    flex: 1
  },
  separator: {
    height: 1,
    backgroundColor: '#dddddd'
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    color: '#656565'
  },
  rowContainer: {
    flexDirection: 'row',
    padding: 10
  }
});

这些代码中的样式将在渲染单元格时用到。

修改renderRow() 方法如下:

renderRow(rowData, sectionID, rowID) {
  var price = rowData.price_formatted.split(' ')[0];
 
  return (
    <TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
        underlayColor='#dddddd'>
      <View>
        <View style={styles.rowContainer}>
          <Image style={styles.thumb} source={{ uri: rowData.img_url }} />
          <View  style={styles.textContainer}>
            <Text style={styles.price}>£{price}</Text>
            <Text style={styles.title} 
                  numberOfLines={1}>{rowData.title}</Text>
          </View>
        </View>
        <View style={styles.separator}/>
      </View>
    </TouchableHighlight>
  );
}

其中价格将以‘300,000 GBP’的格式显示,记得将GBP 后缀删除。上述代码用你已经很熟悉的方式来渲染单元格UI。缩略图以URL方式提供,React Native 自动将其解码(主线程中)。

在TouchableHightlight组件的onPress属性中再次使用了箭头函数,并将该行数据的guid作为传递的参数。

最后一个方法,用于处理点击事件

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop =&gt; prop.guid === propertyGuid)[0];
}

这里,当用户点击某行时,通过guid去房产列表中找到对应的房屋信息。
回到模拟器,按下 Cmd+R ,观察运行结果:

这下看起来好多了——只不过,那些住在London的人居然住得起这么贵房子?真是令人难以置信!

Time to add the final view to the application.

接下来,我们就来实现App的最后一个界面了。

Property Details View

查看房屋详情

新建一个 PropertyView.js 文件到项目中,编辑如下内容:

'use strict';

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

确保进行到这一步的时候,你还没有睡着!:]

继续添加如下样式:

var styles = StyleSheet.create({
  container: {
    marginTop: 65
  },
  heading: {
    backgroundColor: '#F8F8F8',
  },
  separator: {
    height: 1,
    backgroundColor: '#DDDDDD'
  },
  image: {
    width: 400,
    height: 300
  },
  price: {
    fontSize: 25,
    fontWeight: 'bold',
    margin: 5,
    color: '#48BBEC'
  },
  title: {
    fontSize: 20,
    margin: 5,
    color: '#656565'
  },
  description: {
    fontSize: 18,
    margin: 5,
    color: '#656565'
  }
});

然后将组件加入视图:

class PropertyView extends Component {
 
  render() {
    var property = this.props.property;
    var stats = property.bedroom_number + ' bed ' + property.property_type;
    if (property.bathroom_number) {
      stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
        ? 'bathrooms' : 'bathroom');
    }
 
    var price = property.price_formatted.split(' ')[0];
 
    return (
      <View style={styles.container}>
        <Image style={styles.image} 
            source={{uri: property.img_url}} />
        <View style={styles.heading}>
          <Text style={styles.price}>£{price}</Text>
          <Text style={styles.title}>{property.title}</Text>
          <View style={styles.separator}/>
        </View>
        <Text style={styles.description}>{stats}</Text>
        <Text style={styles.description}>{property.summary}</Text>
      </View>
    );
  }
}

render() 方法的第一步,是封装数据。因为从API获得的数据经常不太规范而且某些字段不全。代码采用简单手段让数据变得更便于展示一些。

剩下来的事情就非常简单了,填充组件的状态到UI上。

在文件最后加入export语句:

module.exports = PropertyView;

回到SearchResults.js 在文件头部加入 require 语句:

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

修改 rowPressed() 方法,调用 PropertyView类:

rowPressed(propertyGuid) {
  var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
 
  this.props.navigator.push({
    title: "Property",
    component: PropertyView,
    passProps: {property: property}
  });
}

老规矩:返回模拟器,按下 Cmd+R, 点击搜索结果列表中的某行:

能住得起的房子才是最好的房子——在Pad上看到的这个房子确实很有吸引力!

你的App快接近完成了,最后一步是让用户能够查找距离他们最近的房子。

根据位置查找

在Xcode中打开 Info.plist ,右键,Add Row,增加一个key。 使用NSLocationWhenInUseUsageDescription 作为键名,使用
下列字符串作为键值:

添加完新值后,你的Plist文件将如下图所示:

这将在询问用户是否允许App使用他们的当前位置时,以这串文本作为提示信息。

打开 SearchPage.js, 找到TouchableHighlight 中渲染 ‘Location’ 按钮的代码,加入下列属性值:

onPress={this.onLocationPressed.bind(this)}

这样,当点击Location按钮,会调用 onLocationPressed 方法。

在SearchPage 类中,增加方法:

onLocationPressed() {
  navigator.geolocation.getCurrentPosition(
    location => {
      var search = location.coords.latitude + ',' + location.coords.longitude;
      this.setState({ searchString: search });
      var query = urlForQueryAndPage('centre_point', search, 1);
      this._executeQuery(query);
    },
    error => {
      this.setState({
        message: 'There was a problem with obtaining your location: ' + error
      });
    });
}

navigator.geolocation可获取当前位置。这个方法定义在 Web API中,这对于曾经在浏览器中使用过位置服务的人来说并不陌生。React Native 框架用本地iOS服务重新实现了这个API。

如果当前位置获取成功,我们将调用第一个箭头函数,否则调用第二个箭头函数简单显示一下错误信息。

因为我们修改了plist文件,因此我们需要重新启动App,而不能仅仅是Cmd+R了。请在Xcode中终止App,然后重新编译运行App。

在使用基于地理定位的搜索之前,我们需要指定一个Nestoria数据库中的默认位置。在模拟器菜单中,选择Debug\Location\Custom Location … 然后输入 55.02的纬度和-1.42的经度。这是一个位于英格兰北部的非常优美的海边小镇,我的家。

它远没有伦敦那么繁华——但住起来真的很便宜!:]

接下来做什么

恭喜你,你的第一个React Native App终于完成了!你可以在GitHub上找到每一个”可运行的“步骤的项目源文件,如果你搞不定的时候它们会非常有用的 :]

如果你来自Web领域,你可能觉得在代码中用JS和React框架建立基于本地化UI的App的界面并实现导航不过是小菜一碟。但如果你主要开发的是本地App,我希望你能从中感受到React Native的优点:快速的App迭代,现代JavaScript语法的支持和清晰的CSS样式规则。

在你的下一个App中,你是会使用这个框架,还是会继续顽固不化地使用Swift和O-C呢?

无论你怎么选择,我都希望你能从本文的介绍中学习到一些有趣的新东西,并把其中一些原理应用到你的下一个项目中。

如果你有任何问题及建议,请参与到下面的讨论中来!


收藏 赞 (1) 踩 (0)
上一篇:Swift学习: 从Objective-C到Swift
这篇文章是自己学习Swift的笔记与深化。希望这篇文章能够帮助已经有Objective-C经验的开发者更快地学习Swift。同时也品味到Swift的精妙之处。 结论放在开头: 我认为Swift比Objective-C更优雅,更安全同时也更现代,更性感。 文章组织脉络: 从Objective-C到Swi
下一篇:iOS开发网络篇之文件下载、大文件下载、断点下载
iOS开发中经常会用到文件的下载与上传功能,今天咱们来分享一下文件下载的思路。文件上传下篇再说。 文件下载分为小文件下载与大文件下载 小文件下载 小文件可以是一张图片,或者一个文件,这里指在现行的网络状况下基本上不需要等待很久就能下载好的文件。