React-Native 之 项目实战(三)
前言
- 本文有配套视频,可以酌情观看。
- 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我。
- 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关。
- 如文中内容对您造成不便,烦请联系 277511806@qq.com 处理,谢谢。
- 转载麻烦注明出处,谢谢。
本篇资源:链接: https://pan.baidu.com/s/1jIbW2n8 密码: wqe4
从这篇开始我们就将源码托管到 github 上,需要源码的 点我下载,喜欢的话记得 Star,谢谢!
Android启动页面
-
从上面的效果可以看出,安卓端还没有启动页面,这边我们就通过
React-Native
的方式解决。- 思路:新建一个组件作为 Android 的启动页,index.android.js 的初始化窗口改为 Android启动页,设置定时器,使其在
1.5秒
后自动跳转到 Main 组件。
export default class GDLaunchPage extends Component { componentDidMount() { setTimeout(() => { this.props.navigator.replace({ component:Main }) }, 1500) } render() { return( <Image source={{uri:'launchimage'}} style={styles.imageStyle} /> ); } }
- 思路:新建一个组件作为 Android 的启动页,index.android.js 的初始化窗口改为 Android启动页,设置定时器,使其在
git使用
-
项目的版本管理也是程序猿必须具备的一项技能,它能够让我们避免许多开发中遇到的尴尬问题。
-
公司里面一般使用 SVN 和 Git 两种,而现在 Git 的份额逐渐在蚕食着 SVN,这边我给大家提供了 SVN 和 Git 的详情版,大家可以前往阅读。
-
这小结建议观看视频,视频内有具体操作!
错误修正 —— 模态
-
以前看官方文档竟然没有发现 React-Native 提供了 model 组件,在这里给大家道个歉,以后跪着写教程,不用让我起来,反正我感觉膝盖软软的!
-
前几天在看官方文档的时候,无意中看见 model 组件,我嘞个天,有这东西就可以减少开发中很多功能开发难度。当初怎么没发现,还傻傻地一步一步去封装这个东西 T^T,在这告诫各位,不能太粗心!
-
这边我们就将原本 近半小时热门 这个模块的跳转模式改成 正宗的 模态,代码如下:
render() {
return (
<View style={styles.container}>
{/* 初始化模态 */}
<Modal
animationType='slide'
transparent={false}
visible={this.state.isModal}
onRequestClose={() => this.onRequestClose()}
>
<Navigator
initialRoute={{
name:'halfHourHot',
component:HalfHourHot
}}
renderScene={(route, navigator) => {
let Component = route.component;
return <Component
removeModal={(data) => this.closeModal(data)}
{...route.params}
navigator={navigator} />
}} />
</Modal>
{/* 导航栏样式 */}
<CommunalNavBar
leftItem = {() => this.renderLeftItem()}
titleItem = {() => this.renderTitleItem()}
rightItem = {() => this.renderRightItem()}
/>
{/* 根据网络状态决定是否渲染 listview */}
{this.renderListView()}
</View>
);
}
注:这边需要注意一下 逆向传值 的方式,这里用到最基本的逐层传值,类似于
block
的功能,具体的代码参考 Demo , Demo 下载地址在上面。
-
关于更详细地 model 使用,可以参照官方文档 model ,当然我也给各位上了这道菜 —— React-Native 之 model介绍与使用 。
-
通过查看 modal 的源码,我们不难发现 —— 其实 modal 实现原理也只是使用了 绝对定位,所以如果 modal 无法满足我们的功能,我们可以使用 绝对定位 来自己实现一下类似功能。
加载更多功能完善
-
这边我们来完善一下 加载更多功能数据 的加载,需要注意的一点就是,拼接数组需要使用
concat
方法来拼接,它会返回一个 新的数组 给我们使用,而不修改传入的数组。 -
这边我们加载数据的方法分为 2 个,代码看起来重复性很高,但是其实这就取决于我们的需求了,我们分为 2 个的好处是看起来更清晰,减少沟通成本,想象一下,如果我们把所有逻辑都放到同一个方法内,那么是不是这个方法内的逻辑是不是特别复杂,不方便后期维护?!所以这就是为什么分为 2 个方法进行加载的原因。
-
那来看一下加载最新数据这边逻辑:
// 加载最新数据网络请求 loadData(resolve) { let params = {"count" : 10 }; HTTPBase.get('https://guangdiu.com/api/getlist.php', params) .then((responseData) => { // 清空数组 this.data = []; // 拼接数据 this.data = this.data.concat(responseData.data); // 重新渲染 this.setState({ dataSource: this.state.dataSource.cloneWithRows(this.data), loaded:true, }); // 关闭刷新动画 if (resolve !== undefined){ setTimeout(() => { resolve(); }, 1000); } // 存储数组中最后一个元素的id let cnlastID = responseData.data[responseData.data.length - 1].id; AsyncStorage.setItem('cnlastID', cnlastID.toString()); }) .catch((error) => { }) }
-
再来看下加载更多这边的逻辑:
-
加载更多需要在获取 最新 数据的时候将数组中
最后一个元素
内的ID保存起来,因为不是大批量数据存储,这边我们就使用 AsyncStorage 进行id
的存储。 -
接着,我们拼接请求参数。
// 加载更多数据的网络请求 loadMoreData(value) { let params = { "count" : 10, "sinceid" : value }; HTTPBase.get('https://guangdiu.com/api/getlist.php', params) .then((responseData) => { // 拼接数据 this.data = this.data.concat(responseData.data); this.setState({ dataSource: this.state.dataSource.cloneWithRows(this.data), loaded:true, }); // 存储数组中最后一个元素的id let cnlastID = responseData.data[responseData.data.length - 1].id; AsyncStorage.setItem('cnlastID', cnlastID.toString()); }) .catch((error) => { }) }
-
Cell 点击实现
-
我们回到主页这边来实现以下
cell
的点击,需要注意的是对row
进行绑定操作,不然会找不到当前的this
。// 绑定 renderRow={this.renderRow.bind(this)}
-
接着来看下
renderRow
方法实现:// 返回每一行cell的样式 renderRow(rowData) { return( <TouchableOpacity onPress={() => this.pushToDetail(rowData.id)} > <CommunalHotCell image={rowData.image} title={rowData.title} /> </TouchableOpacity> ); }
-
再来看下
pushToDetail
方法实现,params意思就是将url
参数传递到CommunalDetail
组件:// 跳转到详情页 pushToDetail(value) { this.props.navigator.push({ component:CommunalDetail, params: { url: 'https://guangdiu.com/api/showdetail.php' + '?' + 'id=' + value } }) }
详情页
-
既然我们已经保存了
id
那么就可以来做详情页了,当我们点击 cell 的时候,需要跳转到对应的 详情页 。 -
这边服务器返回给我们的是个 网页数据 ,我们这边就直接使用
webView组件
展示,具体使用我们就不多做介绍了,很简单,详情就参考官方文档 WebView。 -
先来看详情页的实现:
export default class GDCommunalDetail extends Component { static propTypes = { uri:PropTypes.string, }; // 返回 pop() { this.props.navigator.pop(); } // 返回左边按钮 renderLeftItem() { return( <TouchableOpacity onPress={() => {this.pop()}} > <Text>返回</Text> </TouchableOpacity> ); } componentWillMount() { // 发送通知 DeviceEventEmitter.emit('isHiddenTabBar', true); } componentWillUnmount() { // 发送通知 DeviceEventEmitter.emit('isHiddenTabBar', false); } render() { return( <View style={styles.container}> {/* 导航栏 */} <CommunalNavBar leftItem = {() => this.renderLeftItem()} /> {/* 初始化WebView */} <WebView style={styles.webViewStyle} source={{url:this.props.url, method: 'GET' }} javaScriptEnabled={true} domStorageEnabled={true} scalesPageToFit={false} /> </View> ); } } const styles = StyleSheet.create({ container: { flex:1 }, webViewStyle: { flex:1 } });
-
按照上面的方法,我们完成一下 近半小时热门模块 的跳转详情功能。
海淘半小时热门
-
和 近半小时热门 效果是一样的,只是请求参数变了,所以 Copy 然后修改下相应参数啊:
export default class GDUSHalfHourHot extends Component { // 构造 constructor(props) { super(props); // 初始状态 this.state = { dataSource: new ListView.DataSource({rowHasChanged:(r1, r2) => r1 !== r2}), loaded:true, }; this.fetchData = this.fetchData.bind(this); } static defaultProps = { removeModal:{} } // 网络请求 fetchData(resolve) { let params = { "c" : "us" }; HTTPBase.get('http://guangdiu.com/api/gethots.php', params) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.data), loaded:true, }); if (resolve !== undefined){ setTimeout(() => { resolve(); // 关闭动画 }, 1000); } }) .catch((error) => { }) } popToHome(data) { this.props.removeModal(data); } // 返回中间按钮 renderTitleItem() { return( <Text style={styles.navbarTitleItemStyle}>近半小时热门</Text> ); } // 返回右边按钮 renderRightItem() { return( <TouchableOpacity onPress={()=>{this.popToHome(false)}} > <Text style={styles.navbarRightItemStyle}>关闭</Text> </TouchableOpacity> ); } // 根据网络状态决定是否渲染 listview renderListView() { if (this.state.loaded === false) { return( <NoDataView /> ); }else { return( <PullList onPullRelease={(resolve) => this.fetchData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow.bind(this)} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} renderHeader={this.renderHeader} /> ); } } // 返回 listview 头部 renderHeader() { return ( <View style={styles.headerPromptStyle}> <Text>根据每条折扣的点击进行统计,每5分钟更新一次</Text> </View> ); } // 跳转到详情页 pushToDetail(value) { this.props.navigator.push({ component:CommunalDetail, params: { url: 'https://guangdiu.com/api/showdetail.php' + '?' + 'id=' + value } }) } // 返回每一行cell的样式 renderRow(rowData) { return( <TouchableOpacity onPress={() => this.pushToDetail(rowData.id)} > <CommunalHotCell image={rowData.image} title={rowData.title} /> </TouchableOpacity> ); } componentWillMount() { // 发送通知 DeviceEventEmitter.emit('isHiddenTabBar', true); } componentWillUnmount() { // 发送通知 DeviceEventEmitter.emit('isHiddenTabBar', false); } componentDidMount() { this.fetchData(); } render() { return ( <View style={styles.container}> {/* 导航栏样式 */} <CommunalNavBar titleItem = {() => this.renderTitleItem()} rightItem = {() => this.renderRightItem()} /> {/* 根据网络状态决定是否渲染 listview */} {this.renderListView()} </View> ); } } const styles = StyleSheet.create({ container: { flex:1, alignItems: 'center', }, navbarTitleItemStyle: { fontSize:17, color:'black', marginLeft:50 }, navbarRightItemStyle: { fontSize:17, color:'rgba(123,178,114,1.0)', marginRight:15 }, listViewStyle: { width:width, }, headerPromptStyle: { height:44, width:width, backgroundColor:'rgba(239,239,239,0.5)', justifyContent:'center', alignItems:'center' } });
海淘模块
-
我们可以发现 海淘 这一块和 首页 是类似的,只是数据请求参数不同,所以我们还是 Copy 一下代码,然后将请求参数改为如下:
export default class GDHome extends Component { // 构造 constructor(props) { super(props); // 初始状态 this.state = { dataSource: new ListView.DataSource({rowHasChanged:(r1, r2) => r1 !== r2}), loaded:false, isModal:false }; this.data = []; this.loadData = this.loadData.bind(this); this.loadMore = this.loadMore.bind(this); } // 加载最新数据网络请求 loadData(resolve) { let params = { "count" : 10, "country" : "us" }; HTTPBase.get('https://guangdiu.com/api/getlist.php', params) .then((responseData) => { // 拼接数据 this.data = this.data.concat(responseData.data); // 重新渲染 this.setState({ dataSource: this.state.dataSource.cloneWithRows(this.data), loaded:true, }); // 关闭刷新动画 if (resolve !== undefined){ setTimeout(() => { resolve(); }, 1000); } // 存储数组中最后一个元素的id let uslastID = responseData.data[responseData.data.length - 1].id; AsyncStorage.setItem('uslastID', uslastID.toString()); }) .catch((error) => { }) } // 加载更多数据的网络请求 loadMoreData(value) { let params = { "count" : 10, "sinceid" : value, "country" : "us" }; HTTPBase.get('https://guangdiu.com/api/getlist.php', params) .then((responseData) => { // 拼接数据 this.data = this.data.concat(responseData.data); this.setState({ dataSource: this.state.dataSource.cloneWithRows(this.data), loaded:true, }); // 存储数组中最后一个元素的id let uslastID = responseData.data[responseData.data.length - 1].id; AsyncStorage.setItem('uslastID', uslastID.toString()); }) .catch((error) => { }) } // 加载更多数据操作 loadMore() { // 读取id AsyncStorage.getItem('uslastID') .then((value) => { // 数据加载操作 this.loadMoreData(value); }) } // 模态到近半小时热门 pushToHalfHourHot() { this.setState({ isModal:true }) } // 跳转到搜索 pushToSearch() { this.props.navigator.push({ component:Search, }) } // 安卓模态销毁处理 onRequestClose() { this.setState({ isModal:false }) } // 关闭模态 closeModal(data) { this.setState({ isModal:data }) } // 返回左边按钮 renderLeftItem() { return( <TouchableOpacity onPress={() => {this.pushToHalfHourHot()}} > <Image source={{uri:'hot_icon_20x20'}} style={styles.navbarLeftItemStyle} /> </TouchableOpacity> ); } // 返回中间按钮 renderTitleItem() { return( <TouchableOpacity> <Image source={{uri:'navtitle_home_down_66x20'}} style={styles.navbarTitleItemStyle} /> </TouchableOpacity> ); } // 返回右边按钮 renderRightItem() { return( <TouchableOpacity onPress={()=>{this.pushToSearch()}} > <Image source={{uri:'search_icon_20x20'}} style={styles.navbarRightItemStyle} /> </TouchableOpacity> ); } // ListView尾部 renderFooter() { return ( <View style={{height: 100}}> <ActivityIndicator /> </View> ); } // 根据网络状态决定是否渲染 listview renderListView() { if (this.state.loaded === false) { return( <NoDataView /> ); }else { return( <PullList onPullRelease={(resolve) => this.loadData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow.bind(this)} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} renderHeader={this.renderHeader} onEndReached={this.loadMore} onEndReachedThreshold={60} renderFooter={this.renderFooter} /> ); } } // 跳转到详情页 pushToDetail(value) { this.props.navigator.push({ component:CommunalDetail, params: { url: 'https://guangdiu.com/api/showdetail.php' + '?' + 'id=' + value } }) } // 返回每一行cell的样式 renderRow(rowData) { return( <TouchableOpacity onPress={() => this.pushToDetail(rowData.id)} > <CommunalHotCell image={rowData.image} title={rowData.title} /> </TouchableOpacity> ); } componentDidMount() { this.loadData(); } render() { return ( <View style={styles.container}> {/* 初始化模态 */} <Modal animationType='slide' transparent={false} visible={this.state.isModal} onRequestClose={() => this.onRequestClose()} > <Navigator initialRoute={{ name:'halfHourHot', component:USHalfHourHot }} renderScene={(route, navigator) => { let Component = route.component; return <Component removeModal={(data) => this.closeModal(data)} {...route.params} navigator={navigator} /> }} /> </Modal> {/* 导航栏样式 */} <CommunalNavBar leftItem = {() => this.renderLeftItem()} titleItem = {() => this.renderTitleItem()} rightItem = {() => this.renderRightItem()} /> {/* 根据网络状态决定是否渲染 listview */} {this.renderListView()} </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', backgroundColor: 'white', }, navbarLeftItemStyle: { width:20, height:20, marginLeft:15, }, navbarTitleItemStyle: { width:66, height:20, }, navbarRightItemStyle: { width:20, height:20, marginRight:15, }, listViewStyle: { width:width, }, });
获取最新数据个数功能
-
这里需要
cnmaxid
和usmaxid
参数,他们分别是最新数据中第一个元素的id
,也就是我们每次 刷新 的时候都保存一下数组中的第一个元素的id
。// 首页存储数组中第一个元素的id let cnfirstID = responseData.data[0].id; AsyncStorage.setItem('cnfirstID', cnfirstID.toString());
-
这个功能是从程序启动的时候就开始 定时循环执行 ,也就是我们需要放到 入口文件中(Main文件)。
componentDidMount() { // 注册通知 this.subscription = DeviceEventEmitter.addListener('isHiddenTabBar', (data)=>{this.tongZhi(data)}); // 声明变量 let cnfirstID = 0; let usfirstID = 0; // 最新数据的个数 setInterval(() => { // 取出id AsyncStorage.getItem('cnfirstID') .then((value) => { cnfirstID = parseInt(value); }); AsyncStorage.getItem('usfirstID') .then((value) => { usfirstID = parseInt(value); }); if (cnfirstID !== 0 && usfirstID !== 0) { // 参数不为0 // 拼接参数 let params = { "cnmaxid" : cnfirstID, "usmaxid" : usfirstID }; // 请求数据 HTTPBase.get('http://guangdiu.com/api/getnewitemcount.php', params) .then((responseData) => { console.log(responseData); this.setState({ cnbadgeText:responseData.cn, usbadgeText:responseData.us }) }) } }, 30000); }
注:上面使用到的
setInterval
也是个定时器,和我们之前使用的setTimeout
不同的是,setInterval
是周期定时器,比如上面时间为30000毫秒
,意思就是每过30000毫秒
就会执行一次里面的代码。而setTimeout
则是会在规定的时间后尽快
执行任务。