13

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

案例:

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

如果在每个页面都写一段判断逻辑未免麻烦,因此可以借助高阶组件来处理这部分逻辑。

先创建一个高阶组件

// 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;

然后在基础组件中使用

// 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);

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

// 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);

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

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中根据不同的状态决定不同的渲染结果。

我们需要根据实际情况合理的使用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


这波能反杀
12.6k 声望2.7k 粉丝