翻译整理 1ke.co 魔王 文中有改动,以原作为准,不作说明
几个月之前,Facebook发布了React Native,用这个框架,我们就可以用Javascript开发原生iOS程序了(安卓版大概几个月后发布)!3月底,Facebook已经放出了官方repository 。
我们都在用js和HTML5写App,然后用PhoneGap包好发布到iOS(和安卓)了。 React Native真的好用么?
React Native还真就是那么好用。程序猿都为此兴奋无比,主要有以下两个原因:
- 用了React Native, 你的程序尽管用JavaScript写,UI却是本地的,因此无需承受HTML5的UI带来的用户体验损失。
- React 有特别的构建用户界面的技巧,使得界面成为一个叙述软件工作状态的交流窗口。
关键是,React Native是要把React的编程方式带到移动端App开发中。和Java的“开发一次,到处使用”的谎言不同,React主张“学习一次,到处开发”,至少比较现实。本教程暂时只讨论iOS(因为安卓版还没出),但是你看完以后,一定也知道在安卓上怎么用了。
如果你只用Objective-C或者Swift开发程序,可能你不会对用JavaScript开发程序有想法。但是作为Swift的开发者,多一门手艺并不是坏事。
用过Swift的你,一定掌握了更为有效和规范的算法,更好的控制程序的稳定性和可移植性。但实际上,构建UI的方法和Objective-C并没有多大区别:还是在用UIKit而且别无选择。
而我们的React引入了虚拟DOM的概念,将功能性(甚至面向对象)直接带到了UI开发中来。
本教程中,咱们来做一个搜索英国房产并生成列表的软件。
如果你没用过JavaScript也没关系,本会会详解代码。React 用CSS属性来管理样式,很简单,当然如果你有需要的话可以专门去学习一下。
好,我们开始吧。
起步
React Native框架就在GitHub上,你可以克隆一个项目,也可以下载一个zip文件。如果并不想看源代码,也可以用命令行来创建一个React Native项目。本教程中,咱们就这么做。
React使用Node.js,来构建JavaScript代码。如果没有的话需要装上。
先安装Homebrew(本机使用的是MAC,应该不用解释了),然后命令行安装Node.js
brew install node
再安装 watchman,Facebook的一款文件监控程序。
brew install watchman
有了这个React Native就可以监察文件的变化并重建。就好比每次保存都用Xcode来build一次。
接下来就可以用npm安装 React Native CLI(命令行工具)。
npm install -g react-native-cli
这就用Node的包管理工具在全局安装了CLI。这方面npm有点像CocoaPods 或者Carthage。
创建一个项目目录,然后进入这个目录:
react-native init PropertyFinder
这就创建了一个叫做PropertyFinder(找房产)的初始项目,里面包含了构建和运行 React Native 程序所需的所有内容。
进入这个目录,你会找到一个node_modules文件夹,这里面放着React Native框架。
同时还会找到一个 index.ios.js文件,这就是CLI创建的程序文件的骨架。
最后,还会看到一个Xcode项目文件和iOS目录,包含了启动程序必须的相关文件。
下面我们build和运行一下这个程序:
同时我们会注意到一个窗口跳出来:
=============================================================== | 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 Native的包管理程序,在Node下面运行,咱们就知道这是干什么的了。
不要关掉这个窗口,让它在后台自己运行。如果不小心关掉了,重新运行Xcode项目就行。
注意:我们等一下要写大量的JavaScript代码,最好选一个轻量级的编辑器,推荐Sublime Text,当然你自己可以在网上找找。
Hello React Native
在做房产搜索App之前,咱们先做一个Hello World程序。我们在这里会接触到很多重要的组件。
在编辑器中打开index.ios.js,删掉全部内容,然后写下这么一行:
'use strict';
启用Strict模式可以增强JavaScript的除错能力并关掉一些不好的功能。
然后加上下面这行:
var React = require('react-native');
这就把React Native组件调用起来,并赋予React变量。其中require功能和Node.js是一样的。
接下来,加上下面这些:
var styles = React.StyleSheet.create({
text: {
color: 'black',
backgroundColor: 'white',
fontSize: 30,
margin: 80
}
});
这段代码添加了文字的样式。如果你干过web开发,你一定知道这就是CSS。
好了,下面真的是app程序了。
class PropertyFinderApp extends React.Component {
render() {
return React.createElement(React.Text, {style: styles.text}, "Hello World!");
}
}
看到了吗?一个JavaScript类(Class)!
类从ECMAScript 6 (ES6)被引入JavaScript。由于JavaScript不断在改进,web开发人员经常因为浏览器兼容性(万恶的IE)而不得不放弃一些功能。React Native有一个JavaScriptCore,因此无需为浏览器兼容性操心。
注意:如果你是web开发者,我建议先从新的JavaScript标准出发,然后使用Babel之类的工具兼容旧版浏览器。
PropertyFinderApp 继承 React.Component,React UI的基础模块。Component(组件)包含了不可变的property(属性变量),可变的state(状态变量),并提供一个render(渲染)方法。
React Native 组件不是UIKit类,而是轻量级的。框架会将React组件树转换为本地UI。
最后我们加上这行:
React.AppRegistry.registerComponent('PropertyFinder', function() { return PropertyFinderApp });
AppRegistry 定义了程序的入口变量,并提供了根组件。
保存好index.ios.js,返回Xcode,确认把PropertyFinder 加进iPhone模拟器,我们重建并运行一下项目。
这可是本地UI,没有任何浏览器,代码全是JavaScript写的哦!
还是不信?:)打开Xcode的Debug\View Debugging\Capture View Hierarchy,是不是哪都找不到UIWebView ,只有一个真正的View!牛啊。
你一定很好奇是怎么回事。在Xcode中找到application:didFinishLaunchingWithOptions:,这个方法创建了一个RCTRootView,来载入JavaScript并渲染了View。
程序一运行,RCTRootView 立刻读取:
http://localhost:8081/index.ios.bundle
来加载程序。
现在知道前面弹出来那个窗口是干什么的了吧,就是处理这个东西的。
程序启动时,代码会载入JavaScriptCore 框架并执行。在这个程序中,PropertyFinderApp 这个component会被加载并创建一个本地的UIKit视图。后面会详细讲到。
Hello JSX
我们现在的程序使用 React.createElement创建,然后React转换为本地文件。尽管目前我们的js文件很清楚,但随着代码更复杂,各种元素的嵌套会导致代码一团乱麻。
在app继续运行的情况下,我们回到index.ios.js,把return改一改:
return Hello World (again);
这是JSX代码,全称叫做JavaScript syntax extension,使用类似HTML的标签。如果是web开发者,一看就懂。我们全程会大量使用JSX代码。
保存文件返回模拟器,按下Cmd+R,我们可以看到“Hello World (again)”,看到没,重新加载React Native程序就跟刷新一下网页一样简单。
由于我们的程序使用的是同一套JavaScript文件,以后只需要修改index.ios.js并保存,就可以实现内容的调整。
好了,玩够了Hello World,我们来做真正的程序吧。
添加导航控制
我们的寻找房产程序使用和UIKit类似的栈形导航。下面来做这个导航。
在index.ios.js中,我们把PropertyFinderApp 重命名为HelloWorld。
class HelloWorld extends React.Component {
Hello World还是会显示,不过已经不是咱们的根组件了。
我们在下面加一点真货:
class PropertyFinderApp extends React.Component {
render() {
return (
);
}
}
这样就添加了一个导航控制器,使用了原本HellowWorld组件的路径和样式。在web开发时,routing(路由或路径)被用来设计程序的导航结构,根据URL来分配页面。
然后我们要调整样式:
var styles = React.StyleSheet.create({
text: {
color: 'black',
backgroundColor: 'white',
fontSize: 30,
margin: 80
},
container: {
flex: 1
}
});
回到Xcode再刷新一次。
好了现在我们有了导航框架,可以开始在里面做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。它还可以用来操作数组,非常好用。
我们还要添加样式:
var styles = StyleSheet.create({
description: {
marginBottom: 20,
fontSize: 18,
textAlign: 'center',
color: '#656565'
},
container: {
padding: 30,
marginTop: 65,
alignItems: 'center'
}
});
虽然用css没有Interface Builder那么可视化,但比用viewDidLoad()一个一个设置视图属性可方便多了。:)
下面来写组件:
class SearchPage extends Component {
render() {
return (
Search for houses to buy!
Search by place-name, postcode or search near your location.
);
}
}
render 可以完美应用JSX的结构。用css我们很容易就实现了这个图形界面:一个容器和两条label。
最后加上:
module.exports = SearchPage;
这条命令制定了输出SearchPage 以为他用。
下一步就是路由(Routing)的制作。
打开index.ios.js,并且在require下面加上这行:
var SearchPage = require('./SearchPage');
在PropertyFinderApp 的render里面替换initialRoute :
component: SearchPage
现在可以干掉Hello World这种新手用的东西了,现在我们是高手了。
回模拟器我们刷新一下Cmd + R:
这就是我们的新组件,搜索页SearchPage。
用Flexbox来布局
现在,我们用css调整了margin、padding和颜色,不过你可能对flexbox并不熟悉,这是最近出现的新规范,用来做app的UI布局再好不过了。
React Native用的是css-layout库,是flexbox标准的一种javascript实现,可以转化为C(iOS)和Java(安卓)。
在App中,默认的排列是依据column(列),所以,子元素默认是垂直排列的。
这个被叫做主轴(main axis),可以竖着也可以横着。
每个子元素的垂直位置由margin、height和padding决定。容器同时将alignItems 设置为center,所以文档是居中的。
好了,现在要加上input和button了。打开SearchPage.js并将下面这段插入到第二段text元素后面:
Go
Location
现在我们有两个顶级视图了,一个有一个输入框和按钮,一个只有按钮。
下面我们把样式加进style里面:
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'
}
每项属性中用“,”隔开。这些就是输入框和按钮的样式。
我们会模拟器刷新一下 Cmd + R。
输入框和Go按钮在同一行,因为把它们放在了flexDirection: 'row'的容器中。我们并没有明确声明它们的宽度,而是给了一个flex值。输入框flex: 4,而按钮则是 flex: 1,所以它们的宽度比为4:1.
你可能已经注意到,这个按钮……并不是按钮。在UIKit中,按钮只不过是可以点击的Label,所以React Native团队决定直接用JavaScript构建按钮。程序中的按钮使用TouchableHighlight这个组件,使得按钮按下时显示透明和不同的颜色。
最后我们添上一些图片。可以在这里下载。在Xcode中,打开 Images.xcassets,点击加号添加图片。接下来鼠标拖动图片到相应的格子。
这次需要停掉程序并重启,来让图片加载出现。
将下面这行添加到location按钮TouchableHighlight 组件的下面:
下面添加图片的样式:
image: {
width: 217,
height: 138
}
上面的require('image!house') 是为了获取图片的位置。在Xcode中,如果打开Images.xcassets的话,我们会找到房屋图标。
返回模拟器刷新一下页面,Cmd + R。
注意:如果看不到图片而显示“image!house”的话,重启包管理器(或者命令行npm start)
添加组件state(状态参数)
每一个React组件都有其state状态参数,用来储存关键变量。组件初始化之前要先初始化state。
在SearchPage.js中,在render()上面添加如下代码:
constructor(props) {
super(props);
this.state = {
searchString: 'london'
};
}
我们的组件现在有了一个state变量,名为searchString值为’london’。
然后就可以使用组件的状态了。在Render中,改变TextInput如下代码:
我们把输入框的值——也就是用户看到的那个——和我们的state值searchString 绑定起来了。
好了,值是初始化好了,但是,当用户输入时,会发生什么呢?(自己去试试)
我们要绑定一个事件,在SearchPage中添加如下方法:
onSearchTextChanged(event) {
console.log('onSearchTextChanged');
this.setState({ searchString: event.nativeEvent.text });
console.log(this.state.searchString);
}
这样,系统就会取出输入框的值并且替换state的值。
下面要把这个方法串进输入框中,要将函数绑定到onChange属性上:
这样每次用户在输入框中输入,都会触动onChange,并执行onSearchTextChanged。
注意:如果不明白bind(this)方法是干什么的,JavaScript处理this关键字与其他语言不太一样,用Swift翻译就是self。用bind是为了确保this指向组件实例。可以自己去试试就明白了。
最后,把下面这些记录函数添加到render中,return上面:
console.log('SearchPage.render');
这样我们就会明白一些非常神奇的东西。
现在刷新一下程序。我们应该可以看到界面中的”london”,并且在命令行中,我们看到:
嗯……顺序似乎有点问题。
- 首次启动render
- 当输入框有变化,启动onSearchTextChanged()
- 更新state,并引发另一次render
- onSearchTextChanged()并装入新的搜索内容
每次任何state发生变化,React组件会将整个UI全部重新render一遍,这是好的,这就解耦了state与UI状态。
在其他的UI框架中,要么你手动更改UI的内容,要么进行双向绑定,比如某些MVVM模式的框架。
在React中,你无需担心哪块UI绑到什么地方,整个UI都是state的一个体现。
看到这里,你可能已经发现这个模式的缺点了——性能问题!
我们总不能每次state有点改动,就把整个UI全部扔掉,然后重新加载吧?好了React在这个问题上相当智能化。每次UI渲染时,都会把render方法返回的视图树,拿去与现存的UIKit视图树作对比。于是对比的结果,就是需要更改的视图树。所以只有改变了的部分需要重新渲染。
看到这里我是非常佩服React.js的虚拟DOM给iOS带来的如此特别的合作方式。
我们把上面那些日志方法全部删掉,下面我们开始完善程序。
启动搜索功能
要实现搜索功能,我们要处理Go按钮的按下事件,并发出API请求,同时视觉上告诉用户请求正在进行。
在 SearchPage.js中,更改初始化state:
this.state = {
searchString: 'london',
isLoading: false
};
在render中增加如下内容:
var spinner = this.state.isLoading ?
( ) :
( );
根据isLoading这个state是否为真,显示活动视图或空视图。
在搜索UI的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只是写一条日志,将来会完善的。
注意: 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来储存参数,然后构建查询请求。其中,=>这是个新语法,(参数)=>{函数体}。
回到模拟器,按下Cmd + R刷新,点击Go,就会看到滚动图标,然后我们看看后台。
我们会看到url,复制下来贴进浏览器,我们来看看结果,会看到一大段JSON内容。现在还不用管这些。
注意:本教程使用了 Nestoria API 来获取房产信息。相关细节可以参考网站文档。
下一步,就是在App内发起请求了。
发起API请求
还是在SearchPage.js,添加一个message变量。
this.state = {
searchString: 'london',
isLoading: false,
message: ''
};
在render中,在底部添加一行:
{this.state.message}
这个可以用来显示信息给用户看。
在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.'});
}
}
拿掉了isLoading并且做了日志记录。
保存起来,然后回模拟器Cmd + R。我们搜索london,应该会看到20条记录在日志里面。然后试一下随便输一个’ narnia’,你会看到如下情况:
显示结果
创建一个SearchResults.js,并添加代码如下:
'use strict';
var React = require('react-native');
var {
StyleSheet,
Image,
View,
TouchableHighlight,
ListView,
Text,
Component
} = React;
这一段,看是很眼熟吧?
然后是组件本身:
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 (
{rowData.title}
);
}
render() {
return (
);
}
}
上面的代码用了一个ListView 组件,用于在可滚动的容器内添加多行内容,有点像UITableView。数据的来源是ListView.DataSource。
构建数据源时,需要提供一个函数来对比一对数据。ListView在组建的过程中会调用这个函数,用于编制列表。这次的回传数据中,Nestoria API提供guid ,就刚好适合这个目的。
别忘了添加输出模块代码:
module.exports = SearchResults;
讲一下这段添加到SearchPage.js中,require下面:
var SearchResults = require('./SearchResults');
这样我们就可以在SearchPage 中调用SearchResults 了。
编辑_handleResponse 方法:
this.props.navigator.push({
title: 'Results',
component: SearchResults,
passProps: {listings: response.listings}
});
上面的代码导向SearchResults 页面,并且将结果列表传进去。用push方法将搜索结果传入导航,这样就可以用回退功能返回之前页面。
返回模拟器并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 (
this.rowPressed(rowData.guid)}
underlayColor='#dddddd'>
£{price}
{rowData.title}
);
}
这次,图片使用URL,React Native会负责从主线程中解析。而且rowData.grid使用=>方法来获取。
最后一步是处理按下:
rowPressed(propertyGuid) {
var property = this.props.listings.filter(prop => prop.guid === propertyGuid)[0];
}
刷新一下,我们可以看到:
看起来很不错。但是,这个房价根本没人买的起吧。
我们准备将最后一个视图做出来。
房产详情视图
新建一个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 (
£{price}
{property.title}
{stats}
{property.summary}
);
}
}
render的第一部分对数据做了一些处理,因为很多情况下数据是不完整的。
最后别忘了输出:
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}
});
}
刷新一下,你也懂的。
最后一步:让用户搜索附近的房子。
基于地理位置的搜索
在Xcode中,打开 Info.plist并添加新key,右键点击编辑期内并选择Add Row。用NSLocationWhenInUseUsageDescription 作为键名,以下作为键值:
PropertyFinder would like to use your location to find nearby properties
以下是完成的plist :
打开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,React有自己的一套实现,用的是iOS原生的位置服务。
如果成功获取现在的位置,系统会向Nestoria拉取数据,如果失败,就会显示错误信息。
由于改变了plist,这次我们就不能Cmd +R了。重新建立工程并启动吧。
在启动位置搜索服务前,我们必须指定一处Nestoria 数据库覆盖的地方。作为测试,我们在模拟器中选择Debug\Location\Custom Location,输入北纬55.02,经度-1.42,这是北英格兰一处安静的港口。
今后的路
恭喜你,做出了第一个React Native程序!源代码可以在Github上找到!
如果你是web开发的世界过来的,你会发现用javaScript和React做一个本地iOS(几个月后安卓)App是多么简单的事情。
也许你下一个App就用这套程序写了?还是说你会坚持用Objective-C或者Swift?不论如何,还是希望本篇文章让你有所收获,并且在下一个项目中有新的理念。
如果有任何疑问,欢迎到1ke.co上讨论,或者到原作者Colin Eberhardt论坛提问。本文完成翻译时(2015年6月13日),Andoid版的发布日期暂定在2015年10月。我们拭目以待。