React 路由守卫和全局loading控制

5,503 阅读3分钟

在前后端分离的项目中,我们通常会遇到实现前端路由权限的需求以及全局loading效果的需求,在Vue项目中,我们可以通过路由守卫beforEach、afterEach这个两个钩子函数来实现进入一个路由时的全局loading效果。而vue-router也提供了灵活的路由配置项允许我们赋予路由更多的信息,包括权限等等。反观react-router并没有直接提供给这样的组件。虽然说vue-router本身就提供了灵活的配置,但是React高阶组件也赋予了我们大展身手的机会。

封装路由组件

const App: React.FC = () => (
  <Provider store={store}>
    <div className="App">
      <Switch>
        <AuthRoute config={RouteConfig} />
      </Switch>
    </div>
  </Provider>
);

export default withRouter(App);

在最外部我们不使用react-router提供的Route的组件,而是使用我们自己封装的路由组件,这个组件接受一个config参数,传入路由配置,这样我们也可以像vue中那样编写路由配置文件了。

路由配置文件

定义单个路由配置的类型

export interface RouteItem {
  path: string;
  component?: FC;
  auth?: boolean;
}

最后export出的路由配置信息,就是由RouteItem组成的数组。path代表路由路径,component表示对应的组件,auth表示是否需要鉴权,如果有多种角色的话,那么将auth设置成角色名称,后面增加一下判断方式便可。

全局loading的Redux设计

既然要实现全局的loading,那么使用redux最合适不过了。 这里就直接贴代码了,redux的知识就不细说了。 由于使用了combineReducers,所有我们把loading的状态放在了app这个reducer中。

actionTypes.ts

const SET_LOADING = 'SET_LOADING';

export default {
  /**
   * 设置页面的loading状态
   */
  SET_LOADING,
};

app.action.ts

import actionTypes from './actionTypes';

export const setLoading = (newStatus: boolean) => ({
  type: actionTypes.SET_LOADING,
  data: newStatus,
});

app.reducer.ts

import actionTypes from './actionTypes';

export interface AppState {
  loading: boolean;
}

const defaultState: AppState = {
  loading: false,
};

export default (state = defaultState, action: any) => {
  switch (action.type) {
    case actionTypes.SET_LOADING:
      return { ...state, loading: action.data };
    default:
      return state;
  }
};

实现AuthRoute组件

由于 AuthRoute 组件放在了 Switch 组件内部,React Router 还自动为 AuthRoute 注入了 location 属性,当地址栏的路由发生变化时,就会触发 location 属性对象上的 pathname 属性发生变化,我们根据这个变化,再去匹配先前写好的路由配置获得相应的组件重新渲染就可以了。

实现全局loading

我们只需要在Route组件的外部包裹一层Spin组件就可以了,spin组件的loading状态就是redux中的loading,如果需要根据网络请求来决定loading时间,只需要在相应的组件里设置loading的值就可以了,为了方便看效果,我这里就直接用定时器了。

代码

const AuthRoute: React.FC<any> = props => {
  const dispatch = useDispatch();
  const loading: boolean = useSelector((state: Store) => state.app.loading);

  const { pathname } = props.location;
  const isLogin = localStorage.getItem('user_token');
  let timer = 0;

  useEffect(() => {
    window.scrollTo(0, 0);
    dispatch(setLoading(true));
    clearTimeout(timer);
    timer = window.setTimeout(() => {
      dispatch(setLoading(false));
    }, 1000);
  }, [pathname]);

  const targetRouterConfig: RouteItem = props.config.find(
    (v: RouteItem) => v.path === pathname
  );

  if (targetRouterConfig && !targetRouterConfig.auth && !isLogin) {
    const { component } = targetRouterConfig;
    return <Route exact path={pathname} component={component} />;
  }
  if (isLogin) {
    // 如果是登陆状态,想要跳转到登陆,重定向到主页
    if (pathname === '/login') {
      return <Redirect to="/" />;
    }
    // 如果路由合法,就跳转到相应的路由
    if (targetRouterConfig) {
      return (
        <Spin
          tip="Loading"
          size="large"
          spinning={loading}
          // indicator={<Icon type="loading" style={{ fontSize: 24 }} spin />}
          style={{ maxHeight: 'none' }}
        >
          <Route path={pathname} component={targetRouterConfig.component} />
        </Spin>
      );
    }
    // 如果路由不合法,重定向到 404 页面
    return <Redirect to="/404" />;
  }
  // 非登陆状态下,当路由合法时且需要权限校验时,跳转到登陆页面,要求登陆
  if (targetRouterConfig && targetRouterConfig.auth) {
    return <Redirect to="/login" />;
  }
  // 非登陆状态下,路由不合法时,重定向至 404
  return <Redirect to="/404" />;
};

export default AuthRoute;

参考文章