react进阶系列:高阶组件详解(二)

高阶组件能够封装公共逻辑,给当前组件传递方法属性,添加生命周期钩子等。react

案例:jquery

一个项目中有的页面须要判断所处环境,若是在移动端则正常显示页面,并向用户提示当前页面所处的移动端环境,若是不在移动端则显示提示让其在移动端打开。可是有的页面又不须要这个判断。api

若是在每一个页面都写一段判断逻辑未免麻烦,所以能够借助高阶组件来处理这部分逻辑。微信

先建立一个高阶组件cookie

// src/container/withEnvironment/index.jsx
import React from 'react';

const envs = {
    weixin: '微信',
    qq: 'QQ',
    baiduboxapp: '手机百度',
    weibo: '微博',
    other: '移动端'
}

function withEnvironment(BasicComponent) {
    const ua = navigator.userAgent;
    const isMobile = 'ontouchstart' in document;
    let env = 'other';

    if (ua.match(/MicroMessenger/i)) {
        env = 'weixin';
    }
    if (ua.match(/weibo/i)) {
        env = 'weibo';
    }
    if (ua.match(/qq/i)) {
        env = 'qq';
    }
    if (ua.match(/baiduboxapp/i)) {
        env = 'baiduboxapp'
    }

    // 不一样逻辑下返回不一样的中间组件
    if (!isMobile) {
        return function () {
            return (
                <div>
                    <div>该页面只能在移动端查看,请扫描下方二维码打开。</div>
                    <div>假设这里有张二维码</div>
                </div>
            )    
        }
    }

    // 经过定义的中间组件将页面所处环境经过props传递给基础组件
    const C = props => (
        <BasicComponent {...props} env={env} envdesc={envs[env]} />
    )

    return C;
}


export default withEnvironment;

而后在基础组件中使用react-router

// src/pages/Demo01/index.jsx
import React from 'react';
import withEnvironment from '../../container/withEnvironment';

function Demo01(props) {
    return (
        <div>你如今正在{props.envdesc}中访问该页面</div>
    )
}

export default withEnvironment(Demo01);

最后将基础组件渲染出来便可查看到效果。app

// src/index.js
import React from 'react';
import { render } from 'react-dom';
import Demo01 from './pages/Demo01';

const root = document.querySelector('#root');
render(<Demo01 />, root);

在上面这个例子中,咱们将环境判断的逻辑放在了高阶组件中处理,之后只要须要判断环境的页面只须要在基础组件中这样执行便可。dom

export default withEnvironment(Demo01);

除此以外,咱们在实际开发中还会遇到一个很是常见的需求,那就是在进入一个页面时须要判断登陆状态,登陆状态与非登陆状态的不一样显示,登陆状态以后角色的不一样显示均可以经过高阶组件统一来处理这个逻辑,而后将登陆状态,角色信息等传递给基础组件。异步

// 大概的处理逻辑
import React from 'react';
import $ from 'jquery';

// 假设已经封装了一个叫作getCookie的方法获取cookie
import { getCookie } from 'cookie';

function withRule(BasicComponent) {

    return class C extends React.Component {
        state = {
            islogin: false,
            rule: -1,
            loading: true,
            error: null
        }

        componentDidMount() {
            // 若是能直接在cookie中找到uid,说明已经登陆过并保存了相关信息
            if (getCookie('uid')) {
                this.setState({
                    islogin: true,
                    rule: getCookie('rule') || 0,
                    loading: false
                })
            } else {
                // 若是找不到uid,则尝试自动登陆,先从kookie中查找是否保存了登陆帐号与密码
                const userinfo = getCookie('userinfo');
                if (userinfo) {
                    // 调用登陆接口
                    $.post('/api/login', {
                        username: userinfo.username,
                        password: userinfo.password
                    }).then(resp => {
                        this.setState({
                            islogin: true,
                            rule: resp.rule,
                            islogin: false
                        })
                    }).catch(err => this.setState({ error: err.message }))
                } else {
                    // 当没法自动登陆时,你能够选择在这里弹出登陆框,或者直接显示未登陆页面的样式等均可以
                }
            }
        }

        render() {
            const { islogin, rule, loading, error } = this.state;

            if (error) {
                return (
                    <div>登陆接口请求失败!错误信息为:{error}</div>
                )
            }

            if (loading) {
                return (
                    <div>页面加载中, 请稍后...</div>
                )
            }

            return (
                <BasicComponent {...props} islogin={islogin} rule={rule} />
            )
        }
    }
}

export default withRule;

与第一个例子相比,这个例子更加接近实际应用而且逻辑也更更加复杂。所以涉及到了异步数据,所以最好的方式是在中间组件的componentDidMount中来处理逻辑。并在render中根据不一样的状态决定不一样的渲染结果。post

咱们须要根据实际状况合理的使用react建立组件的两种方式。这一点相当重要。上面两个例子我的认为仍是比较典型的能表明大多数状况。

react-router中的高阶组件

咱们在学习react的过程当中,会逐渐的与高阶组件打交道,react-router 中的 withRouter应该算是会最先接触到的高阶组件。咱们在使用的时候就知道,经过withRouter包装的组件,咱们能够在props中访问到location, router等对象,这正是withRouter经过高阶组件的方式传递过来的。

import React, { Component } from 'react';
import { withRouter } from 'react-router';

class Home extends Component {
    componentDidMount() {
        const { router } = this.props;

        router.push('/');
    }
    render() {
        return (
            <div className="my-home">...</div>
        )
    }
}
export default withRouter(Home);

咱们能够来看看在react-router v4withRouter的源码。

import React from 'react';
import PropTypes from 'prop-types';
import hoistStatics from 'hoist-non-react-statics';
import Route from './Route';

// 传入基础组件做为参数
const withRouter = (Component) => {

    // 建立中间组件
    const C = (props) => {
        const { wrappedComponentRef, ...remainingProps } = props;
        return (
            <Route render={routeComponentProps => (
                // wrappedComponentRef 用来解决高阶组件没法正确获取到ref的问题
                <Component {...remainingProps} {...routeComponentProps} ref={wrappedComponentRef}/>
            )}/>
        )
    }

    C.displayName = `withRouter(${Component.displayName || Component.name})`;
    C.WrappedComponent = Component;
    C.propTypes = {
        wrappedComponentRef: PropTypes.func
    }

    // hoistStatics相似于Object.assign,用于解决基础组件由于高阶组件的包裹而丢失静态方法的问题
    return hoistStatics(C, Component);
}

export default withRouter;

若是对于高阶组件的例子你已经熟知,那么withRouter的源码其实很容易理解。它作所的工做就仅仅只是把routeComponentProps传入基础组件而已。

另外还须要注意点是在该源码中,解决了两个由于高阶组件带来的问题,一个是通过高阶组件包裹的组件在使用时没法经过ref正确获取到对应的值。二是基础组件的静态方法也会由于高阶组件的包裹会丢失。不过好在这段源码已经给咱们提供了对应的解决方案。所以若是咱们在使用中须要处理这2点的话,按照这里的方式来作就能够了。

可是一般状况下,咱们也不多会在自定义的组件中添加静态方法和使用ref。若是在开发中确实遇到了必须使用它们,就必定要注意高阶组件的这2个问题并认真解决。

clipboard.png

相关文章
相关标签/搜索