blog icon indicating copy to clipboard operation
blog copied to clipboard

React路由权限控制,附带错误边界,移动端路由转场动画,加载中动画

Open xianzou opened this issue 4 years ago • 0 comments

根据后端返回的权限进行页面路由的控制,大致思路,进入页面获取用户信息,具体操作可以放入到布局组件中。

router.js代码如下:

// 动画样式
import './index.scss';
import React, { Suspense, useEffect, useState } from 'react';
import {
    Switch,
    withRouter,
    HashRouter
} from 'react-router-dom';
// 错误监控
import fundebug from 'fundebug-javascript';
//react 动画
import { CSSTransition, TransitionGroup  } from 'react-transition-group';

import { getContextPath, userInfoContext } from '@/utils';
// 加载中组件
import Loading from '@/components/Loading';
//布局组件,用于控制权限跳转
import BasicLayout from '@/BasicLayout/BasicLayout';
// 错误信息页面
import InternalServerError from '@/components/InternalServerError';

// 路由
import { routes } from '@/config/routes';
// 用户信息接口
import { getMe } from '@/services';

fundebug.apikey = '520615ea954491e455201d8ba453e5b0e92087bf0fe5add9f5e5f0c50701bea6';
window._fundebug = fundebug;

// 动画效果的class
const ANIMATION_MAP = {
    PUSH: 'forward',
    POP: 'back',
    REPLACE: 'replace'
};

const NotFound = () => {
    useEffect(() => {
        const { origin } = location;
        const contextPath = getContextPath();

        window.location = `${origin}${contextPath}404/index.html`;
    }, []);
    return null;
};

class ErrorBoundary extends React.Component {
    state = { hasError: false };
    // static getDerivedStateFromError() {
    //     return { hasError: true };
    // }
    componentDidCatch(error, info) {
        this.setState({ hasError: true });
        // 将component中的报错发送到Fundebug
        fundebug.notifyError(error, {
            metaData: {
                info
            }
        });
    }
    render() {
        if (this.state.hasError) {
            return <InternalServerError />;
        }
        return this.props.children;
    }
}

const Routes = withRouter(({ location, history }) => {

    const [animationEnd, setAnimationEnd] = useState(true);

    return (
        <TransitionGroup
            className={'router-wrapper'}
            childFactory={child => React.cloneElement(
                child,
                { classNames: ANIMATION_MAP[history.action] }
            )}
        >
            <CSSTransition
             timeout={500}
             key={location.pathname}
             onEnter={() => {
                    setAnimationEnd(false);
             }}
             onEntered={() => {
                    setAnimationEnd(true);
              }}
             >
                <div className="mobile-main">
                    <Suspense fallback={<Loading />} maxDuration={200}>
                        <Switch location={location}>
                            <BasicLayout
                                location={location}
                                history={history}
                                routerConfig={routes}
                                animationEnd={animationEnd}
                            />
                            <NotFound />
                        </Switch>
                    </Suspense>
                </div>
            </CSSTransition>
        </TransitionGroup>
    );
});


export default () => {

    const [loaded, setLoaded] = useState(false);
    const [userInfo, setUserInfo] = useState({});
    // 获取用户信息
    const getUserInfo = async () => {
        const me = await getMe();

        setUserInfo(me);
        setLoaded(true);
    };
    // 更新用户信息
    const updateUserInfo = () => {
        getUserInfo();
    };

    useEffect(() => {
        getUserInfo();
    }, []);
    
	// 如果获取用户信息没有返回结果,显示loading
    if (!loaded){
        return <Loading />;
    }
	//只有获取完用户信息才显示下面的路由
    return (
        <ErrorBoundary>
            <userInfoContext.Provider
                value={{  userInfo, updateUserInfo }}
            >
                <HashRouter>
                    <Routes />
                </HashRouter>
            </userInfoContext.Provider>
        </ErrorBoundary>
    );
};

utils/index.js


export const userInfoContext = createContext();

//获取文根,用户动态文根
export const getContextPath = () => {
    const basePath = window.SHARE.CONTEXT_PATH; 

    return basePath.replace(location.origin, '');
};

Loading.js

# 这里使用的是框架的样式,实际可以自己实现
import React from 'react';

const Loading = React.memo(() => {
    return (
        <div className="mshare-toast mshare-toast-mask">
            <span>
                <div className="mshare-toast-notice mshare-toast-notice-closable">
                    <div className="mshare-toast-notice-content">
                        <div className="mshare-toast-text mshare-toast-text-icon" role="alert" aria-live="assertive" >
                            <div className="circle-side" />
                            <div className="mshare-toast-text-info">加载中...</div>
                        </div>
                    </div>
                    <a className="mshare-toast-notice-close">
                        <span className="mshare-toast-notice-close-x" />
                    </a>
                </div>
            </span>
        </div>
    );
});


export default Loading;

InternalServerError.js

import styles from './InternalServerError.scss';
import React from 'react';
import img500 from '@/assets/images/500.png';

