美文网首页React-NativeReact Native
React-Native 之 项目实战(二)

React-Native 之 项目实战(二)

作者: 珍此良辰 | 来源:发表于2017-03-22 22:42 被阅读710次

    前言


    • 本文有配套视频,可以酌情观看。
    • 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我。
    • 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关。
    • 如文中内容对您造成不便,烦请联系 277511806@qq.com 处理,谢谢。
    • 转载麻烦注明出处,谢谢。

    属性声明和属性确定


    • 有朋友反馈这边 属性声明和属性确定 不了解,这边就来补充一下。

    • React-Native 创建的自定义组件是可以复用的,而开发过程中一个组件可能会由多个人同时开发或者多个人使用一个组件,为了让开发人员之间减少沟通成本,我们会对某些必要的属性进行属性声明,让使用的人知道需要传入什么!甚至有些需要传入但没有传入值的属性我们会进行警告处理!

    • 这边先来看下 属性声明 的示例:

        static propTypes = {
            name:PropTypes.string,
            ID:PropTypes.number.isRequired,
        }
    
    
    • 上面我们声明了 nameID 两个属性,并且进行了属性的确认,其中,'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内部进行一些处理。

         {/* 左边图片 */}
         <Image source={{uri:this.props.image === '' ? 'defaullt_thumb_83x83' : this.props.image}} style={styles.imageStyle} />
    
    
    占位图.png

    无数据情况处理


    • 还是网络问题,在网络出现问题或者无法加载数据的时候,一般我们会展示空白页,在空白页中提示 无数据 之类的提示,比较好的还会使用 指示器 的方式告诉用户网络出现问题等等。

    • 这边我们做以下处理,当无数据时,我们就先初始化基础界面,然后展示 提示 页面,等到有数据时,再重新渲染数据。

    • 首先设置 无数据 页面

        import React, { Component } from 'react';
        import {
            StyleSheet,
            View,
            Text,
        } from 'react-native';
    
        export default class GDNoDataView extends Component {
    
            render() {
                return(
                    <View style={styles.container}>
                        <Text style={styles.textStyle}>无数据  </Text>
                    </View>
                );
    
            }
        }
    
        const styles = StyleSheet.create({
            container: {
                flex:1,
                justifyContent:'center',
                alignItems:'center',
            },
    
            textStyle: {
                fontSize:21,
                color:'gray'
            }
        });
        
    
    • 接着,没有数据的时候我们进行一些处理就可以了
        // 根据网络状态决定是否渲染 listview
        renderListView() {
            if (this.state.loaded === false) {
                return(
                    <NoDataView />
                );
            }else {
                return(
                    <PullList
                        onPullRelease={(resolve) => this.fetchData(resolve)}
                        dataSource={this.state.dataSource}
                        renderRow={this.renderRow}
                        showsHorizontalScrollIndicator={false}
                        style={styles.listViewStyle}
                        initialListSize={5}
                    />
                );
            }
        }
    
    
    无数据界面.png

    listView 头部设置


    • 根据原版效果发现 提示标题 应该放到 ListView 的头部才对,所以这边就做下小修改。
        <ListView
                 dataSource={this.state.dataSource}
                 renderRow={this.renderRow}
                 showsHorizontalScrollIndicator={false}
                 style={styles.listViewStyle}
                 initialListSize={5}
                 renderHeader={this.renderHeader}
       />
    
    
    • renderHeader 方法实现
        // 返回 listview 头部
        renderHeader() {
            return (
                <View style={styles.headerPromptStyle}>
                    <Text>根据每条折扣的点击进行统计,每5分钟更新一次</Text>
                </View>
            );
        }
        
    
    ListView头部.gif

    下拉刷新


    • 为了避免适配问题带来的麻烦,这边我们采用第三方框架 react-native-pull 实现下拉刷新和上拉加载更多的功能。
        <PullList
                onPullRelease={(resolve) => 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();
            });
        }
    
    
    下拉刷新.gif

    网络请求之POST(重要)


    • GETPOST 是我们请求 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);
            })
    
    

    首页模块


    • 这边我们按照前面提到的步骤,进行数据的加载。
        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(
                    <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
            renderListView() {
                if (this.state.loaded === false) {
                    return(
                        <NoDataView />
                    );
                }else {
                    return(
                        <PullList
                            onPullRelease={(resolve) => this.fetchData(resolve)}
                            dataSource={this.state.dataSource}
                            renderRow={this.renderRow}
                            showsHorizontalScrollIndicator={false}
                            style={styles.listViewStyle}
                            initialListSize={5}
                            renderHeader={this.renderHeader}
                        />
                    );
                }
            }
        
            // 返回每一行cell的样式
            renderRow(rowData) {
                return(
                    <CommunalHotCell
                        image={rowData.image}
                        title={rowData.title}
                    />
                );
            }
        
            componentDidMount() {
                this.fetchData();
            }
        
        
            render() {
                return (
                    <View style={styles.container}>
                        {/* 导航栏样式 */}
                        <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,
            },
        });
    
    
    • OK,这边也已经成功拿到数据,所以接着就是完成 cell 样式部分就可以了。
    首页数据效果.gif

    效果:

    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跳转动画.gif

    关闭 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返回手势关闭.gif
    • 这样我们就成功关闭了 Navigator 手势功能。

    上拉加载更多


    • react-native-pull 框架的上拉加载使用也很简单,配合 onEndReachedonEndReachedThresholdrenderFooter使用
        loadMore() {
            // 数据加载操作
        }
    
        renderFooter() {
            return (
                <View style={{height: 100}}>
                    <ActivityIndicator />
                </View>
            );
        }
    
        // 根据网络状态决定是否渲染 listview
        renderListView() {
            if (this.state.loaded === false) {
                return(
                    <NoDataView />
                );
            }else {
                return(
                    <PullList
                        onPullRelease={(resolve) => 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}
                    />
                );
            }
        }
        
    
    上拉加载更多.gif

    网络请求基础封装


    • 到这里,相信各位对 React-Native 有所熟悉了吧,从现在开始我们要慢慢往实际的方向走,这边就先从网络请求这部分开始,在正式开发中,网络请求一般都单独作为一部分,我们在需要使用的地方只需要简单调用一下即可,这样做的好处是让整个 工程 的结构更加清晰,让组件们各司其职,只管好自己该管的事,并且后期维护成本也会相应降低。

    • 首先,我们要先对 fetchGETPOST 请求方式进行一层基础封装,也就是要把它们单独独立出来,那么这边先来看下 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) => {
    
                })
        }
    
    
    • 这次篇幅有点短,实在是太忙了!

    相关文章

      网友评论

      • 6b639b538eab:楼主,为什么在fetch请求的url是http://时,error:network request failed;而url是https://开头就可以正常请求到数据
        珍此良辰:上面兄弟说的没错,需要修改项目的配置文件,这个文章里有提到过,你可以翻看实战第一篇,至于原因嘛:这是苹果的要求,为了安全考虑,苹果现在已经强制所有app请求必须基于https协议,而不再是以前的http,所以才会有现在的这个错误,应对方法就自己看实战一吧!
        劉光軍_MVP:xcode里面需要进行设置的
      • 慕诩:感谢楼主的教程,楼主,网络请求方法 fetchData(resolve) ,这个resolve参数是什么意思?componentDidMount() {
        this.fetchData();
        }里面调用该方法没有用到这个参数,只是下拉刷新的时候用到了,这是干什么的呢?
        菜鸟程序员_:@雨泽Forest 赞
        珍此良辰:resolve 应该是pullList重置下拉动画状态的参数(官方demo里是这样写的,但是后面发现并不好用,所以在后面的章节中教大家分析源码,通过别的方式直接操作这部分,所以不必太纠结哈),至于componentDidMount()中没有传递参数是因为进入页面时我们并没有使用到pulllist的下拉刷新功能,而是直接请求数据并更新,所以不传参也是可以的;这也是为什么会在网络请求中有if (resolve !== undefined){
        setTimeout(() => {
        resolve();
        }, 1000);
        } 这样的判断
      • aa035040c2a6:公用提示组件可介绍下吗,比如QQ里面的提示同意重顶部冒充,然后在讲下redux在native中的应用
        :clap: :+1: :grin:
        珍此良辰:@静_949a 这个功能很容易实现啊,可以使用modal组件实现,然后为顶部提示加入动画效果就可以了:smile: 需要的话,我会在接下来的教程中实现一下
        aa035040c2a6:@雨泽Forest 指顶部渐渐滑出然后又渐渐消失的提示,因为以前在WEB开发中都会有统一的组件,在native中不知道怎么加,好像移动端的容器和web网页的很不一样,都是场景的切换
        :grin:
        珍此良辰:@静_949a 是指顶部渐渐滑出然后又渐渐消失的提示?还是像alert弹出式的提示?redux肯定会说的,因为比较复杂,所以放到最后面来讲解,内容比较多,先将功能实现了解后,再来根据redux结构进行一次重构,理解起来会比较不那么费劲,毕竟涉及到了中间件等等内容,很绕

      本文标题:React-Native 之 项目实战(二)

      本文链接:https://www.haomeiwen.com/subject/kaajnttx.html