模拟路由守卫功能

就像之前React Router v5一章中所介绍的,React Router中没有提供路由守卫功能。路由守卫是Angular框架引入的,用于在一个路由的导航前和导航后进行额外的操作处理的机制。虽然React Router中没有直接提供路由守卫功能,但是在实际使用的时候是可以借助React的高阶组件功能来实现的。

路由守卫功能在应用中最经常用到的地方就是页面权限控制,所以为了方便在高阶组件中进行权限判断,这里先定义一个用于权限判断的Hook供高阶组件使用。这个Hook使用了来自Ramda函数式编程库中提供的工具函数,具体使用到的函数可以在顶部的import语句中查找。

import { difference, equals, length, lt, or } from 'ramda';

export function useAuthenticated() {
  const state = useContext<UserStore>(UserContext);

  return state.isLoggedIn && !state.isTokenExpired;
}

export function useAuthorization() {
  const state = useContext<PrivilegeStore>(PrivelegeContext);

  function hasAll(...privileges: string[]): boolean {
    const differ = difference(privileges, state.privileges);
    return or(state.isAdmin, equals(length(differ), 0));
  }

  function hasAny(...privileges: string[]): boolean {
    const originalPrivilegesSize = length(privileges);
    const differ = difference(privileges, state.privileges);
    return or(state.isAdmin, lt(length(differ), originalPrivilegesSize));
  }

  return { hasAll, hasAny };
}

接下来就是要定义一个用于包装其他组件的高阶组件了。

import { defaultTo, isEmpty, isNil } from 'ramda';
import { Component } from 'react';
import { useLocation } from 'react-router';
import { Navigate, Route } from 'react-router-dom';

export interface AuthorizeOptions {
  all: string[];
  any: string[];
}

export const requireAuthorize = (Comp: Component, options?: Partial<AuthorizeOptions>): Component => {
  const altedOptions = defaultTo({ any: [], all: [] })(options);
  return props => {
    const isAuthenticated = useAuthenticated();
    const { hasAll, hasAny } = useAuthorization();
    const location = useLocation();

    let requiredAllPrivileges: string[] = defaultTo([])(altedOptions.all);
    let requiredAnyPrivileges: string[] = defaultTo([])(altedOptions.any);

    const isUserMatchNeeds =
      (isEmpty(requiredAllPrivileges) && isEmpty(requiredAnyPrivileges)) ||
      (isEmpty(requiredAllPrivileges) ? hasAny(...requiredAnyPrivileges) : hasAll(...requiredAllPrivileges));

    if (isAuthenticated && isUserMatchNeeds) {
      return <Comp {...props} />;
    } else {
      return <Navigate to="/login" state={{ from: location }} replace={true} />;
    }
  };
};

然后就可以像下面这样在导出组件的时候声明当前要导出的组件所需要的用户权限了。

export default requireAuthorize(Task, { any: ['manager'] });