前端权限-react

通过react-router-dom6的loader实现菜单权限和登录拦截。

react-router-dom中route介绍:

const router = createBrowserRouter([
  {
    // it renders this element
    element: <Team />,

    // when the URL matches this segment
    path: "teams/:teamId",

    // with this data loaded before rendering
    loader: async ({ request, params }) => {
      return fetch(
        `/fake/api/teams/${params.teamId}.json`,
        { signal: request.signal }
      );
    },

    // performing this mutation when data is submitted to it
    action: async ({ request }) => {
      return updateFakeTeam(await request.formData());
    },

    // and renders this element in case something went wrong
    errorElement: <ErrorBoundary />,
  },
]);

//或组件版
const router = createBrowserRouter(
  createRoutesFromElements(
    <Route
      element={<Team />}
      path="teams/:teamId"
      loader={async ({ params }) => {
        return fetch(
          `/fake/api/teams/${params.teamId}.json`
        );
      }}
      action={async ({ request }) => {
        return updateFakeTeam(await request.formData());
      }}
      errorElement={<ErrorBoundary />}
    />
  )
);

渲染内容为element,当渲染出错会渲染errorElement(该组件中通过hook:useRouteError可以获取到错误信息,之后ui显示处理),path为*匹配任何路由用来兜底

  • loader:在内容渲染前为element提供数据=》获取数据读,在element组件中通过useLoaderData()获取数据,在整个项目中任何地方获取用useRouteLoaderData(此时路由里边要加上id,该hook通过id获取对应数据)
createBrowserRouter([
  {
    path: "/",
    loader: () => fetchUser(),
    element: <Root />,
    id: "root",
    children: [
      {
        path: "jobs/:jobId",
        loader: loadJob,
        element: <JobListing />,
      },
    ],
  },
]);

const user = useRouteLoaderData("root");
  • action:解决于form组件等,可以接收组件提交的数据并处理=》处理数据写,在组件中获取其数据用useActionData
import { useActionData, Form } from "react-router-dom";

export default function Login() {
  //页面初次渲染,获取为空  并不会触发上面的action submit后才会触发
  const errors = useActionData();

  return (
    <Form method="post">
      <p>
        <input type="text" name="email" />
        {errors?.email && <span>{errors.email}</span>}
      </p>

      <p>
        <input type="text" name="password" />
        {errors?.password && <span>{errors.password}</span>}
      </p>

      <p>
        <button type="submit">Sign up</button>
      </p>
    </Form>
  );
}

export async function loginAction({ request }) {
  const formData = await request.formData();
  const email = formData.get("email");
  const password = formData.get("password");
  const errors = {};

  // validate the fields
  if (typeof email !== "string" || !email.includes("@")) {
    errors.email =
      "That doesn't look like an email address";
  }

  if (typeof password !== "string" || password.length < 6) {
    errors.password = "Password must be > 6 characters";
  }

  // return data if we have errors
  if (Object.keys(errors).length) {
    return errors;
  }

  // otherwise create the user and redirect
  await createUser(email, password);
  return redirect("/dashboard");
}

loader和action中可以使用redirect跳转,注意redirect跳转 前面加上return,loader和action要求必须有return,不跳转并直接导致loader/action报错,redirect不在其他地方使用,其他地方用navigate/useNavigate实现重定向

 权限控制实现

使用loader去获取用户权限信息,组件上包裹一层权限组件,里面获取loader中获取的权限信息,之后判断要显示的组件是否在权限内。

import { lazy, Suspense } from "react";
import { createBrowserRouter, Navigate, redirect } from "react-router-dom";
import type { RouteObject } from "react-router-dom";
import ErrorBoundary from "../components/ErrorBoundary";

// 不需要懒加载的页面组件
import Layout from "../pages/Layout";
import Permission from "../components/Permission";
import NotFound from "../container/NotFound";

// 需要懒加载的页面组件
const Home = lazy(() => import("../container/Home"));
const About = lazy(() => import("../container/About"));
const Setting = lazy(() => import("../container/Setting"));
const Login = lazy(() => import("../container/Login"));

/**
 * @param Component 懒加载的组件
 * @param code 用于判断权限的字段(你可以自己定)
 * @returns
 */
