React Native之构建一个容易的列表页
本文中我们将创建一个简单的电影应用,这个应用将从Rotten Tomatoes网站抓取目前正在上映的最新的25部电影,并将它们展示在一个ListView中。
一、伪造数据
在我们开始编写代码从Rotten Tomatoes网站抓取数据之前,我们先来伪造一些数据,以便我们可以马上体验一下React Native。我们一般会在JS文件的顶部声明常量,并在后面使用。在index.ios.js中添加以下代码:
var MOCKED_MOVIES_DATA = [ {title: 'Title', year: '2015', posters: {thumbnail: 'http://i.imgur.com/UePbdph.jpg'}}, ];
二、渲染一部电影
我们会渲染电影的标题、年份以及海报缩略图。由于缩略图在React Native中是一个Image组件,所以我们需要将Image组件加到React的依赖项中。
var { AppRegistry, Image, StyleSheet, Text, View, } = React;
现在我们来修改render函数,以便我们可以渲染上面的模拟数据。
render: function() { var movie = MOCKED_MOVIES_DATA[0]; return ( <View style={styles.container}> <Text>{movie.title}</Text> <Text>{movie.year}</Text> <Image source={{uri: movie.posters.thumbnail}} style={styles.thumbnail} /> </View> ); }
同时我们还需要修改styles,用来将样式应用到相应的组件上。
var styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, thumbnail: { width: 53, height: 81, }, });
按下cmd+R,此时的电影标题、年份以及海报缩略图已经渲染出来了。
三、修改样式,将文字放在图片的右侧
接下来,我们把文字放在图片的右侧,同时让标题字体大一些并居中显示。
修改render函数,添加另一个View,这是为了让我们的组件在最外层的组件中垂直居中显示。
render: function() { var movie = MOCKED_MOVIES_DATA[0]; return ( <View style={styles.container}> <Image source={{uri: movie.posters.thumbnail}} style={styles.thumbnail} /> <View style={styles.rightContainer}> <Text style={styles.title}>{movie.title}</Text> <Text style={styles.year}>{movie.year}</Text> </View> </View> ); }
同时我们还需要修改styles,用来将样式应用到相应的组件上。
var styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, thumbnail: { width: 53, height: 81, }, rightContainer: { flex: 1, }, title: { fontSize: 20, marginBottom: 8, textAlign: 'center', }, year: { textAlign: 'center', }, });
按下cmd+R来查看更新之后的视图。
四、抓取真实数据
将下面的常量放在文件的顶部,用来创建一个请求数据使用的REQUEST_URL:
var API_KEY = '7waqfqbprs7pajbz28mqf6vz'; var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/lists/movies/in_theaters.json'; var PAGE_SIZE = 25; var PARAMS = '?apikey=' + API_KEY + '&page_limit=' + PAGE_SIZE; var REQUEST_URL = API_URL + PARAMS;
为我们的应用添加初始状态,这样我们可以通过检查this.state.movies === null来确定电影数据有没有被加载。当电影数据返回时,我们可以通过this.setState({movies: moviesData})来设置数据。
将下面的代码添加到render函数之前:
getInitialState: function() { return { movies: null, }; },
我们想要在组件完成加载后发送请求,componentDidMount是React组件中的一个函数,它只会在组件加载完成后被调用一次。
componentDidMount: function() { this.fetchData(); },
接下来添加组件中会用到的fetchData函数,这个函数将负责处理数据的抓取。你需要做的仅仅是在promise完成解析后调用this.setState({movies: data})。因为setState会触发重新渲染,而此时render函数会注意到this.state.movies不再是null。
注意一定要在promise链的最后调用done(),否则错误信息可能会被忽略。
fetchData: function() { fetch(REQUEST_URL) .then((response) => response.json()) .then((responseData) => { this.setState({ movies: responseData.movies, }); }) .done(); },
现在修改render函数,如果电影数据还没有返回的话,就渲染一个loading视图,否则将渲染第一部电影。
render: function() { if (!this.state.movies) { return this.renderLoadingView(); } var movie = this.state.movies[0]; return this.renderMovie(movie); }, renderLoadingView: function() { return ( <View style={styles.container}> <Text> Loading movies... </Text> </View> ); }, renderMovie: function(movie) { return ( <View style={styles.container}> <Image source={{uri: movie.posters.thumbnail}} style={styles.thumbnail} /> <View style={styles.rightContainer}> <Text style={styles.title}>{movie.title}</Text> <Text style={styles.year}>{movie.year}</Text> </View> </View> ); },
按下cmd+R,首先会看到loading页面
电影数据返回后,就会渲染第一部从Rotten Tomatoes获取过来的电影。
五、显示完整的电影列表
现在我们来修改应用,将所有的数据渲染在一个ListView组件中,而不是只渲染一部电影。
这里使用ListView是因为ListView会自动渲染视线之内的视图,而那些在屏幕之外的视图会被暂时移除。
这里既然用到了ListView,我们就需要将ListView组件加到React的依赖项中。
var { AppRegistry, Image, ListView, StyleSheet, Text, View, } = React;
接下来修改render函数,一旦数据返回就可以在ListView里面渲染数据:
render: function() { if (!this.state.loaded) { return this.renderLoadingView(); } return ( <ListView dataSource={this.state.dataSource} renderRow={this.renderMovie} style={styles.listView} /> ); },
DataSource是ListView的一个接口,作用是决定哪些行会改变。
接下来,我们需要在getInitialState的返回对象上添加一个空的dataSource,我们不能再使用this.state.movies防止数据被存储两次。我们可以使用this.state.loaded来判断数据抓取是否结束。
getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), loaded: false, }; },
我们还需要修改fetchData来更新state:
fetchData: function() { fetch(REQUEST_URL) .then((response) => response.json()) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.movies), loaded: true, }); }) .done(); },
最后,我们在styles中为ListView组件添加样式:
listView: { paddingTop: 20, backgroundColor: '#F5FCFF', },
下面是最终的效果图:
六、本文最终完整的源码
'use strict'; var React = require('react-native'); var API_KEY = '7waqfqbprs7pajbz28mqf6vz'; var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/lists/movies/in_theaters.json'; var PAGE_SIZE = 25; var PARAMS = '?apikey=' + API_KEY + '&page_limit=' + PAGE_SIZE; var REQUEST_URL = API_URL + PARAMS; var { AppRegistry, Image, ListView, StyleSheet, Text, View, } = React; var Movie = React.createClass({ getInitialState: function() { return { dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), loaded: false, }; }, componentDidMount: function() { this.fetchData(); }, fetchData: function() { fetch(REQUEST_URL) .then((response) => response.json()) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.movies), loaded: true, }); }) .done(); }, render: function() { if (!this.state.loaded) { return this.renderLoadingView(); } return ( <ListView dataSource={this.state.dataSource} renderRow={this.renderMovie} style={styles.listView} /> ); }, renderLoadingView: function() { return ( <View style={styles.container}> <Text> Loading movies... </Text> </View> ); }, renderMovie: function(movie) { return ( <View style={styles.container}> <Image source={{uri: movie.posters.thumbnail}} style={styles.thumbnail} /> <View style={styles.rightContainer}> <Text style={styles.title}>{movie.title}</Text> <Text style={styles.year}>{movie.year}</Text> </View> </View> ); }, }); var styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, thumbnail: { width: 53, height: 81, }, rightContainer: { flex: 1, }, title: { fontSize: 20, marginBottom: 8, textAlign: 'center', }, year: { textAlign: 'center', }, listView: { paddingTop: 20, backgroundColor: '#F5FCFF', }, }); AppRegistry.registerComponent('Movie', () => Movie);