const InternalServerError = () => (
    <div className={styles.internalServerError}>
        <div className={styles.wrapper}>
            <img src={img500} alt="500" />
            <div>
                <h1>500</h1>
                {/* <p>抱歉,服务器出错了</p>*/}
                <p>抱歉,您访问的页面出错了,请刷新后重试</p>
            </div>
        </div>
    </div>
);

export default InternalServerError;

# InternalServerError.scss

.internalServerError {
    display: flex;
    justify-content: center;
    margin-top: 150px;

    .wrapper {
        display: flex;

        img { margin-right: 44px }
        > div {
            display: flex;
            flex-direction: column;
            justify-content: center;
        }
        h1, p {
            color: $color-text1-2;
            line-height: 1.0;
        }
        h1 { font-size: 60px }
        p {
            margin-top: 20px;
            font-size: 16px;
        }
    }
}
@include media(xs) {
    .internalServerError .wrapper {
        flex-direction: column;
        align-items: center;
        padding: 0 30px;

        img {
            margin-right: 0;
            width:50%;
        }
        h1 {
            display: none;
        }
        p {
            margin-top: 30px;
        }
    }
}

config/route.js

// 菜单路由,不用懒加载,不然会闪一下
import { lazy } from 'react';
import ParentsCenter from '@/pages/mobile/routes/ParentsCenter/ParentsCenter';
import Messages from '@/pages/mobile/routes/Messages/Messages';
import My from '@/pages/mobile/routes/My/My';

export const routes = [
    {
        key: 'messages',
        path: '/messages',
        component: Messages,
        sceneConfig: {
            enter: 'from-right',
            exit: 'to-right'
        }
    },
    {
        key: 'parents-center',
        path: '/parents-center',
        component: ParentsCenter,
        sceneConfig: {
            enter: 'from-right',
            exit: 'to-right'
        }
    },
    {
        key: 'my',
        path: '/my',
        component: My,
        sceneConfig: {
            enter: 'from-right',
            exit: 'to-right'
        }
    },
    {
        key: 'login',
        path: '/login',
        component: lazy(() => import('@/pages/mobile/routes/Login/Login')),
        sceneConfig: {
            enter: 'from-right',
            exit: 'to-right'
        }
    },
    # ...其他动态路由
];

index.scss

:global{
  
  .router-wrapper {
    // overflow: hidden;
    height: 100%;
    // background-color: rgba(0,0,0,0.9);
  }
  .mobile-main{
    position: relative;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    height: 100%;
    display: flex;
    flex-direction: column;
    >div:first-child{
      position: relative;
      flex: 1;
      overflow: auto;
    }
    // >div:first-child{
    //   background-color: #fff;
    //   min-height: 100%;
    // }
  }
 
  .forward-enter {
    opacity: 0;
    transform: translateX(100%);
    box-shadow: 0 4px 7px rgba(0,0,0,.4);
    transition-timing-function:ease-in;
  }
  
  .forward-enter-active {
    opacity: 1;
    transform: translateX(0);
    transition: all 120ms;
    transition-timing-function:ease-in;
  }
  
  .forward-exit {
    opacity: 1;
    transform: translateX(0);
    transition-timing-function:ease-in;
  }
  
  .forward-exit-active {
    opacity: 0;
    transform: translateX(-100%);
    transition: all 120ms;
    transition-timing-function:ease-in;
  }
  
  .back-enter {
    opacity: 0;
    transform: translateX(-100%);
    box-shadow: 0 4px 7px rgba(0,0,0,.4);
    transition-timing-function:ease-in;
  }
  
  .back-enter-active {
    opacity: 1;
    transform: translateX(0);
    transition: all 120ms;
    transition-timing-function:ease-in;
  }
  
  .back-exit {
    opacity: 1;
    transform: translateX(0);
    transition-timing-function:ease-in;
  }
  
  .back-exit-active {
    opacity: 0;
    transform: translate(100%);
    transition: all 120ms;
    transition-timing-function:ease-in;

  }

  .replace-enter >div:first-child{
    opacity: 0.01;
  }
  
  .replace-enter-active >div:first-child{
    opacity: 1;
  transition: opacity 250ms ease-in;
  }
  
  .replace-exit >div:first-child{
    opacity: 1;
  }
  
  .replace-exit-active >div:first-child{
    opacity: 0.01;
    transition: opacity 250ms ease-in;
  }
    .circle-side{
        width: 50px;
        height: 50px;
        border: 3px solid #D0D3D4;
        border-top-color: #626567;
        border-radius: 50%;
        animation: spin 1.5s linear infinite;
        margin: 0 auto;
    }
}

BasicLayout.js

import React, { Fragment, useEffect, useContext, useState } from 'react';
import { Route } from 'react-router-dom';
import Menu, { getUrlPath } from '@/components/Menu';
import Loading from '@/components/Loading';
import { SYSTEM_STATUS_MAP } from '@/config/status';
import { getContextPath, userInfoContext } from '@/utils';

