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

相關文章
相關標籤/搜索