const LazyLoad = (
  Component: React.LazyExoticComponent<() => JSX.Element>,
  code?: string
) => {
  return (
    <Permission code={code}>
      <Suspense fallback={<div>loading...</div>}>
        <Component />
      </Suspense>
    </Permission>
  );
};

export interface LoginInfoProps {
  email: string;
  password: string;
}

export interface UserInfo {
  age: number;
  permissionRoutes: string[];
  code: number;
}
export type UserInfoProps = LoginInfoProps & UserInfo;
/**
 * @description 模拟请求用户信息
 * @returns
 */
export const getUserInfo = (): Promise<UserInfoProps> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        email: "[email protected]",
        password:"123456",
        age: 12,
        permissionRoutes: ["home", "list"],
        code: 0,
      });
    }, 1000);
  });
};

/**
 * @description 这个loader函数会在路由渲染前触发,所以可以用来做路由权限控制和登陆重定向
 * @description (取代请求拦截器中的登陆重定向)
 * @description 这个loader函数返回值可以在页面中通过 useRouteLoaderData(id)或者useLoaderData获取
 */
const rootLoader = async () => {
  console.log("页面加载前请求用户信息");

  const user = await getUserInfo();
  if (!user) {
    //注意加return 否则不跳转 继续向下执行 导致loader报错
    return redirect("/login");
  }

  // 这里用假的接口模拟下
  const { permissionRoutes, email, age, code } = user;
  // 假设20001代表登陆过期
  if (code === 20001) {
    return redirect("/login");
  }
  return {
    name: email,
    age,
    permissionRoutes,
  };
};

const routerConfig: RouteObject[] = [
  {
    path: "/",
    element: <Navigate to="/home" />,
  },
  {
    path: "/",
    id: "root",
    errorElement: <ErrorBoundary />,
    element: <Layout />,
    loader: rootLoader,
    children: [
      {
        path: "/home",
        element: LazyLoad(Home, "home"),
      },
      {
        path: "/list",
        element: LazyLoad(About, "about"),
      },
      {
        path: "/detail",
        element: LazyLoad(Setting, "setting"),
      },
    ],
  },
  {
    path: "/login",
    action: loginAction,
    element: LazyLoad(Login),
  },
  {
    path: "*",
    element: <NotFound />,
  },
];

export const routes = createBrowserRouter(routerConfig);

Permission组件: 

import { FC, PropsWithChildren } from "react";
import { useRouteLoaderData } from "react-router-dom";
import type { UserInfo } from "../routers";

interface Iprops {
  code?: string;
}

const Permission: FC<PropsWithChildren<Iprops>> = (props) => {
  // 这个root是我们在前面路由中定义了 id: 'root'
  const loaderData = useRouteLoaderData("root") as UserInfo;
  const { children, code } = props;
  if (!code || loaderData?.permissionRoutes?.includes(code)) {
    return <>{children}</>;
  }
  return <div>403...</div>;
};

export default Permission;

ErrorBoundary组件: 

import { useRouteError } from "react-router-dom";

const ErrorBoundary = () => {
  const err = useRouteError() as any;
  return (
    <div>
      <p>出错啦~</p>
      <p>错误信息: {err.message}</p>
    </div>
  );
};

export default ErrorBoundary;

 Layout组件:Outlet用于渲染子组件内容,路由配置时不设置path而是添加index则代表时默认路由<Route index element={Default}></Route>

import { Outlet, useLoaderData } from "react-router-dom";

function Layout() {
  const data = useLoaderData();

  return (
    <main>
      <h1>
        welcome {data.name} 年龄:{data.age}
      </h1>
      <Outlet />
    </main>
  );
}

export default Layout;

loader在组件渲染前加载数据,可以防止页面先进去之后数据返回来在判断权限不符合跳转兜底页要好,类似于vue-router的router.beforeEach在页面跳转前判断是否有权限跳转

其他:借助于react自定义hook+context实现 

React-Router v6 实现登录验证流程 - 掘金

react-router v6路由拦截/路由守卫/路由鉴权_#Undefined的博客-CSDN博客_react路由拦截和路由守卫

猜你喜欢

转载自blog.csdn.net/CamilleZJ/article/details/129023896