微信号:FrontDev

介绍:分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯

深入浅出 React Native:使用 JavaScript 构建原生应用(下)

2015-04-08 20:11 前端大全

(点击 上方蓝字 ,可快速关注我们)

添加组件状态


每个 React 组件都带有一个key-value存储的状态对象,你必须在组件渲染之前设置其初始状态。


在 SearchPage.js 中,我们对 SearchPage 类中,render方法前添加以下的代码。


constructor(props) {

super(props);

this.state = {

searchString: 'london'

};

}


现在你的组件拥有一个状态变量:searchString ,且初始值被设置为 london 。


这时候你需要利用起组件中的状态了。在render方法中,用以下的代码替换TextInput元素中的内容:


<TextInput

style={styles.searchInput}

value={this.state.searchString}

placeholder='Search via name or postcode'/>


这一步设置了 TextInput 组件 value 属性的值,这个值用于把状态变量 searchString 的当前值作为展示给用户的文字。我们已经考虑初始值的设定了,但如果用户编辑这里的文字会发生什么呢?


第一步需要建立一个方法来处理事件。在 SearchPage 类中添加以下的代码:


onSearchTextChanged(event) {

console.log('onSearchTextChanged');

this.setState({ searchString: event.nativeEvent.text });

console.log(this.state.searchString);

}


上面的代码从 events 中取出了 text 属性的值,并用于更新组件的状态。这里面也添加了一些有用的调试代码。


当文字改变时,需要让这个方法被调用,调用后的文字会通过 render 函数返回到组件中。因此我们需要在标签上添加一个 onChange 属性,添加后的标签如下所示:


<TextInput

style={styles.searchInput}

value={this.state.searchString}

onChange={this.onSearchTextChanged.bind(this)}

placeholder='Search via name or postcode'/>


当用户更改文本时,会调用 onChange 上 的函数;在本例中,则是 onSearchTextChanged 。


注意:你估计会对 bind(this) 语句有疑问。在 JavaScript 中,this 这个关键字有点不同于大多数其他语言;在 Swift 表示 “自身”。在这种情况中,bind 可以确保在 onSearchTextChanged 方法中, this 可以作为组件实例的引用。有关更多信息,请参见MDN this页面。


在你再次刷新你的应用程序之前,还有一个步骤:在 return 前添加以下语句,打印一条日志来记录 render() 函数的调用:


console.log('SearchPage.render');


你会从这些日志语句中学到一些很有趣的东西!:]


回到你的模拟器,然后按Cmd + R。您现在应该看到文本输入的初始值为 “london” ,编辑一下文本,从而在 Xcode 控制台中产生一些日志:



注意看上面的截图,日志打印的顺序看起来有些奇怪:


第一次调用 render() 函数用于设置视图。当文本变化时, onSearchTextChanged 函数被调用。之后,通过更新组件的状态来反映输入了新的文本,这会触发另一次 render 。 onSearchTextChanged() 函数也会被调用,会将改变的字符串打印出来。每当应用程序更新任何 React 组件,将会触发整个UI层的重新绘制,这会调用你所有组件的 render 方法。这是一个好主意,因为这样做把组件的渲染逻辑,从状态变化影响UI这一过程中完全解耦出来。


与其他大多数 UI 框架所不同的是,你既不需要在状态改变的时候去手动更新 UI ,或使用某种类型的绑定框架,来创建某种应用程序状态和它的 UI 表现的关联;例如,我的文章中讲的,通过ReactiveCocoa实现MVVM模式。


在 React 中,你不再需要担心 UI 的哪些部分可能受到状态变化的影响;你的整个应用程序的 UI,都可以简单地表示为一个函数的状态。


此时,你可能已经发现了这一概念中一个根本性的缺陷。是的,非常准确——性能!


你肯定不能在 UI 变化时,完全抛弃掉整个 UI 然后重新绘制吧

?这就是 React 高明的地方了。每当 UI 渲染出来后,render 方法会返回一颗视图渲染树,并与当前的 UIKit 视图进行比较。这个称之为 reconciliation 的过程的输出是一个简单的更新列表, React 会将这个列表应用到当前视图。这意味着,只有实际改变了的部分才会重新绘制。


这个令人拍案叫绝的崭新概念让ReactJS变得独特——virtual-DOM(文档对象模型,一个web文档的视图树)和 reconciliation 这些概念——被应用于iOS应用程序。


稍后你可以整理下思路,之后,在刚才的应用中你仍然有一些工作要做。日志代码增加了代码的繁琐性,已经不需要了,所以删除掉日志代码。


初始化搜索功能


为了实现搜索功能,你需要处理 “Go” 按钮的点击事件,调用对应的 API,并提供一个视觉效果,告诉用户正在做查询。


在 SearchPage.js 中,在构造函数中把初始状态更新成:


this.state = {

searchString: 'london',

isLoading: false

};


新的 isLoading 属性将会记录是否有请求正在处理的状态。


在 render 开始的添加如下逻辑:


var spinner = this.state.isLoading ?

