前言
- 本文有配套视频,可以酌情观看。
- 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我。
- 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关。
- 如文中内容对您造成不便,烦请联系 277511806@qq.com 处理,谢谢。
- 转载麻烦注明出处,谢谢。
本篇资源:链接: https://pan.baidu.com/s/1i4HFmeT 密码: yy79
源码托管到 github 上,需要源码的 ,喜欢的话记得 Star,谢谢!
属性声明和属性确定
有朋友反馈这边 属性声明和属性确定 不了解,这边就来补充一下。
在 React-Native 创建的自定义组件是可以复用的,而开发过程中一个组件可能会由多个人同时开发或者多个人使用一个组件,为了让开发人员之间减少沟通成本,我们会对某些必要的属性进行属性声明,让使用的人知道需要传入什么!甚至有些需要传入但没有传入值的属性我们会进行警告处理!
这边先来看下 属性声明 的示例:
static propTypes = { name:PropTypes.string, ID:PropTypes.number.isRequired, }
上面我们声明了
name
和ID
两个属性,并且进行了属性的确认,其中,'isRequired' 表示如果不传递这个属性,那么开发阶段中,系统会出现警告,让我们对其进行属性确认,也就是说是否为必须属性。- 属性确认语法分为:
- 属性为任何类型
React.PropTypes.any
- 属性是否是 JavaScript 基本类型
React.PropTypes.array; React.PropTypes.func; React.PropTypes.bool; React.PropTypes.number; React.PropTypes.object; React.PropTypes.string;
- 属性是某个 React 元素
React.PropTypes.element;
- 属性为几个特定的值
React.PropTypes.oneOf(['value1', 'value2'])
- 属性为指定类型中的一个
React.PropTypes.oneOfType([ React.PropTypes.node, React.PropTypes.number, React.PropTypes.string ])
- 属性为可渲染的节点
React.PropTypes.node;
- 属性为某个指定类的实例
React.PropTypes.instanceOf(NameOfClass);
- 属性为指定类型的数组
React.PropTypes.arrayOf(React.PropTypes.string)
- 属性有一个指定的成员对象
React.PropTypes..objectOf(React.PropTypes.number)
- 属性是一个指定构成方式的对象
React.PropTypes.shape({ color:React.PropTypes.stirng, fontSize:React.PropTypes.number })
- 属性默认值(当我们没有传递属性的时候使用)
static defaultProps = { name:'苍井空' };
占位图
开发中,我们会有许多图片都是从网络进行请求的,但是,如果出现网络卡顿的情况,图片就会迟迟不出现,又或者有的并没有图片,这样图片就为空白状态;为了不让用户感觉太突兀影响用户体验,也为了视图整体性,一般我们会选择使用占位图先展示给用户看,等到图片加载完毕再将图片展示出来。
这边我们需要对cell内部进行一些处理。
{/* 左边图片 */}
无数据情况处理
还是网络问题,在网络出现问题或者无法加载数据的时候,一般我们会展示空白页,在空白页中提示
无数据
之类的提示,比较好的还会使用指示器
的方式告诉用户网络出现问题等等。这边我们做以下处理,当无数据时,我们就先初始化基础界面,然后展示
提示
页面,等到有数据时,再重新渲染数据。首先设置 无数据 页面
import React, { Component } from 'react'; import { StyleSheet, View, Text, } from 'react-native'; export default class GDNoDataView extends Component { render() { return(); } } const styles = StyleSheet.create({ container: { flex:1, justifyContent:'center', alignItems:'center', }, textStyle: { fontSize:21, color:'gray' } }); 无数据
- 接着,没有数据的时候我们进行一些处理就可以了
// 根据网络状态决定是否渲染 listview renderListView() { if (this.state.loaded === false) { return(); }else { return( this.fetchData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} /> ); } }
listView 头部设置
- 根据原版效果发现 提示标题 应该放到 ListView 的头部才对,所以这边就做下小修改。
- renderHeader 方法实现
// 返回 listview 头部 renderHeader() { return (); } 根据每条折扣的点击进行统计,每5分钟更新一次
下拉刷新
- 为了避免适配问题带来的麻烦,这边我们采用第三方框架
react-native-pull
实现下拉刷新和上拉加载更多的功能。
this.fetchData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} renderHeader={this.renderHeader} />
- fetchData 方法修改
// 网络请求 fetchData(resolve) { setTimeout(() => { fetch('http://guangdiu.com/api/gethots.php') .then((response) => response.json()) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.data), loaded:true, }); if (resolve !== undefined){ setTimeout(() => { resolve(); // 关闭动画 }, 1000); } }) .done(); }); }
网络请求之POST(重要)
GET 和 POST 是我们请求 HTTP 接口常用的方式,针对表单提交的请求,我们通常采用 POST 的方式。
在 JQuery 中,传入对象框架会自动封装成 formData 的形式,但是在 fetch 中没有这个功能,所以我们需要自己初始化一个 FormData 直接传给 body (补充:FormData也可以传递字节流实现上传图片功能)。
let formData = new FormData(); formData.append("参数", "值"); formData.append("参数", "值"); fetch(url, { method:'POST, headers:{}, body:formData, }).then((response)=>{ if (response.ok) { return response.json(); } }).then((json)=>{ alert(JSON.stringify(json)); }).catch.((error)=>{ console.error(error); })
- 如果想详细了解 Fetch ,可以点击 查看。
首页模块
- 这边我们按照前面提到的步骤,进行数据的加载。
import React, { Component } from 'react'; import { StyleSheet, Text, View, TouchableOpacity, Image, ListView, Dimensions } from 'react-native'; // 第三方 import {PullList} from 'react-native-pull'; const {width, height} = Dimensions.get('window'); // 引用外部文件 import CommunalNavBar from '../main/GDCommunalNavBar'; import CommunalHotCell from '../main/GDCommunalHotCell'; import HalfHourHot from './GDHalfHourHot'; import Search from './GDSearch'; export default class GDHome 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); } // 网络请求 fetchData(resolve) { let formData = new FormData(); formData.append("count", "30"); setTimeout(() => { fetch('http://guangdiu.com/api/getlist.php', { method:'POST', headers:{}, body:formData, }) .then((response) => response.json()) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.data), loaded:true, }); if (resolve !== undefined){ setTimeout(() => { resolve(); }, 1000); } }) .done(); }); } // 跳转到近半小时热门 pushToHalfHourHot() { this.props.navigator.push({ component: HalfHourHot, }) } // 跳转到搜索 pushToSearch() { this.props.navigator.push({ component:Search, }) } // 返回左边按钮 renderLeftItem() { return({this.pushToHalfHourHot()}} > ); } // 返回中间按钮 renderTitleItem() { return(); } // 返回右边按钮 renderRightItem() { return( {this.pushToSearch()}} > ); } // 根据网络状态决定是否渲染 listview renderListView() { if (this.state.loaded === false) { return(); }else { return( this.fetchData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} renderHeader={this.renderHeader} /> ); } } // 返回每一行cell的样式 renderRow(rowData) { return( ); } componentDidMount() { this.fetchData(); } render() { return ( {/* 导航栏样式 */} ); } } 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, }, });this.renderLeftItem()} titleItem = {() => this.renderTitleItem()} rightItem = {() => this.renderRightItem()} /> {/* 根据网络状态决定是否渲染 listview */} {this.renderListView()}
- OK,这边也已经成功拿到数据,所以接着就是完成 cell 样式部分就可以了。
效果:
navigator 跳转动画
有时候我们需要在跳转的时候使用不同的跳转动画,比如我们 半小时热门 的跳转方式在
iOS
内叫 模态跳转,特性就是当页面退出后会直接销毁,多用于注册、登录等不需要常驻内存的界面。react-native 中为了方便实现这样的功能,我们可以在初始化
Navigator
的时候,在 ‘configsSence’ 中进行操作;具体操作如下:
// 设置跳转动画configureScene={(route) => this.setNavAnimationType(route)}// 设置Navigator跳转动画 setNavAnimationType(route) { if (route.animationType) { // 有值 return route.animationType; }else { return Navigator.SceneConfigs.PushFromRight; } }
- 这样我们在需要跳转的地方只需要传入相应的参数即可。
// 跳转到近半小时热门 pushToHalfHourHot() { this.props.navigator.push({ component: HalfHourHot, animationType:Navigator.SceneConfigs.FloatFromBottom }) }
关闭 Navigator 返回手势
- 上面操作后,发现这边有个小细节就是我们使用了 `
作为跳转动画,但是当我们下拉的时候,动画中默认附带的 **返回手势** 会干扰我们
ListView的滑动手势,这个怎么解决呢?其实很简单,我们只要关闭
Navigator手势就可以了嘛,怎么关闭呢?其实在源码中我们可以找到,手势包含在动画中,我们如果不需要,只需要给其赋值为
null` ,这样它就不知道需要响应手势事件了,方法如下:
// 设置Navigator跳转动画 setNavAnimationType(route) { if (route.animationType) { // 有值 let conf = route.animationType; conf.gestures = null; // 关闭返回手势 return conf; }else { return Navigator.SceneConfigs.PushFromRight; } }
- 这样我们就成功关闭了
Navigator
手势功能。
上拉加载更多
- react-native-pull 框架的上拉加载使用也很简单,配合
onEndReached
、onEndReachedThreshold
、renderFooter
使用
loadMore() { // 数据加载操作 } renderFooter() { return (); } // 根据网络状态决定是否渲染 listview renderListView() { if (this.state.loaded === false) { return( ); }else { return( this.fetchData(resolve)} dataSource={this.state.dataSource} renderRow={this.renderRow} showsHorizontalScrollIndicator={false} style={styles.listViewStyle} initialListSize={5} renderHeader={this.renderHeader} onEndReached={this.loadMore} onEndReachedThreshold={60} renderFooter={this.renderFooter} /> ); } }
网络请求基础封装
到这里,相信各位对 React-Native 有所熟悉了吧,从现在开始我们要慢慢往实际的方向走,这边就先从网络请求这部分开始,在正式开发中,网络请求一般都单独作为一部分,我们在需要使用的地方只需要简单调用一下即可,这样做的好处是让整个 工程 的结构更加清晰,让组件们各司其职,只管好自己该管的事,并且后期维护成本也会相应降低。
首先,我们要先对 fetch 的
GET
和POST
请求方式进行一层基础封装,也就是要把它们单独独立出来,那么这边先来看下 GET 这边:
var HTTPBase = {}; /** * * GET请求 * * @param url * @param params {}包装 * @param headers * * @return {Promise} * * */ HTTPBase.get = function (url, params, headers) { if (params) { let paramsArray = []; // 获取 params 内所有的 key let paramsKeyArray = Object.keys(params); // 通过 forEach 方法拿到数组中每个元素,将元素与参数的值进行拼接处理,并且放入 paramsArray 中 paramsKeyArray.forEach(key => paramsArray.push(key + '=' + params[key])); // 网址拼接 if (url.search(/\?/) === -1) { url += '?' + paramsArray.join('&'); }else { url += paramsArray.join('&'); } } return new Promise(function (resolve, reject) { fetch(url, { method:'GET', headers:headers }) .then((response) => response.json()) .then((response) => { resolve(response); }) .catch((error) => { reject({status:-1}) }) .done(); }) }
- 好,这边我们 GET 就封装好了,简单使用一下:
fetchData(resolve) { HTTPBase.get('http://guangdiu.com/api/gethots.php') .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.data), loaded:true, }); if (resolve !== undefined){ setTimeout(() => { resolve(); // 关闭动画 }, 1000); } }) .catch((error) => { }) } export default HTTPBase;
- 接着,我们继续来对 POST 进行封装:
/** * * POST请求 * * @param url * @param params {}包装 * @param headers * * @return {Promise} * * */ HTTPBase.post = function (url, params, headers) { if (params) { // 初始化FormData var formData = new FormData(); // 获取 params 内所有的 key let paramsKeyArray = Object.keys(params); // 通过 forEach 方法拿到数组中每个元素,将元素与参数的值进行拼接处理,并且放入 paramsArray 中 paramsKeyArray.forEach(key => formData.append(key, params[key])); } return new Promise(function (resolve, reject) { fetch(url, { method:'POST', headers:headers, body:formData, }) .then((response) => response.json()) .then((response) => { resolve(response); }) .catch((error) => { reject({status:-1}) }) .done(); }) } export default HTTPBase;
- 好,来试一下:
// 网络请求 fetchData(resolve) { let params = {"count" : 5 }; HTTPBase.post('http://guangdiu.com/api/getlist.php', params) .then((responseData) => { this.setState({ dataSource: this.state.dataSource.cloneWithRows(responseData.data), loaded:true, }); if (resolve !== undefined){ setTimeout(() => { resolve(); }, 1000); } }) .catch((error) => { }) }
- 这次篇幅有点短,实在是太忙了!