const menuConfig = ['messages', 'parents-center', 'my']; //底部菜单


const BasicLayout = props => {
    const { routerConfig, location, history } = props;
    const { pathname } = location;
    const { getMsgCount, userInfo: me } = useContext(userInfoContext);
    const [isCheckMe, setIsCheckMe] = useState(false); // 判断是否走完权限控制
	//获取当前的进入的路由信息
    const targetRouterConfig = routerConfig.find(
        item => item.path === pathname
    );

    // 是否渲染底部菜单
    const isRenderMenu = menuConfig.some(name => name === getUrlPath(history));

    useEffect(() => {
        if (!me) {
            const { origin } = window.location;
            const contextPath = getContextPath();

            if (pathname !== '/login'){
                window.location = `${origin}${contextPath}expired/index.html`;
                return undefined;
            }
        } else {
            // 没有登录
            if (me.status === SYSTEM_STATUS_MAP.NO_LOGIN && pathname !== '/login'){
                history.push('/login');
                return undefined;
            }
            // 没有注册绑定过,且访问的不是注册绑定页面     有注册但未填写信息  没有注册  正常的
            if (me.status === SYSTEM_STATUS_MAP.NO_REGISTER && pathname !== '/binding-children') {
                history.push('/binding-children');
                return undefined;
            }
            // 有注册 不允许在进入注册页
            if (me.status === SYSTEM_STATUS_MAP.NORMAL && pathname === '/binding-children') {
                history.push('/parents-center');
                return undefined;
            }
            // 没有绑定孩子
            if (me.status === SYSTEM_STATUS_MAP.NO_BIND_CHILD && pathname !== '/my-children' && pathname !== '/query-info') {
                history.push('/my-children');
                return undefined;
            }
        }
        // 走完了useEffect,防止页面渲染
        setIsCheckMe(true);
    }, []);//eslint-disable-line
    // 如果还没有走完useEffect,不渲染页面
    if (!isCheckMe){
        return null;
    }
   //动画是否结束
    if (!animationEnd){
            return null;
        }
    // 用户信息不存在
    if (!me){
        return <Loading />;
    }
    // 访问的路由不存在
    if (!targetRouterConfig){
        const { origin } = window.location;
        const contextPath = getContextPath();

        window.location = `${origin}${contextPath}404/index.html`;
        return null;
    }
    return (
        <Fragment>
            <Route exact path={pathname} component={targetRouterConfig.component} />
            {
                isRenderMenu && <Menu menuConfig={menuConfig} />
            }
        </Fragment>
    );
};

export default BasicLayout;

# SYSTEM_STATUS_MAP
// me.do的status对应的用户状态
export const SYSTEM_STATUS_MAP = {
    NO_LOGIN: '0', // 没有登录
    NO_REGISTER: '1', // 未填写信息
    NO_BIND_CHILD: '2', // 无绑定孩子
    NORMAL: '3'// 正常
};

其他

1、移动端布局使用flex,内容区域flex:1,底部菜单height:50px,外层div设置display: flex;flex-direction: column;

2、html加载中样式

<style>
    body{
      background-color: #f5f5f5;
    }
    .boxLoading {
      width: 50px;
      height: 50px;
      margin: auto;
      position: absolute;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
    }
    .boxLoading:before {
      content: "";
      width: 50px;
      height: 5px;
      background: #000;
      opacity: 0.1;
      position: absolute;
      top: 59px;
      left: 0;
      border-radius: 50%;
      animation: shadow 0.5s linear infinite;
    }
    .boxLoading:after {
      content: "";
      width: 50px;
      height: 50px;
      background: #0099dd;
      animation: animate 0.5s linear infinite;
      position: absolute;
      top: 0;
      left: 0;
      border-radius: 3px;
    }

    .loading_tip {
      text-align: center;
      color: #cdcdcd;
      font-size: 14px;
      word-break: keep-all;
      margin-top: 80px;
    }
    @keyframes animate {
      17% {
        border-bottom-right-radius: 3px;
      }
      25% {
        transform: translateY(9px) rotate(22.5deg);
      }
      50% {
        transform: translateY(18px) scale(1, 0.9) rotate(45deg);
        border-bottom-right-radius: 40px;
      }
      75% {
        transform: translateY(9px) rotate(67.5deg);
      }
      100% {
        transform: translateY(0) rotate(90deg);
      }
    }
    @keyframes shadow {
      0%,
      100% {
        transform: scale(1, 1);
      }
      50% {
        transform: scale(1.2, 1);
      }
    }
    #root{
      height: 100%;
    }
    @keyframes spin {
        0% {
          transform: rotate(0deg);
        }
    
        100% {
          transform: rotate(360deg);
        }
      }
  </style>
<div id="root">
    <div class="boxLoading">
      <p class="loading_tip">加载中...</p>
    </div>
  </div>

xianzou avatar Dec 21 '20 09:12 xianzou