( <ActivityIndicatorIOS

hidden='true'

size='large'/> ) :

( <View/>);


这是一个三元操作符,与 if 语句类似,即根据组件 isLoading 的状态,要么添加一个 indicator,要么添加一个空的 view。因为整个组件会不停地更新,所以你自由地混合 JSX 和 JavaSript 代码。


回到用 JSX 定义搜索界面的地方,在图片的下面添加:


{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 设置为对应的值,这样 UI 就可以显示新的状态了。


提示: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 => key + '=' + encodeURIComponent(data[key]))

.join('&');


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

};


这个函数并不依赖 SearchPage,因此被定义成了一个独立的函数,而不是类方法。他首先通过 data 来定义查询字符串所需要的参数,接着将 data 转换成需要的字符串格式,name=value 对,使用 & 符号分割。语法 => 是一个箭头函数,又一个对 JavaScript 语言的扩展,提供了这个便捷的语法来创建一个匿名函数。


回到模拟器,Cmd+R,重新加载应用,点击 “Go” 按钮。你可以看到 activity indicator 显示出来,再看看 Xcode 的控制台:



activity indicator 渲染了,并且作为请求的 URL 出现在输出中。把 URL 拷贝到浏览器中访问看看得到的结果。你会看到大量的 JSON 对象。别担心——你不需要理解它们,之后会使用代码来解析之。


提示:应用使用了 Nestoria 的 API 来做房产的搜索。API 返回的 JSON 数据非常的直白。但是你也可以看看文档了解更多细节,请求什么 URL 地址,以及返回数据的格式。


下一步就是从应用中发出请求。


执行 API 请求


还是 SearchPage.js 文件中,更新构造器中的初始 state 添加一个message 变量:


this.state = {

searchString: 'london',

isLoading: false,

message: ''

};


在 render 内部,将下面的代码添加到 UI 的底部:


<Text style={styles.description}>{this.state.message}</Text>


你需要使用这个为用户展示多种信息。


在 SearchPage 类内部,将以下代码添加到 _executeQuery() 底部:


fetch(query)

.then(response => response.json())

.then(json => this._handleResponse(json.response))

.catch(error =>

this.setState({

isLoading: false,

message: 'Something bad happened ' + error

}));


这里使用了 fetch 函数,它是 Web API 的一部分。和 XMLHttpRequest 相比,它提供了更加先进的 API。异步响应会返回一个 promise,成功的话会转化 JSON 并且为它提供了一个你将要添加的方法。


最后一步是将下面的函数添加到 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.'});

}

}


如果查询成功,这个方法会清除掉正在加载标识并且记录下查询到属性的个数。


注意:Nestoria 有很多种返回码具备潜在的用途。比如,202 和 200 会返回最佳位置列表。当你创建完一个应用,为什么不处理一下这些,可以为用户呈现一个可选列表。


保存项目,然后在模拟器中按下 Cmd+R,尝试搜索 ‘london’;你会在日志信息中看到 20 properties were found。然后随便尝试搜索一个不存在的位置,比如 ‘narnia’,你会得到下面的问候语。



是时候看一下这20个房屋所对应的真实的地方,比如伦敦!


结果显示


创建一个新的文件,命名为 SearchResults.js,然后加上下面这段代码:


'use strict';


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

var {

StyleSheet,

Image,

View,

TouchableHighlight,

ListView,

Text,

Component

} = React;


你肯定注意到啦,这里用到了 require 语句将 react-native 模块引入其中,还有一个重构赋值语句。


接着就是加入搜索结果的组件:


class SearchResults extends Component {


constructor(props) {

super(props);

var dataSource = new ListView.DataSource(

{rowHasChanged: (r1, r2) => r1.guid !== r2.guid});

this.state = {

dataSource: dataSource.cloneWithRows(this.props.listings)

};

}


renderRow(rowData, sectionID, rowID) {

return (

<TouchableHighlight

underlayColor='#dddddd'>

<View>

<Text>{rowData.title}</Text>

</View>

</TouchableHighlight>

);

}


render() {

return (

<ListView

dataSource={this.state.dataSource}

renderRow={this.renderRow.bind(this)}/>

);

}


}


上述的代码里用到了一个特定的组件 – ListView – 它能将数据一行行地呈现出来,并放置在一个可滚动的容器内,和 UITableView 很相似。通过 ListView.DataSource 将 ListView 的数据引入,还有一个函数来显示每一行UI。


在构建数据源的同时,你还需要一个函数用来比较每两行之间是否重复。 为了确认列表数据的变化,在 reconciliation 过程中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方法确保搜索结果全部推进导航栈中,这样你就可以通过 ‘Back’ 按钮返回到根页面。


回到模拟器,按下 Cmd+R 刷新页面,然后试试看我们的搜索。估计你会得到类似下面这样的结果:



耶!你的搜索实现了呢,不过这搜索结果页面的颜值也太低了,不要担心,接下来给它化化妆。


可点击样式


这些 React Native 的原生代码现在应该理解起来轻车熟路了,所以本教程将会加快速度。


