blog
blog copied to clipboard
React路由权限控制,附带错误边界,移动端路由转场动画,加载中动画
根据后端返回的权限进行页面路由的控制,大致思路,进入页面获取用户信息,具体操作可以放入到布局组件中。
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>