360SDN.COM

React Navtive框架教程

来源:  2017-06-19 20:32:19    评论:0点击:

几个月前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:

  1. brew install node

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

  1. brew install watchman

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

  1. npm install -g react-native-cli

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

  1. 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 ,删除所有内容。然后加入以下语句:

  1. 'use strict';

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

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

然后加入这一句:

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

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

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

然后加入如下语句:

  1. var styles = React.StyleSheet.create({
  2.   text: {
  3.     color: 'black',
  4.     backgroundColor: 'white',
  5.     fontSize: 30,
  6.     margin: 80
  7.   }
  8. });

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

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

这里我们定义了一个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对象。
最后敲入如下代码:

  1. 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的内容:

  1. 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语句为:

  1. 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:

  1. class HelloWorld extends React.Component {

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

  1. class PropertyFinderApp extends React.Component {
  2.   render() {
  3.     return (
  4.  
  5.     );
  6.   }
  7. }

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

  1. var styles = React.StyleSheet.create({
  2.   text: {
  3.     color: 'black',
  4.     backgroundColor: 'white',
  5.     fontSize: 30,
  6.     margin: 80
  7.   },
  8.   container: {
  9.     flex: 1
  10.   }
  11. });

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

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

实现查找

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

  1. 'use strict';
  2.  
  3. var React = require('react-native');
  4. var {
  5.   StyleSheet,
  6.   Text,
  7.   TextInput,
  8.   View,
  9.   TouchableHighlight,
  10.   ActivityIndicatorIOS,
  11.   Image,
  12.   Component
  13. } = 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()方法中用代码写要好一些。
然后加入以下代码:

  1. class SearchPage extends Component {
  2.   render() {
  3.     return (
  4.  
  5.  
  6.           Search for houses to buy!
  7.  
  8.  
  9.           Search by place-name, postcode or search near your location.
  10.  
  11.  
  12.     );
  13.   }
  14. }

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

  1. module.exports = SearchPage;

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

然后需要修改App的导航。

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

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

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

  1. 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 元素后添加:

  1. <View style={styles.flowRight}>
  2.   <TextInput
  3.     style={styles.searchInput}
  4.     placeholder='Search via name or postcode'/>
  5.   <TouchableHighlight style={styles.button}
  6.       underlayColor='#99d9f4'>
  7.     <Text style={styles.buttonText}>Go</Text>
  8.   </TouchableHighlight>
  9. </View>
  10. <TouchableHighlight style={styles.button}
  11.     underlayColor='#99d9f4'>
  12.   <Text style={styles.buttonText}>Location</Text>
  13. </TouchableHighlight>

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

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

  1. flowRight: {
  2.   flexDirection: 'row',
  3.   alignItems: 'center',
  4.   alignSelf: 'stretch'
  5. },
  6. buttonText: {
  7.   fontSize: 18,
  8.   color: 'white',
  9.   alignSelf: 'center'
  10. },
  11. button: {
  12.   height: 36,
  13.   flex: 1,
  14.   flexDirection: 'row',
  15.   backgroundColor: '#48BBEC',
  16.   borderColor: '#48BBEC',
  17.   borderWidth: 1,
  18.   borderRadius: 8,
  19.   marginBottom: 10,
  20.   alignSelf: 'stretch',
  21.   justifyContent: 'center'
  22. },
  23. searchInput: {
  24.   height: 36,
  25.   padding: 4,
  26.   marginRight: 5,
  27.   flex: 4,
  28.   fontSize: 18,
  29.   borderWidth: 1,
  30.   borderColor: '#48BBEC',
  31.   borderRadius: 8,
  32.   color: '#48BBEC'
  33. }

不同样式属性间以逗号分隔,这样你在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 组件下加入:

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

 

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

  1. image: {
  2.   width: 217,
  3.   height: 138
  4. }

由于我们将图片添加到了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()方法前,加入以下代码:

  1. constructor(props) {
  2.   super(props);
  3.   this.state = {
  4.     searchString: 'london'
  5.   };
  6. }

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

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

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

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

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

  1. onSearchTextChanged(event) {
  2.   console.log('onSearchTextChanged');
  3.   this.setState({ searchString: event.nativeEvent.text });
  4.   console.log(this.state.searchString);
  5. }

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

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

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

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

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

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

  1. 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的初始化代码:

  1. this.state = {
  2.   searchString: 'london',
  3.   isLoading: false
  4. };

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

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

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

  1. {spinner}

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

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

在 SearchPage 类中新增如下方法:

  1. _executeQuery(query) {
  2.   console.log(query);
  3.   this.setState({ isLoading: true });
  4. }
  5.  
  6. onSearchPressed() {
  7.   var query = urlForQueryAndPage('place_name', this.state.searchString, 1);
  8.   this._executeQuery(query);
  9. }

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

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

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

  1. function urlForQueryAndPage(key, value, pageNumber) {
  2.   var data = {
  3.       country: 'uk',
  4.       pretty: '1',
  5.       encoding: 'json',
  6.       listing_type: 'buy',
  7.       action: 'search_listings',
  8.       page: pageNumber
  9.   };
  10.   data[key] = value;
  11.  
  12.   var querystring = Object.keys(data)
  13.     .map(key =&gt; key + '=' + encodeURIComponent(data[key]))
  14.     .join('&amp;');
  15.  
  16.   return 'http://api.nestoria.co.uk/api?' + querystring;
  17. };

这个函数不依赖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属性:

  1. this.state = {
  2.   searchString: 'london',
  3.   isLoading: false,
  4.   message: ''
  5. };

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

  1. {this.state.message}

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

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

  1. fetch(query)
  2.   .then(response =&gt; response.json())
  3.   .then(json =&gt; this._handleResponse(json.response))
  4.   .catch(error =&gt; 
  5.      this.setState({
  6.       isLoading: false,
  7.       message: 'Something bad happened ' + error
  8.    }));

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

然后在 SearchPage类中增加方法:

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

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

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

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

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

显示搜索结果

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

  1. 'use strict';
  2.  
  3. var React = require('react-native');
  4. var {
  5.   StyleSheet,
  6.   Image, 
  7.   View,
  8.   TouchableHighlight,
  9.   ListView,
  10.   Text,
  11.   Component
  12. } = React;

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

然后定义一个Componet子类:

  1. class SearchResults extends Component {
  2.  
  3.   constructor(props) {
  4.     super(props);
  5.     var dataSource = new ListView.DataSource(
  6.       {rowHasChanged: (r1, r2) =&gt; r1.guid !== r2.guid});
  7.     this.state = {
  8.       dataSource: dataSource.cloneWithRows(this.props.listings)
  9.     };
  10.   }
  11.  
  12.   renderRow(rowData, sectionID, rowID) {
  13.     return (
  14.  
  15.  
  16.           {rowData.title}
  17.  
  18.  
  19.     );
  20.   }
  21.  
  22.   render() {
  23.     return (
  24.  
  25.     );
  26.   }
  27.  
  28. }

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

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

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

  1. module.exports = SearchResults;

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

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

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

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

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

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

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

表格样式

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

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

  1. var styles = StyleSheet.create({
  2.   thumb: {
  3.     width: 80,
  4.     height: 80,
  5.     marginRight: 10
  6.   },
  7.   textContainer: {
  8.     flex: 1
  9.   },
  10.   separator: {
  11.     height: 1,
  12.     backgroundColor: '#dddddd'
  13.   },
  14.   price: {
  15.     fontSize: 25,
  16.     fontWeight: 'bold',
  17.     color: '#48BBEC'
  18.   },
  19.   title: {
  20.     fontSize: 20,
  21.     color: '#656565'
  22.   },
  23.   rowContainer: {
  24.     flexDirection: 'row',
  25.     padding: 10
  26.   }
  27. });

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

修改renderRow() 方法如下:

  1. renderRow(rowData, sectionID, rowID) {
  2.   var price = rowData.price_formatted.split(' ')[0];
  3.  
  4.   return (
  5.     <TouchableHighlight onPress={() => this.rowPressed(rowData.guid)}
  6.         underlayColor='#dddddd'>
  7.       <View>
  8.         <View style={styles.rowContainer}>
  9.           <Image style={styles.thumb} source={{ uri: rowData.img_url }} />
  10.           <View  style={styles.textContainer}>
  11.             <Text style={styles.price}>£{price}</Text>
  12.             <Text style={styles.title} 
  13.                   numberOfLines={1}>{rowData.title}</Text>
  14.           </View>
  15.         </View>
  16.         <View style={styles.separator}/>
  17.       </View>
  18.     </TouchableHighlight>
  19.   );
  20. }

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

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

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

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

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

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

Time to add the final view to the application.

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

Property Details View

查看房屋详情

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

  1. 'use strict';
  2.  
  3. var React = require('react-native');
  4. var {
  5.   StyleSheet,
  6.   Image, 
  7.   View,
  8.   Text,
  9.   Component
  10. } = React;

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

继续添加如下样式:

  1. var styles = StyleSheet.create({
  2.   container: {
  3.     marginTop: 65
  4.   },
  5.   heading: {
  6.     backgroundColor: '#F8F8F8',
  7.   },
  8.   separator: {
  9.     height: 1,
  10.     backgroundColor: '#DDDDDD'
  11.   },
  12.   image: {
  13.     width: 400,
  14.     height: 300
  15.   },
  16.   price: {
  17.     fontSize: 25,
  18.     fontWeight: 'bold',
  19.     margin: 5,
  20.     color: '#48BBEC'
  21.   },
  22.   title: {
  23.     fontSize: 20,
  24.     margin: 5,
  25.     color: '#656565'
  26.   },
  27.   description: {
  28.     fontSize: 18,
  29.     margin: 5,
  30.     color: '#656565'
  31.   }
  32. });

然后将组件加入视图:

  1. class PropertyView extends Component {
  2.  
  3.   render() {
  4.     var property = this.props.property;
  5.     var stats = property.bedroom_number + ' bed ' + property.property_type;
  6.     if (property.bathroom_number) {
  7.       stats += ', ' + property.bathroom_number + ' ' + (property.bathroom_number > 1
  8.         ? 'bathrooms' : 'bathroom');
  9.     }
  10.  
  11.     var price = property.price_formatted.split(' ')[0];
  12.  
  13.     return (
  14.       <View style={styles.container}>
  15.         <Image style={styles.image} 
  16.             source={{uri: property.img_url}} />
  17.         <View style={styles.heading}>
  18.           <Text style={styles.price}>£{price}</Text>
  19.           <Text style={styles.title}>{property.title}</Text>
  20.           <View style={styles.separator}/>
  21.         </View>
  22.         <Text style={styles.description}>{stats}</Text>
  23.         <Text style={styles.description}>{property.summary}</Text>
  24.       </View>
  25.     );
  26.   }
  27. }

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

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

在文件最后加入export语句:

  1. module.exports = PropertyView;

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

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

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

  1. rowPressed(propertyGuid) {
  2.   var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
  3.  
  4.   this.props.navigator.push({
  5.     title: "Property",
  6.     component: PropertyView,
  7.     passProps: {property: property}
  8.   });
  9. }

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

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

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

根据位置查找

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

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

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

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

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

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

在SearchPage 类中,增加方法:

  1. onLocationPressed() {
  2.   navigator.geolocation.getCurrentPosition(
  3.     location => {
  4.       var search = location.coords.latitude + ',' + location.coords.longitude;
  5.       this.setState({ searchString: search });
  6.       var query = urlForQueryAndPage('centre_point', search, 1);
  7.       this._executeQuery(query);
  8.     },
  9.     error => {
  10.       this.setState({
  11.         message: 'There was a problem with obtaining your location: ' + error
  12.       });
  13.     });
  14. }

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呢?

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

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

 

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权