React Native 简单教程
作为一个 JavaScript 开发者,我之前从来没想过用 JavaScript 很容易地写原生移动应用。当然,我们已经有了如 PhoneGap 等工具,但在原生应用中封装一个基于浏览器的应用还有许多需要改进的地方。
现在这一切都改变了—— 非死book 的 React 团队发布了 React Native。它不仅可以让我们使用 React 框架来使用原生移动组件创建应用程序,但它使一切成为了现实——这意味着我们在开发应用时不需要重新编译——这使得它非常容易地创建移动应用!我有幸预览了 React Native 的 beta 版本,从那开始它大规模成长了起来。
请注意,目前已经支持 iOS。因此你需要运行 OS X 上的 Xcode 来跟随本教程。
如果你还没有过机会学习 React, 看看我的教程 来开始用用它吧.
要重点注意学习这个并不意味着我们可以写一次代码就能将这段代码用到每一个地方。尝试那样做会因为疯狂的抽象级别而陷入一场灾难。React Native 则让我们可以学习一次,到处编写。
回到 2004
如果你关注社交网络领域的话你会记得 FaceMash,正式这个应用开创了 非死book。对于不关注这个领域的人,其实是11年前(哇塞) Mark Zuckerberg 创建了 FaceMash,它是一个你可以用来查看两个人之中谁更加热门的应用程序。每一个人都有一个能反映他们有多“热门”的分数值 (尽管不知道原来使用的是什么算法,不过那部电影(社交网络)显示 Elo 排名算法 曾被使用过) .
它全部的荣耀都在于此 -
让我们整个来过一遍吧 - 我们准备用 React Native 来重新创建 FaceMash。如果你觉得凭外貌来评价姑娘们不道德,你可以把图片变成你觉得能吸引人的其它事务(狗狗,代码块,等等,我不做评价),随便。
创建你的应用
如你所愿,你可以从 这儿 clone 到初始的代码库。这不是必须的,不过为了不让你错过不同阶段代码的不同分支,你可以 clone 一份下来!
休斯顿,我们已经升空
如果你没有 clone 代码库,就需要设置基础项目. React Native 可以让我们使用 react-native-cli npm 包 CLI 快速开始一个项目。如果你还没有安装这个,可以快速运行命令:
npm install -g react-native-cli
然后我们就可以开始了.
在终端里导航到一个文件夹并运行命令:
react-native init FaceMash
这样做能为我们准备好基础到应用程序,供我们挖掘和加入更多东西.
打开它
打开 XCode 并浏览到你创建了应用程序的目录里面. 你需要从这里打开 facemash.xcodeproj。
React Native 支持我们在 iOS 模拟器和实际的 iOS 设备上工作.
我将会在 iOS 模拟器上面进行开发,因为它运行更多快速的应用程序开发 - 当我们修改了JavaScript 时,可以按下 Command + R 组合建来刷新应用,或者我们也可以通过 developer 菜单(通过 Command + Control + Z 就能访问到)启用动态重新载入来变成超级懒人。我们设置可以在Chrome的开发者工具中调试我们的代码。
如果你希望使用你的 iOS 设备来开发你的应用程序,就需要让设备痛你的计算机处于同一个网络中。React 默认会在 localhost 找到 JavaScript,所以就需要你将它指向你的计算机.
我们可以通过编辑 AppDelegate.m 文件,将 localhost 改成我们的本地 IP 来达成这个目的. 你可以通过按下 Alt 的同时点击 wireless 菜单 在 OS X 来找到这个东西.
现在就可以运行我们的应用程序了。应用程序会在你在 XCode 中选择的目标中打开. 当我们点击运行,同时会产生一个在我们应用程序目录中运行着 npm start 的终端线程. 如果你不希望通过 XCode 运行应用,确保你运行了 npm start。这将会创建一个在端口8081上的本地 web 服务器,它指向我们编译好的 JavaScript 代码,并且也会监视到我们保存代码的动作以进行重新编译。
我把应用程序运行在一个模拟的 iPhone6 之上, 屏幕是真实设备的50%那么大.
这就是了,我们有了一个空的 canvas,有好多空间活动啊!
悄悄来看一看
让我来看看拿来渲染我们可以在上面的截屏中所看到的东西的代码. 打开 index.ios.js.
/** * Sample React Native App * https://github.com/非死book/react-native */ 'use strict'; var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, } = React; var facemash = React.createClass({ render: function() { 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+Control+Z for dev menu </Text> </View> ); } }); var 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('facemash', () => facemash);
你可以合上你惊讶的嘴了 - 是的,这就是我们拿来渲染我们的应用程序的全部东西。看起来熟悉,对不对?
React Native vs 浏览器
不是 React Native 的所有东西都能满足你在浏览器中使用React的用途. 不过,两者之间的区别是如此的微不足道,所以完全没有必要担心它们.
-
不使用诸如 div 活着 section 之类的块元素, 我们在React中使用的是View组件. 它会映射到原生的 iOS 组件 UIView.
-
所有的文本都必须被封装到 Text 组件里面。
-
没有样式表 - 你的所有的样式都是被写成 JavaScript 对象的。
-
我们没有必要担心浏览器的兼容性问题 - ES6 harmony 是在盒子之外受到支持的,flexbox也是如此。
开始工作
我们准备从清理 React 组件的样式表盒渲染函数开始。为了对 React Native 有一个理想的基本了解,我们将尝试使用尽可能多的不同组件。
让我们先从 TabBarIOS 组件开始. 你也许能认出 TabBar 组件来,它被用在诸如时钟和照片这样一些核心的iOS应用中。
var React = require('react-native'); var { AppRegistry, StyleSheet, Text, View, TabBarIOS } = React; var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, render: function() { return ( <TabBarIOS> <TabBarIOS.Item title="FaceMash" icon={ require('image!facemash') } selected={ this.state.selectedTab === 'faceMash' }> <View style={ styles.pageView }> <Text>Face Mash</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Messages" icon={ require('image!messages') } selected={ this.state.selectedTab === 'messages' }> <View style={ styles.pageView }> <Text>Messages</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Settings" icon={ require('image!settings') } selected={ this.state.selectedTab === 'settings' }> <View style={ styles.pageView }> <Text>Settings</Text> </View> </TabBarIOS.Item> </TabBarIOS> ); } }); var styles = StyleSheet.create({ pageView: { backgroundColor: '#fff', flex: 1 } }); // omitted code
看看这个!你会注意到当前的文本覆在了状态条上面,我们稍后会修复这个问题。
TabBarIOS 组件对它的每一个子项都使用了 TabBarIOS.Item。我们将会有三个页面——分别是你给人们评级的页面,一个消息列表以及一个设置的页面。
TabBarIOS.Item 必须有一个子项。他将会是已经被选取的页面的内容(你可以发现我们会根据组件的状态来选择设置成true还是false)。
很明显,一个 TAB 条没有图标不会好看。有几个系统图标是你可以拿来用的,不过如果你用了他们的话,TAB 的文字也会发生变化,以与系统的图标配对. 所以我们会使用自己的图标。为了在 React Native 中引入本地的图片资源,你可以使用 require 后面带上图片的资源名称!
我使用的图标是可以免费拿来用的,来自于 flaticon 的 CC 3.0 许可.
使用静态图片
为了向 React Native 添加静态图片,请打开 XCode。在 Project Navigator (左手边的第一个图标)中, 打开 Images.xcassets 。你所有的图片都在那儿。
这可以让我们将所有的资源保持在同一个名称下,这样可以针对每一个分辨率、甚至是设备的特定图片提供不同的图像资源。
图像必须遵循一个严格的命名约定。使用的资源名称(比如 messages 或者是 settings) 并在后面给它带上它应该适用来显示的分辨率。例如,我要为 iPhone6 构建一个应用程序,我会为此使用 @2x 分辨率。
一旦为你的图片进行了正确的命名,就可以将它拖入左手边的 Images.xcassets 中了。
然后你就可以在 React Native 中使用 require('image!assetname') 了!
回到代码
下一个逻辑步骤就是设置我们的主组件使得 Tab 之间的切换可用。我们可以通过设置用户点击它时的状态来做到。TabBarIOS.Item 让我们可以给它一个 onPress 属性,可以拿来检测用户何时按下了一个tab。
// omitted code var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, changeTab(tabName) { this.setState({ selectedTab: tabName }); }, render: function() { return ( <TabBarIOS> <TabBarIOS.Item title="FaceMash" icon={ require('image!facemash') } onPress={ () => this.changeTab('faceMash') } selected={ this.state.selectedTab === 'faceMash' }> <View style={ styles.pageView }> <Text>Face Mash</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Messages" icon={ require('image!messages') } onPress={ () => this.changeTab('messages') } selected={ this.state.selectedTab === 'messages' }> <View style={ styles.pageView }> <Text>Messages</Text> </View> </TabBarIOS.Item> <TabBarIOS.Item title="Settings" icon={ require('image!settings') } onPress={ () => this.changeTab('settings') } selected={ this.state.selectedTab === 'settings' }> <View style={ styles.pageView }> <Text>Settings</Text> </View> </TabBarIOS.Item> </TabBarIOS> ); } }); // omitted code
可以了!它是多么的简单. 通过在 iOS 模拟器中按下 Command+R 来刷新应用(或者如果你是在真实设备上开发,可以通过 XCode 来对它进行重新编译) 你就会看到现在我们可以进行按下 tab 的操作了,并且主屏幕的显示也发生了变化!
尽管我们还没有写太多的代码,但是已经见第一个步骤分支的代码 checkout 出来了,里面也包含了我们在这个 tab 上用上了的图标。
让我们打分吧
让我们来实现 FaceMash 的 tab 界面吧。我们将会从一个端点那里使用获取来加载到数据。在步骤一的分支中,我已经在 rest/ 目录中包含进来了一个 config.yaml 文件,那是我们将会用来使用 stubby 对端点进行模拟的。所有 endpoint/pictures 中的用户都会被从 randomuser.me 处随机的生成。
打开你的终端并且运行命令
stubby -d rest/config.yaml
接着我们就开始吧!
在名为 tabs/ 的目录中创建一个新文件,命名为 FaceMash.js,在里面放一个基础的 React 组件 -
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View } = React; var facemashTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <Text> FaceMash tab! </Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } }); module.exports = facemashTab;
目前我们能从这个Tab上得到全部就是一个里面有一些文字的基础的 View 组件。我们还可以为这个 View 弄一些基础的样式,这样可以确保它具有合适的高和宽。
我们会添加一个头部,纯粹是用于展示的目的.
// omitted code var facemashTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <View style={ styles.header }> </View> <View style={ styles.content }> <Text> FaceMash tab! </Text> </View> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, header: { height: 40, background: '#ff0000' } }); module.exports = facemashTab;
现在我们会抱怨状态条的黑色很糟糕,不过不要担心,因为我们可以使用 StatusBarIOS 的 API 来对其进行修改。当 changeTabfunction 被调用时,我们可以检查看看当前的 tab 是不是 FaceMash 的 tab。如果是的话,我们将会把状态调的状态设置为1(白色),如果不是就设置为0(黑色).
index.ios.js
// omitted code var { AppRegistry, StyleSheet, Text, View, TabBarIOS, StatusBarIOS } = React; var facemash = React.createClass({ ..., changeTab(tabName) { StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0); this.setState({ selectedTab: tabName }); }, ... }); // omitted code
刷新你就会看到一个白色的状态条 - 解决了!
我们现在可以访问端点来向我们的用户进行展示了。我们将会使用 fetch,它在 React Native 中默认是被包含了进来的。
// omitted code var facemashTab = React.createClass({ getInitialState: function() { return { list: [], currentIndex: 0 }; }, componentWillMount: function() { fetch('http://localhost:8882/rest/mash') .then(res => res.json()) .then(res => this.setState({ list: res })); }, render: function() { return ( ... ); } }); // omitted code
请求会用返回的数据对我们的状态进行填充。因为初始的数据时一个空的数组,所以我们可以在 render 函数中进行检查,在他们等待的时候显示一个加载页面。
var { StyleSheet, Text, View, ActivityIndicatorIOS } = React; var facemashTab = React.createClass({ ..., render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { contents = ( <View style={ styles.content }> <Text>Loaded</Text> </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, loading: { flex: 1, backgroundColor: '#fff', justifyContent: 'center', alignItems: 'center' }, loadingText: { fontSize: 14, marginBottom: 20 }, header: { height: 50, backgroundColor: '#760004', paddingTop: 20, alignItems: 'center' }, headerText: { color: '#fff', fontSize: 20, fontWeight: 'bold' } });
现在我们将对位于 this.state.list 的数据进行访问。我们也会在端点返回一个对象的数组时,得到位于状态中的数组的当前索引 - 每个对象都是用户可以进行评比的两个人.
因为要从两个人中选一个,两者都有同自身相关联的相同数据,我们将创建一个 React 组件来展示他们的数据。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Text>Person!</Text> </View> ) } }); var facemashTab = React.createClass({ getInitialState: function() { return { list: [], currentIndex: 0 }; }, componentWillMount: function() { fetch('http://localhost:8882/rest/mash') .then(res => res.json()) .then(res => this.setState({ list: res })); }, render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { var { list, currentIndex } = this.state; var record = list[currentIndex]; var people = record.users.map(person => <Person person={ person } />); contents = ( <View style={ styles.content }> { people } </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } }); var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' } });
我们现在就有了一个进行两次数据装入(每个人一次)的组件,合适的配置会向它进行传递。现在就可以将个人资料图片和相关的用户信息展示出来了。
展示外部的图片
不同于我们的 tab 图标,我们用来展示的每一个用户的图片都来自一个外部的源. 这不是问题,事实上展示它们要比展示静态资源更加简单.
我们只是向 Image 组件传递一个对象,而不是向它传入一个需要的图片. 这个对象会有一个属性—— url,它会指向我们想要加载的图片.
当我们将用户信息作为一个叫做person的属性进行传递时,我们可以通过 this.props.person.picture 访问到图片的 URL。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> </View> ) } }); // omitted code var styles = StyleSheet.create({ ... person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personImage: { flex: 1, height: 200 }, ... }); module.exports = facemashTab;
这里也还有一些必要的样式 - 重新设置图片的大小难以置信的简单. 类似的 CSS 属性,比如 background-size,可以在 React Native 中被应用到图片之上, 而这里我们智慧在上面放一个 height,而图片会据此对尺寸进行重新设置.
现在我们可以将剩下的用户信息添加进去了。
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </View> ) } }); // omitted code var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, personImage: { flex: 1, height: 200 }, personName: { fontSize: 18, flex: 1, paddingLeft: 5 }, personScore: { flex: 0.25, alignItems: 'center' }, personScoreHeader: { color: 'rgba( 0, 0, 0, 0.3 )', fontSize: 10, fontWeight: 'bold' }, personScoreValue: { color: 'rgba( 0, 0, 0, 0.6 )', fontSize: 16 }, won: { color: '#93C26D' }, lost: { color: '#DD4B39' } }); module.exports = facemashTab;
你可以从分支二检出到目前这儿为止的代码。
现在我们已经让用户显示了出来,可以着手加入点击时间来让用户选择出谁比较热门了。
手指触击
React Native 为我们提供了 TouchableHighlight 组件. 它能让我们的View组件正常的响应触摸. 当它被触摸时,被封装视图的透明度降低了. 这就让我们的组件“感官上”是可以触摸的了.
我们准备用这个东西封装个人信息部分. 将来我们可能想要创建它来让用户可以在上面点击,从而看到更多有关那个人的图片.
// omitted code var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight> <View style={ styles.personInfo }> <Text style={ styles.personName }> { person.firstName } { person.lastName } </Text> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> WON </Text> <Text style={ [styles.personScoreValue, styles.won] }> { person.won } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> LOST </Text> <Text style={ [styles.personScoreValue, styles.lost] }> { person.lost } </Text> </View> <View style={ styles.personScore }> <Text style={ styles.personScoreHeader }> SCORE </Text> <Text style={ styles.personScoreValue }> { person.score } </Text> </View> </View> </TouchableHighlight> </View> ) } }); // omitted code
当你重新载入我们所做的修改并且在用户信息上点击,会发现它起作用了 - 但看起来有点糟糕. 这是因为我们还没有在视图上设置一个背景颜色,其意义是让整个组件变暗.
// omitted code var styles = StyleSheet.create({ ..., person: { flex: 1, margin: 10, borderRadius: 3, overflow: 'hidden' }, personInfo: { backgroundColor: '#fff', borderLeftColor: 'rgba( 0, 0, 0, 0.1 )', borderLeftWidth: 1, borderRightColor: 'rgba( 0, 0, 0, 0.1 )', borderRightWidth: 1, borderBottomColor: 'rgba( 0, 0, 0, 0.1 )', borderBottomWidth: 1, padding: 10, alignItems: 'center', flexDirection: 'row' }, ... }); // omitted code
现在当你在信息盒子上点击时,它就能正确工作了!
TouchableHighlight 为我们提供了 TouchableWithoutFeedback 也有的一个相同的事件。TouchableWithoutFeedback 不应该被使用,因为所有被触摸的东西都应该提供某些类型的视觉上可见的反馈。
这样我们就可以利用 onPress - 它会在用户已经释放了触摸,但是还没有被打断 (比如还在让他们的手指在可触摸的区域移动)时被调用。
我们需要向下将一个属性传递到我们的 Person 组件,当其被触摸到时。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ActivityIndicatorIOS, TouchableHighlight } = React; var Person = React.createClass({ render: function() { var person = this.props.person; return ( <View style={ styles.person }> <Image style={ styles.personImage } source={ { uri: person.picture } } /> <TouchableHighlight onPress={ this.props.onPress }> ... </TouchableHighlight> </View> ) } }); var facemashTab = React.createClass({ ..., onPersonPress: function() { this.setState({ currentIndex: this.state.currentIndex + 1 }); }, ..., render: function() { var contents; if (!this.state.list.length) { contents = ( <View style={ styles.loading }> <Text style={ styles.loadingText }>Loading</Text> <ActivityIndicatorIOS /> </View> ) } else { var { list, currentIndex } = this.state; var record = list[currentIndex]; var people = record.users.map(person => <Person person={ person } onPress={ this.onPersonPress } />); contents = ( <View style={ styles.content }> { people } </View> ) } return ( <View style={ styles.container }> <View style={ styles.header }> <Text style={ styles.headerText }>FaceMash</Text> </View> { contents } </View> ); } }); // omitted code
如你所见,在你的主TAB组件里面现在有了一个 onPersonPress 属性. 然后我们就可以将这个传到 Person 组件那儿, 而它们会在 TouchableHighlight 区域被触摸时调用到它. 而后我们可以增加索引,视图就会用新的人物集合来进行重新渲染.
这是对 facemash 的 tab 所做的最后修改. 如果你希望走得更远,下面是一些好主意
-
当选取了一个人物时可以去请求一个 REST 的端点
-
检查是否已经到达了列表的尽头,显示一条消息
-
让用户可以在照片上面点击来看更多的照片
你可以在分支三上面检出 facemash 的 tab 的最终代码。
消息
我们现在将注意力转移到消息tab上了。这一功能有点像 iMessage - 它是有关用户的一个可滚动列表,在其中一项上面点击将会导航至一个针对那个用户的聊天视图。
幸运的是, React Native 给了我们 ListView 组件,它能让我们拥有一个简单的,(使用了ScrollView的)可滚动列表,而且能高效的显示出列表(只对发生变化的行进行重新渲染,并限制了每次事件循环渲染的行只有一个)。
为了使用一个 ListView, 我们需要有一个数据源. 数据元让我们可以拥有一个定制的函数来检查一行是不是发生了变化 (可以想到它类似于 toshouldComponentUpdate) ,我们可以把JSON数据放到它里面去. 数据源存在于我们的状态对象之中。
在 thetabs/folder 下创建一个名为 Messages.js 的新文件
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image } = React; var messagesTab = React.createClass({ render: function() { return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } }); module.exports = messagesTab;
你同时需要编辑 editindex.ios.jsto 来指向新创建的组件 -
// omitted code var MessagesTab = require('./tabs/Messages'); var facemash = React.createClass({ getInitialState() { return { selectedTab: 'faceMash' } }, changeTab(tabName) { StatusBarIOS.setStyle(tabName === 'faceMash' ? 1 : 0); this.setState({ selectedTab: tabName }); }, render: function() { return ( <TabBarIOS> ... <TabBarIOS.Item title="Messages" icon={ require('image!messages') } onPress={ () => this.changeTab('messages') } selected={ this.state.selectedTab === 'messages' }> <MessagesTab /> </TabBarIOS.Item> ... </TabBarIOS> ); } }); // omitted code
根据我上面所说的,我们列表视图需要的一个数据源。这个可以通过 viaListView.DataSource 访问。我们将会在 ourgetInitialState 赋初值。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView } = React; var messagesTab = React.createClass({ getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' } }); module.exports = messagesTab;
现在我们已经获取到数据源,需要从服务器获取一些数据,知道我们跟谁在通信。我已经包含在一个端点配置中,在 thestep-threebranch 显得更短。
// omitted code var messagesTab = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, render: function(){ return ( <View style={ styles.container }> <Text>Messages!</Text> </View> ); } }); // omitted code
这个会获取到我们的数据,然后更新我们的数据源。这些处理完之后,无论什么时候我们的数据源更新了,我们的视图也会更新的。
现在我们来看看 ListViewcomponent。简单的说, 组件需要两个属性 -dataSource(我们已经有了) 。andrenderRow.renderRow 是一个需要返回一个 React 元素的函数。在数据源中每一行都会调用一次,然后作为参数为每一行传递合适的数据。
// omitted code var messagesTab = React.createClass({ ..., renderRow: function (){ return ( <View> <Text>Row goes here!</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
我们现在每一行都可以显示正确的数据。每个项的对象都作为第一个参数传递给 ourrenderRowfunction。
// omitted code var messagesTab = React.createClass({ ..., renderRow: function (person){ return ( <View> <Text>{ person.user.firstName } { person.user.lastName }</Text> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
我们继续我们的步骤,在这里添加其他的信息,比如图片和最新接收到的信息。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio } = React; function prettyTime(timestamp) { var createdDate = new Date(timestamp); var distance = Math.round( ( +new Date() - timestamp ) / 60000 ); var hours = ('0' + createdDate.getHours()).slice(-2); var minutes = ('0' + createdDate.getMinutes()).slice(-2); var month = ('0' + (createdDate.getMonth() + 1)).slice(-2); var date = ('0' + createdDate.getDate()).slice(-2); var year = createdDate.getFullYear(); var string; if (distance < 1440) { string = [hours, minutes].join(':'); } else if (distance < 2879) { string = 'Yesterday'; } else { string = [date, month, year].join('/'); } return string; } var messagesTab = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, row: { flex: 1, alignItems: 'center', backgroundColor: 'white', flexDirection: 'row', padding: 10 }, textContainer: { flex: 1 }, cellImage: { height: 60, borderRadius: 30, marginRight: 10, width: 60 }, time: { position: 'absolute', top: 0, right: 0, fontSize: 12, color: '#cccccc' }, name: { flex: 1, fontSize: 16, fontWeight: 'bold', marginBottom: 2 }, lastMessage: { color: '#999999', fontSize: 12 }, cellBorder: { backgroundColor: 'rgba(0, 0, 0, 0.1)', height: 1 / PixelRatio.get(), marginLeft: 4 } }); module.exports = messagesTab;
Looking good!
你会注意到我们在样式表中使用了一个叫做 PixelRatio 的东西. 用这个我们就可以得到能够拿来在设备上显示的最细的线. 一般,我们会用 1px 作为最细的边框,但是 React Native 中没有 px 的概念。
现在我们可以添加代码来处理在用户项上面的触摸了。我们将使用 NavigatorIOS 组件 - 你会在诸如 iMessage 和 Notes 这样的应用上看到这个东西. 它能让我们获得视图之间的回退功能,顶端的导航条也会如此。
实际上我们准备创建一个新的 React 组件来装这个导航。这是因为组件需要对一个初始的 React 组件进行渲染。
我们会将 messagesTab 组件改称做 messageList,并创建另外一个叫做 messagesTab 的组件
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS } = React; // omitted code var messageList = React.createClass({ ..., render: function(){ return ( <View style={ > <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); var messagesTab = React.createClass({ render: function() { return ( <NavigatorIOS style={ styles.container } initialRoute={ { title: 'Messages', component: messageList } } /> ); } }); // omitted code
看看已经变得更专业了哦。就像我们在 facemash 的 Tab 中所做的,我们现在可以向行中添加触摸时高亮效果( TouchableHighlight) 了。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View, Image, ListView, PixelRatio, NavigatorIOS, TouchableHighlight } = React; // omitted code var messageList = React.createClass({ componentWillMount: function() { fetch('http://localhost:8882/rest/messages') .then(res => res.json()) .then(res => this.updateDataSource(res)); }, getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (r1, r2) => r1 !== r2 }) }; }, updateDataSource: function(data){ this.setState({ dataSource: this.state.dataSource.cloneWithRows(data) }) }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </TouchableHighlight> </View> ); }, render: function(){ return ( <View style={ styles.container }> <ListView dataSource={ this.state.dataSource } renderRow={ this.renderRow } /> </View> ); } }); // omitted code
重新加载然后,你会收到一个错误。这是因为我们传了两个子组件到 TouchableHighlight,但它只能很好的拿一个来进行显示。不要担心啦,我们还可以把这俩子组件封装到另外一个 View 组件中来解决问题啊。
// omitted code var messageList = React.createClass({ ..., renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... }); // omitted code
现在,当我们在一行上面触摸时,就会收到我们预期的效果了。等等 - 我们的底部边框看起来怪怪的!这是因为我们使用的是 rgba 值。整个视图的背景颜色正在发生变化,这意味着我们的边框随后会变得更暗。不要担心,我们可以给它一个十六进制值的。
var styles = StyleSheet.create({ ..., cellBorder: { backgroundColor: '#F2F2F2', height: 1 / PixelRatio.get(), marginLeft: 4 } });
如上所述的代码你可以在分支四中看到。
按下和弹出
现在我们已经让主列表有了样式,可以来处理用户触摸时导航发生的变化了。
NavigatorIOS 让我们可以在想要改变当前的路由时,“按下”到组件的里面去。为此,我们需要子组件以及 messagList 中能有某些方式能访问到 NavigatorIOS 实体。幸运的是,这个已经以叫做 navigator 的属性传入了。
让我们向 TouchableHighlight 组件加入一个 onPress 事件吧.
// omitted code var messageList = React.createClass({ ..., openChat: function (user){ }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> ... </TouchableHighlight> </View> ); }, ... }); // omitted code
现在我们需要一个 React 组件来传递到 navigator。请在 tabs/ 文件夹中创建一个叫做 MessageView.js 的新文件。
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View } = React; var messageView = React.createClass({ render: function(){ return ( <View style={ styles.container }> <Text>Message view!</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff' }, }); module.exports = messageView;
我们可以将这个包含到 Messages.js 文件中,并将它放到 navigator 那儿去。
// omitted code var messageList = React.createClass({ ..., openChat: function (user){ this.props.navigator.push({ title: `${user.firstName} ${user.lastName}`, component: MessageView, passProps: { user } }); }, renderRow: function (person){ var time = prettyTime(person.lastMessage.timestamp); return ( <View> <TouchableHighlight onPress={ this.openChat.bind(this, person.user) }> <View> <View style={ styles.row }> <Image source={ { uri: person.user.picture } } style={ styles.cellImage } /> <View style={ styles.textContainer }> <Text style={ styles.name } numberOfLines={ 1 }> { person.user.firstName } { person.user.lastName } </Text> <Text style={ styles.time } numberOfLines={ 1 }> { time } </Text> <Text style={ styles.lastMessage } numberOfLines={ 1 }> { person.lastMessage.contents } </Text> </View> </View> <View style={ styles.cellBorder } /> </View> </TouchableHighlight> </View> ); }, ... }); // omitted code
从这儿你可以看到我们将下一个路由压入了 navigator。在这儿我们可以设置下一个路由的标题 (用户的姓名),渲染什么组件 (我们新创建的 MessageView) 以及传入什么属性。这让我们可以访问我们在 MessageView 组件中需要的任何东西 (我们准备传入用户对象)。
这对于我们聊天列表中的每一个都会起作用,不管其数量多还是少。
不过,文本会被我们新的标题条切段。解决这个问题,主要在第一个 View 组件上放一个内边距(padding)就可以了。
我们现在已经有一个用户属性被传进来了,同样可以对其进行展示!
MessageView.js
'use strict'; var React = require('react-native'); var { StyleSheet, Text, View } = React; var messageView = React.createClass({ render: function(){ var user = this.props.user; return ( <View style={ styles.container }> <Text>Chat with { user.firstName } { user.lastName }</Text> </View> ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#fff', paddingTop: 64 }, }); module.exports = messageView;
你可以在step-five分支检测出上面的代码。
如果你想在这个tab上做更多的事情,下面是一些好主意
-
当MessageView载入时获取那个用户的聊天信息
-
尝试实现下拉刷新 - 提示:使用ListView上的renderHeader属性
-
在用户的姓名右侧添加一个“设置(Settings)" 按钮
总结
请自由地在设置 tab 中加入一些功能。尝试使用其他的组件,比如 DatePickerIOS 遗迹 TextInput,来做一些通用的设置 (DOB,name,等等)。
希望这里的讨论能触及一些同你在你的 React Native 应用程序中通常会用到有所不同的组件。如有任何疑问,可以在推特 @rynclark 上联系我.