React-Native 之 项目实战(二)

前言


  • 本文有配套视频,可以酌情观看。
  • 文中内容因各人理解不同,可能会有所偏差,欢迎朋友们联系我。
  • 文中所有内容仅供学习交流之用,不可用于商业用途,如因此引起的相关法律法规责任,与我无关。
  • 如文中内容对您造成不便,烦请联系 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) => {

            })
    }

  • 这次篇幅有点短,实在是太忙了!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容