在 SearchResults.js 中,destructuring 声明后面添加以下语句来定义样式:


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>

);

}


这个操作修改了返回的价格,将已经格式了化的”300000 GBP”中的GBP后缀删除。然后它通过你已经很熟悉的技术来渲染每一行的 UI 。这一次,通过一个 URL 来提供缩略图的数据, React Native 负责在主线程之外解码这些数据。


同时要注意 TouchableHighlight 组件中 onPress属性后使用的箭头函数;它用于捕获每一行的 guid。


最后一步,给类添加一个方法来处理按下操作:


rowPressed(propertyGuid) {

var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];

}


该方法通过用户触发的属性来定位。目前该方法没有做任何事,你可以稍后处理。现在,是时候欣赏你的大作了。


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



看起来好多了——尽管你会怀疑是否任何人都能承受住在伦敦的代价!


是时候向应用程序添加最后一个视图了。


房产详情视图


添加一个新的文件 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 返回的数据良莠不齐,往往有些字段是缺失的。这段代码通过一些简单的逻辑,让数据更加地规整一些。


render 剩余的部分就非常直接了。它就是一个简单的这个状态不可变状态的函数。


最后在文件的末尾加上如下的 export:


module.exports = PropertyView;


返回到 SearchResults.js 文件,在顶部,require React 的下面,添加一个新的 require 语句。


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


接下来更新 rowPassed(),添加跳转到新加入的 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,一路通过搜索点击一行到房产详情界面:



物廉价美——看上去很不错哦!


应用即将完成,最后一步是允许用户搜索附近的房产。


地理位置搜索


在 Xcode 中,打开 Info.plist 添加一个新的 key,在编辑器内部单击鼠标右键并且选择 Add Row。使用NSLocationWhenInUseUsageDescription 作为 key 名并且使用下面的值:


PropertyFinder would like to use your location to find nearby properties


下面是当你添加了新的 key 后,所得到的属性列表:


你将把这个关键的细节提示呈现给用户,方便他们请求访问当前位置。


打开 SearchPage.js,找到用于渲染 Location 按钮的TouchableHighlight,然后为其添加下面的属性值:


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

当你用手指轻点这个按钮,会调用 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 所定义的接口,所以对于每个在浏览器中使用 location 服务的用户来说这个接口都应该是一致的。React Native 框架借助原生的 iOS location 服务提供了自身的 API 实现。


如果当前位置很容易获取到,你将调用第一个箭头函数;这会向Nestoria 发送一个 query。如果出现错误则会得到一个基本的出错信息。


因为你已经改变了属性列表,你需要重新启动这个应用以看到更改。抱歉,这次不可以 Cmd+R。请中断 Xcode 中的应用,然后创建和运行项目。


在使用基于位置的搜索前,你需要指定一个被 Nestoria 数据库覆盖的位置。在模拟器菜单中,选择 Debug\Location\Custom Location … 然后输入 55.02 维度和 -1.42 经度,这个坐标是英格兰北部的一个景色优美的海边小镇,我经常在那给家里打电话。



警示:我们可以正常地使用位置搜索功能,不过可能有部分同学不能使用(在访问时返回 access denied 错误)—— 我们尚不确定其原因,可能是 React Native 的问题?如果谁遇到了同样的问题并且已经结果,烦请告诉我们。这里没有伦敦那样值得炫耀 —— 不过更加经济!:]


下一步行动?


完成了第一个 React Native 应用呢,恭喜你!你可以下载本教程的完整代码,亲自来试试看。


如果已经接触过 Web 开发了,你会发现使用 JavaScript 和 React 来定义与原生 UI 相连接的接口和导航是多么地容易。而如果你曾经开发过原生 App,我相信在使用 React Native 的过程里你会感受到它种种好处:快速的应用迭代,JavaScript 的引入以及清晰地使用 CSS 定义样式。


也许下次做 App 的时候,你可以试试这个框架?或者说,你依然坚持使用 Swift 或者 Objective-C?无论之后你的选择是怎么样的,我都希望读完这篇文章的你有所收获,还能把这些收获融入到你的项目当中是最好的啦。



原文出处:Colin Eberhardt

译文出处:前端外刊评论

链接:http://zhuanlan.zhihu.com/FrontendMagazine/19996445




1.『前端大全』分享 Web 前端相关的技术文章、工具资源、精选课程、热点资讯,相关职位。欢迎关注。


微信号:FrontDev

(长按上图,弹出“识别二维码”后可快速关注)




 
前端大全 更多文章 5个典型的JavaScript面试题(上) Limu:JavaScript的那些书 Web开发:我希望得到的编程学习路线图 JavaScript基础工具清单 常用排序算法之JavaScript实现
猜您喜欢 终端环境下更好用的文件比较差异工具icdiff 看完这篇文章,我再也不敢说熟练使用Word! 使用ImitateLogin模拟登录百度 How can I make my code faster 到美国找份大数据工作去!《美国大数据工程师面试攻略》