在前后端分离的项目中,我们通常会遇到实现前端路由权限的需求以及全局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;