如何用React生态替代Ant Design Pro快速搭建轻量级后台管理系统?

2026-04-01 12:481阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计3164个文字,预计阅读时间需要13分钟。

如何用React生态替代Ant Design Pro快速搭建轻量级后台管理系统?

目录+前言+项目初始化+数据请求++Mock+配置+axios+配置+react-query+Mock+路由权限配置+路由文件+main.tsx+App.tsx+页面编写+login+页面+BasicLayout+动态菜单栏+封装页面通用组件包+总结+前言+你是否经历过这样的项目开发?

目录
  • 前言
  • 项目初始化
  • 数据请求 + mock
    • 配置 axios
    • 配置 react-query
    • mock
  • 路由权限配置
    • 路由文件
    • main.tsx
    • App.tsx
  • 页面编写
    • login 页面
    • BasicLayout
    • 动态菜单栏
    • 封装页面通用面包屑
  • 总结

    前言

    你是否经历过公司的产品和 ui 要求左侧菜单栏要改成设计图上的样子? 苦恼 antd-pro 强绑定的 pro-layout 菜单栏不能自定义?你可以使用 umi,但是就要根据它的约定来开发,捆绑全家桶等等。手把手教你搭一个轻量级的后台模版,包括路由的权限、动态菜单等等。

    为方便使用 antd 组件库,你可以改成任意你喜欢的。数据请求的管理使用 react-query,类似 useRequest,但是更加将大。样式使用 tailwindcssstyled-components,因为 antd v5 将使用 css in js。路由的权限和菜单管理使用 react-router-auth-plus。。。

    仓库地址

    项目初始化

    vite

    # npm 7+ npm create vite spirit-admin -- --template react-ts

    antd

    tailwindcss

    styled-components

    react-query

    axios

    react-router

    react-router-auth-plus (权限路由、动态菜单解决方案) 仓库地址 文章地址

    如何用React生态替代Ant Design Pro快速搭建轻量级后台管理系统?

    等等...

    数据请求 + mock

    配置 axios

    设置拦截器,并在 main.ts 入口文件中引入这个文件,使其在全局生效

    // src/axios.ts import axios, { AxiosError } from "axios"; import { history } from "./main"; // 设置 response 拦截器,状态码为 401 清除 token,并返回 login 页面。 axios.interceptors.response.use( function (response) { return response; }, function (error: AxiosError) { if (error.response?.status === 401) { localStorage.removeItem("token"); // 在 react 组件外使用路由方法, 使用方式会在之后路由配置时讲到 history.push("/login"); } return Promise.reject(error); } ); // 设置 request 拦截器,请求中的 headers 带上 token axios.interceptors.request.use(function (request) { request.headers = { authorization: localStorage.getItem("token") || "", }; return request; });

    配置 react-query

    在 App 外层包裹 QueryClientProvider,设置默认选项,窗口重新聚焦时和失败时不重新请求。

    // App.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false, }, }, }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode> );

    我们只有两个请求,登录和获取当前用户,src 下新建 hooks 文件夹,再分别建 query、mutation 文件夹,query 是请求数据用的,mutation 是发起数据操作的请求用的。具体可以看 react-query 文档

    获取当前用户接口

    // src/hooks/query/useCurrentUserQuery.ts import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { queryClient } from "../../main"; // useQuery 需要唯一的 key,react-query v4 是数组格式 const currentUserQueryKey = ["currentUser"]; // 查询当前用户,如果 localStorage 里没有 token,则不请求 export const useCurrentUserQuery = () => useQuery(currentUserQueryKey, () => axios.get("/api/me"), { enabled: !!localStorage.getItem("token"), }); // 可以在其它页面获取 useCurrentUserQuery 的数据 export const getCurrentUser = () => { const data: any = queryClient.getQueryData(currentUserQueryKey); return { username: data?.data.data.username, }; };

    登录接口

    // src/hooks/mutation/useLoginMutation.ts import { useMutation } from "@tanstack/react-query"; import axios from "axios"; export const useLoginMutation = () => useMutation((data) => axios.post("/api/login", data));

    mock

    数据请求使用 react-query + axios, 因为只有两个请求,/login(登录) 和 /me(当前用户),直接使用 express 本地 mock 一下数据。新建 mock 文件夹,分别建立 index.jsusers.js

    // users.js 存放两种类型的用户 export const users = [ { username: "admin", password: "admin" }, { username: "employee", password: "employee" }, ];

    // index.js 主文件 import express from "express"; import { users } from "./users.js"; const app = express(); const port = 3000; const router = express.Router(); // 登录接口,若成功返回 token,这里模拟 token 只有两种情况 router.post("/login", (req, res) => { setTimeout(() => { const username = req.body.username; const password = req.body.password; const user = users.find((user) => user.username === username); if (user && password === user.password) { res.status(200).json({ code: 0, token: user.username === "admin" ? "admin-token" : "employee-token", }); } else { res.status(200).json({ code: -1, message: "用户名或密码错误" }); } }, 2000); }); // 当前用户接口,请求时需在 headers 中带上 authorization,若不正确返回 401 状态码。根据用户类型返回权限和用户名 router.get("/me", (req, res) => { setTimeout(() => { const token = req.headers.authorization; if (!["admin-token", "employee-token"].includes(token)) { res.status(401).json({ code: -1, message: "请登录" }); } else { const auth = token === "admin-token" ? ["application", "setting"] : []; const username = token === "admin-token" ? "admin" : "employee"; res.status(200).json({ code: 0, data: { auth, username } }); } }, 2000); }); app.use(express.json()); // 接口前缀统一加上 /api app.use("/api", router); // 禁用 304 缓存 app.disable("etag"); app.listen(port, () => { console.log(`Example app listening on port ${port}`); });

    package.json 中的 scripts 添加一条 mock 命令,需安装 nodemon,用来热更新 mock 文件的。npm run mock 启动 express 服务。

    "scripts": { ... "mock": "nodemon mock/index.js" }

    现在在项目中还不能使用,需要在 vite 中配置 proxy 代理

    // vite.config.ts export default defineConfig({ plugins: [react()], server: { proxy: { "/api": { target: "localhost:3000", changeOrigin: true, }, }, }, });

    路由权限配置

    路由和权限这块使用的方案是 react-router-auth-plus,具体介绍见上篇

    路由文件

    新建一个 router.tsx,引入页面文件,配置项目所用到的所有路由,配置上权限。这里我们扩展一下 AuthRouterObject 类型,自定义一些参数,例如左侧菜单的 icon、name 等。设置上 /account/center/application 路由需要对应的权限。

    import { AppstoreOutlined, HomeOutlined, UserOutlined, } from "@ant-design/icons"; import React from "react"; import { AuthRouterObject } from "react-router-auth-plus"; import { Navigate } from "react-router-dom"; import BasicLayout from "./layouts/BasicLayout"; import Application from "./pages/application"; import Home from "./pages/home"; import Login from "./pages/login"; import NotFound from "./pages/404"; import Setting from "./pages/account/setting"; import Center from "./pages/account/center"; export interface MetaRouterObject extends AuthRouterObject { name?: string; icon?: React.ReactNode; hideInMenu?: boolean; hideChildrenInMenu?: boolean; children?: MetaRouterObject[]; } // 只需在需要权限的路由配置 auth 即可 export const routers: MetaRouterObject[] = [ { path: "/", element: <Navigate to="/home" replace /> }, { path: "/login", element: <Login /> }, { element: <BasicLayout />, children: [ { path: "/home", element: <Home />, name: "主页", icon: <HomeOutlined />, }, { path: "/account", name: "个人", icon: <UserOutlined />, children: [ { path: "/account", element: <Navigate to="/account/center" replace />, }, { path: "/account/center", name: "个人中心", element: <Center />, }, { path: "/account/setting", name: "个人设置", element: <Setting />, // 权限 auth: ["setting"], }, ], }, { path: "/application", element: <Application />, // 权限 auth: ["application"], name: "应用", icon: <AppstoreOutlined />, }, ], }, { path: "*", element: <NotFound /> }, ];

    main.tsx

    使用 HistoryRouter,在组件外可以路由跳转,这样就可以在 axios 拦截器中引入 history 跳转路由了。

    import { createBrowserHistory } from "history"; import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom"; export const history = createBrowserHistory({ window }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <HistoryRouter history={history}> <App /> </HistoryRouter> </QueryClientProvider> </React.StrictMode> );

    App.tsx

    import { useAuthRouters } from "react-router-auth-plus"; import { routers } from "./router"; import NotAuth from "./pages/403"; import { Spin } from "antd"; import { useEffect, useLayoutEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useCurrentUserQuery } from "./hooks/query"; function App() { const navigate = useNavigate(); const location = useLocation(); // 获取当前用户,localStorage 里没 token 时不请求 const { data, isFetching } = useCurrentUserQuery(); // 第一次进入程序,不是 login 页面且没有 token,跳转到 login 页面 useEffect(() => { if (!localStorage.getItem("token") && location.pathname !== "/login") { navigate("/login"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 第一次进入程序,若是 login 页面,且 token 没过期(code 为 0),自动登录进入 home 页面。使用 useLayoutEffect 可以避免看到先闪一下 login 页面,再跳到 home 页面。 useLayoutEffect(() => { if (location.pathname === "/login" && data?.data.code === 0) { navigate("/home"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data?.data.code]); return useAuthRouters({ // 传入当前用户的权限 auth: data?.data.data.auth || [], // 若正在获取当前用户,展示 loading render: (element) => isFetching ? ( <div className="flex justify-center items-center h-full"> <Spin size="large" /> </div> ) : ( element ), // 若进入没权限的页面,显示 403 页面 noAuthElement: () => <NotAuth />, routers, }); } export default App;

    页面编写

    login 页面

    html 省略,antd Form 表单账号密码输入框和一个登录按钮

    // src/pages/login/index.tsx const Login: FC = () => { const navigate = useNavigate(); const { mutateAsync: login, isLoading } = useLoginMutation(); // Form 提交 const handleFinish = async (values: any) => { const { data } = await login(values); if (data.code === 0) { localStorage.setItem("token", data.token); // 请求当前用户 await queryClient.refetchQueries(currentUserQueryKey); navigate("/home") message.success("登录成功"); } else { message.error(data.message); } }; return ... };

    BasicLayout

    BasicLayout 这里简写一下,具体可以看源码。BasicLayout 会接收到 routers,在 routers.tsx 配置的 children 会自动传入 routers,不需要像这样手动传入<BasicLayout routers={[]} />Outlet 相当于 children,是 react-router v6 新增的。

    将 routers 传入到 Outlet 的 context 中。之后就可以在页面中用 useOutletContext 获取到 routers 了。

    // src/layouts import { Layout } from "antd"; import { Outlet } from "react-router-dom"; import styled from "styled-components"; // 使用 styled-components 覆盖样式 const Header = styled(Layout.Header)` height: 48px; line-height: 48px; padding: 0 16px; `; // 同上 const Slider = styled(Layout.Sider)` .ant-layout-sider-children { display: flex; flex-direction: column; } `; interface BasicLayoutProps { routers?: MetaRouterObject[]; } const BasicLayout: FC<BasicLayoutProps> = ({ routers }) => { // 样式省略简写 return ( <Layout> <Header> ...顶部 </Header> <Layout hasSider> <Slider> ...左侧菜单 </Slider> <Layout> <Layout.Content> <Outlet context={{ routers }} /> </Layout.Content> </Layout> </Layout> </Layout> ); };

    动态菜单栏

    把左侧菜单栏单独拆分成一个组件,在 BasicLayout 中引入,传入 routers 参数。

    // src/layouts/BasicLayout/components/SliderMenu.tsx import { Menu } from "antd"; import { FC, useEffect, useState } from "react"; import { useAuthMenus } from "react-router-auth-plus"; import { useNavigate } from "react-router-dom"; import { useLocation } from "react-router-dom"; import { MetaRouterObject } from "../../../router"; import { ItemType } from "antd/lib/menu/hooks/useItems"; // 转化成 antd Menu 组件需要的格式。只有配置了 name 和不隐藏的才展示 const getMenuItems = (routers: MetaRouterObject[]): ItemType[] => { const menuItems = routers.reduce((total: ItemType[], router) => { if (router.name && !router.hideInMenu) { total?.push({ key: router.path as string, icon: router.icon, label: router.name, children: router.children && router.children.length > 0 && !router.hideChildrenInMenu ? getMenuItems(router.children) : undefined, }); } return total; }, []); return menuItems; }; interface SlideMenuProps { routers: MetaRouterObject[]; } const SlideMenu: FC<SlideMenuProps> = ({ routers }) => { const location = useLocation(); const navigate = useNavigate(); const [selectedKeys, setSelectedKeys] = useState<string[]>([]); // useAuthMenus 先过滤掉没有权限的路由。再通过 getMenuItems 获得 antd Menu组件需要的格式 const menuItems = getMenuItems(useAuthMenus(routers)); // 默认打开的下拉菜单 const defaultOpenKey = menuItems.find((i) => location.pathname.startsWith(i?.key as string) )?.key as string; // 选中菜单 useEffect(() => { setSelectedKeys([location.pathname]); }, [location.pathname]); return ( <Menu style={{ borderRightColor: "white" }} className="h-full" mode="inline" selectedKeys={selectedKeys} defaultOpenKeys={defaultOpenKey ? [defaultOpenKey] : []} items={menuItems} {/* 选中菜单回调,导航到其路由 */} onSelect={({ key }) => navigate(key)} /> ); }; export default SlideMenu;

    封装页面通用面包屑

    封装一个在 BasicLayout 下全局通用的面包屑。

    // src/components/PageBreadcrumb.tsx import { Breadcrumb } from "antd"; import { FC } from "react"; import { Link, matchRoutes, useLocation, useOutletContext, } from "react-router-dom"; import { MetaRouterObject } from "../router"; const PageBreadcrumb: FC = () => { const location = useLocation(); // 获取在 BasicLayout 中传入的 routers const { routers } = useOutletContext<{ routers: MetaRouterObject[] }>(); // 使用 react-router 的 matchRoutes 方法匹配路由数组 const match = matchRoutes(routers, location); // 处理一下生成面包屑数组 const breadcrumbs = (match || []).reduce((total: MetaRouterObject[], current) => { if ((current.route as MetaRouterObject).name) { total.push(current.route); } return total; }, []); // 最后一个面包屑不能点击,前面的都能点击跳转 return ( <Breadcrumb> {breadcrumbs.map((i, index) => ( <Breadcrumb.Item key={i.path}> {index === breadcrumbs.length - 1 ? ( i.name ) : ( <Link to={i.path as string}>{i.name}</Link> )} </Breadcrumb.Item> ))} </Breadcrumb> ); }; export default PageBreadcrumb;

    这样就能在页面中引入这个组件使用了,如果你想在每个页面中都使用,可以写在 BasicLayout 的 Content 中,并在 routers 配置中加一个 hideBreadcrumb 选项,通过配置来控制是否在当前路由页面显示面包屑。

    function Home() { return ( <div> <PageBreadcrumb /> </div> ); }

    总结

    react 的生态是越来越多样化了,学的东西也越来越多(太卷了)。总的来说,上面所使用的一些库,或多或少都要有所了解。应该都要锻炼自己有具备能搭建一个简易版的后台管理模版的能力github 地址

    以上就是react最流行的生态替代antdpro搭建轻量级后台管理的详细内容,更多关于react生态轻量级后台管理的资料请关注易盾网络其它相关文章!

    本文共计3164个文字,预计阅读时间需要13分钟。

    如何用React生态替代Ant Design Pro快速搭建轻量级后台管理系统?

    目录+前言+项目初始化+数据请求++Mock+配置+axios+配置+react-query+Mock+路由权限配置+路由文件+main.tsx+App.tsx+页面编写+login+页面+BasicLayout+动态菜单栏+封装页面通用组件包+总结+前言+你是否经历过这样的项目开发?

    目录
    • 前言
    • 项目初始化
    • 数据请求 + mock
      • 配置 axios
      • 配置 react-query
      • mock
    • 路由权限配置
      • 路由文件
      • main.tsx
      • App.tsx
    • 页面编写
      • login 页面
      • BasicLayout
      • 动态菜单栏
      • 封装页面通用面包屑
    • 总结

      前言

      你是否经历过公司的产品和 ui 要求左侧菜单栏要改成设计图上的样子? 苦恼 antd-pro 强绑定的 pro-layout 菜单栏不能自定义?你可以使用 umi,但是就要根据它的约定来开发,捆绑全家桶等等。手把手教你搭一个轻量级的后台模版,包括路由的权限、动态菜单等等。

      为方便使用 antd 组件库,你可以改成任意你喜欢的。数据请求的管理使用 react-query,类似 useRequest,但是更加将大。样式使用 tailwindcssstyled-components,因为 antd v5 将使用 css in js。路由的权限和菜单管理使用 react-router-auth-plus。。。

      仓库地址

      项目初始化

      vite

      # npm 7+ npm create vite spirit-admin -- --template react-ts

      antd

      tailwindcss

      styled-components

      react-query

      axios

      react-router

      react-router-auth-plus (权限路由、动态菜单解决方案) 仓库地址 文章地址

      如何用React生态替代Ant Design Pro快速搭建轻量级后台管理系统?

      等等...

      数据请求 + mock

      配置 axios

      设置拦截器,并在 main.ts 入口文件中引入这个文件,使其在全局生效

      // src/axios.ts import axios, { AxiosError } from "axios"; import { history } from "./main"; // 设置 response 拦截器,状态码为 401 清除 token,并返回 login 页面。 axios.interceptors.response.use( function (response) { return response; }, function (error: AxiosError) { if (error.response?.status === 401) { localStorage.removeItem("token"); // 在 react 组件外使用路由方法, 使用方式会在之后路由配置时讲到 history.push("/login"); } return Promise.reject(error); } ); // 设置 request 拦截器,请求中的 headers 带上 token axios.interceptors.request.use(function (request) { request.headers = { authorization: localStorage.getItem("token") || "", }; return request; });

      配置 react-query

      在 App 外层包裹 QueryClientProvider,设置默认选项,窗口重新聚焦时和失败时不重新请求。

      // App.tsx import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false, }, }, }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode> );

      我们只有两个请求,登录和获取当前用户,src 下新建 hooks 文件夹,再分别建 query、mutation 文件夹,query 是请求数据用的,mutation 是发起数据操作的请求用的。具体可以看 react-query 文档

      获取当前用户接口

      // src/hooks/query/useCurrentUserQuery.ts import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { queryClient } from "../../main"; // useQuery 需要唯一的 key,react-query v4 是数组格式 const currentUserQueryKey = ["currentUser"]; // 查询当前用户,如果 localStorage 里没有 token,则不请求 export const useCurrentUserQuery = () => useQuery(currentUserQueryKey, () => axios.get("/api/me"), { enabled: !!localStorage.getItem("token"), }); // 可以在其它页面获取 useCurrentUserQuery 的数据 export const getCurrentUser = () => { const data: any = queryClient.getQueryData(currentUserQueryKey); return { username: data?.data.data.username, }; };

      登录接口

      // src/hooks/mutation/useLoginMutation.ts import { useMutation } from "@tanstack/react-query"; import axios from "axios"; export const useLoginMutation = () => useMutation((data) => axios.post("/api/login", data));

      mock

      数据请求使用 react-query + axios, 因为只有两个请求,/login(登录) 和 /me(当前用户),直接使用 express 本地 mock 一下数据。新建 mock 文件夹,分别建立 index.jsusers.js

      // users.js 存放两种类型的用户 export const users = [ { username: "admin", password: "admin" }, { username: "employee", password: "employee" }, ];

      // index.js 主文件 import express from "express"; import { users } from "./users.js"; const app = express(); const port = 3000; const router = express.Router(); // 登录接口,若成功返回 token,这里模拟 token 只有两种情况 router.post("/login", (req, res) => { setTimeout(() => { const username = req.body.username; const password = req.body.password; const user = users.find((user) => user.username === username); if (user && password === user.password) { res.status(200).json({ code: 0, token: user.username === "admin" ? "admin-token" : "employee-token", }); } else { res.status(200).json({ code: -1, message: "用户名或密码错误" }); } }, 2000); }); // 当前用户接口,请求时需在 headers 中带上 authorization,若不正确返回 401 状态码。根据用户类型返回权限和用户名 router.get("/me", (req, res) => { setTimeout(() => { const token = req.headers.authorization; if (!["admin-token", "employee-token"].includes(token)) { res.status(401).json({ code: -1, message: "请登录" }); } else { const auth = token === "admin-token" ? ["application", "setting"] : []; const username = token === "admin-token" ? "admin" : "employee"; res.status(200).json({ code: 0, data: { auth, username } }); } }, 2000); }); app.use(express.json()); // 接口前缀统一加上 /api app.use("/api", router); // 禁用 304 缓存 app.disable("etag"); app.listen(port, () => { console.log(`Example app listening on port ${port}`); });

      package.json 中的 scripts 添加一条 mock 命令,需安装 nodemon,用来热更新 mock 文件的。npm run mock 启动 express 服务。

      "scripts": { ... "mock": "nodemon mock/index.js" }

      现在在项目中还不能使用,需要在 vite 中配置 proxy 代理

      // vite.config.ts export default defineConfig({ plugins: [react()], server: { proxy: { "/api": { target: "localhost:3000", changeOrigin: true, }, }, }, });

      路由权限配置

      路由和权限这块使用的方案是 react-router-auth-plus,具体介绍见上篇

      路由文件

      新建一个 router.tsx,引入页面文件,配置项目所用到的所有路由,配置上权限。这里我们扩展一下 AuthRouterObject 类型,自定义一些参数,例如左侧菜单的 icon、name 等。设置上 /account/center/application 路由需要对应的权限。

      import { AppstoreOutlined, HomeOutlined, UserOutlined, } from "@ant-design/icons"; import React from "react"; import { AuthRouterObject } from "react-router-auth-plus"; import { Navigate } from "react-router-dom"; import BasicLayout from "./layouts/BasicLayout"; import Application from "./pages/application"; import Home from "./pages/home"; import Login from "./pages/login"; import NotFound from "./pages/404"; import Setting from "./pages/account/setting"; import Center from "./pages/account/center"; export interface MetaRouterObject extends AuthRouterObject { name?: string; icon?: React.ReactNode; hideInMenu?: boolean; hideChildrenInMenu?: boolean; children?: MetaRouterObject[]; } // 只需在需要权限的路由配置 auth 即可 export const routers: MetaRouterObject[] = [ { path: "/", element: <Navigate to="/home" replace /> }, { path: "/login", element: <Login /> }, { element: <BasicLayout />, children: [ { path: "/home", element: <Home />, name: "主页", icon: <HomeOutlined />, }, { path: "/account", name: "个人", icon: <UserOutlined />, children: [ { path: "/account", element: <Navigate to="/account/center" replace />, }, { path: "/account/center", name: "个人中心", element: <Center />, }, { path: "/account/setting", name: "个人设置", element: <Setting />, // 权限 auth: ["setting"], }, ], }, { path: "/application", element: <Application />, // 权限 auth: ["application"], name: "应用", icon: <AppstoreOutlined />, }, ], }, { path: "*", element: <NotFound /> }, ];

      main.tsx

      使用 HistoryRouter,在组件外可以路由跳转,这样就可以在 axios 拦截器中引入 history 跳转路由了。

      import { createBrowserHistory } from "history"; import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom"; export const history = createBrowserHistory({ window }); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <HistoryRouter history={history}> <App /> </HistoryRouter> </QueryClientProvider> </React.StrictMode> );

      App.tsx

      import { useAuthRouters } from "react-router-auth-plus"; import { routers } from "./router"; import NotAuth from "./pages/403"; import { Spin } from "antd"; import { useEffect, useLayoutEffect } from "react"; import { useLocation, useNavigate } from "react-router-dom"; import { useCurrentUserQuery } from "./hooks/query"; function App() { const navigate = useNavigate(); const location = useLocation(); // 获取当前用户,localStorage 里没 token 时不请求 const { data, isFetching } = useCurrentUserQuery(); // 第一次进入程序,不是 login 页面且没有 token,跳转到 login 页面 useEffect(() => { if (!localStorage.getItem("token") && location.pathname !== "/login") { navigate("/login"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 第一次进入程序,若是 login 页面,且 token 没过期(code 为 0),自动登录进入 home 页面。使用 useLayoutEffect 可以避免看到先闪一下 login 页面,再跳到 home 页面。 useLayoutEffect(() => { if (location.pathname === "/login" && data?.data.code === 0) { navigate("/home"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data?.data.code]); return useAuthRouters({ // 传入当前用户的权限 auth: data?.data.data.auth || [], // 若正在获取当前用户,展示 loading render: (element) => isFetching ? ( <div className="flex justify-center items-center h-full"> <Spin size="large" /> </div> ) : ( element ), // 若进入没权限的页面,显示 403 页面 noAuthElement: () => <NotAuth />, routers, }); } export default App;

      页面编写

      login 页面

      html 省略,antd Form 表单账号密码输入框和一个登录按钮

      // src/pages/login/index.tsx const Login: FC = () => { const navigate = useNavigate(); const { mutateAsync: login, isLoading } = useLoginMutation(); // Form 提交 const handleFinish = async (values: any) => { const { data } = await login(values); if (data.code === 0) { localStorage.setItem("token", data.token); // 请求当前用户 await queryClient.refetchQueries(currentUserQueryKey); navigate("/home") message.success("登录成功"); } else { message.error(data.message); } }; return ... };

      BasicLayout

      BasicLayout 这里简写一下,具体可以看源码。BasicLayout 会接收到 routers,在 routers.tsx 配置的 children 会自动传入 routers,不需要像这样手动传入<BasicLayout routers={[]} />Outlet 相当于 children,是 react-router v6 新增的。

      将 routers 传入到 Outlet 的 context 中。之后就可以在页面中用 useOutletContext 获取到 routers 了。

      // src/layouts import { Layout } from "antd"; import { Outlet } from "react-router-dom"; import styled from "styled-components"; // 使用 styled-components 覆盖样式 const Header = styled(Layout.Header)` height: 48px; line-height: 48px; padding: 0 16px; `; // 同上 const Slider = styled(Layout.Sider)` .ant-layout-sider-children { display: flex; flex-direction: column; } `; interface BasicLayoutProps { routers?: MetaRouterObject[]; } const BasicLayout: FC<BasicLayoutProps> = ({ routers }) => { // 样式省略简写 return ( <Layout> <Header> ...顶部 </Header> <Layout hasSider> <Slider> ...左侧菜单 </Slider> <Layout> <Layout.Content> <Outlet context={{ routers }} /> </Layout.Content> </Layout> </Layout> </Layout> ); };

      动态菜单栏

      把左侧菜单栏单独拆分成一个组件,在 BasicLayout 中引入,传入 routers 参数。

      // src/layouts/BasicLayout/components/SliderMenu.tsx import { Menu } from "antd"; import { FC, useEffect, useState } from "react"; import { useAuthMenus } from "react-router-auth-plus"; import { useNavigate } from "react-router-dom"; import { useLocation } from "react-router-dom"; import { MetaRouterObject } from "../../../router"; import { ItemType } from "antd/lib/menu/hooks/useItems"; // 转化成 antd Menu 组件需要的格式。只有配置了 name 和不隐藏的才展示 const getMenuItems = (routers: MetaRouterObject[]): ItemType[] => { const menuItems = routers.reduce((total: ItemType[], router) => { if (router.name && !router.hideInMenu) { total?.push({ key: router.path as string, icon: router.icon, label: router.name, children: router.children && router.children.length > 0 && !router.hideChildrenInMenu ? getMenuItems(router.children) : undefined, }); } return total; }, []); return menuItems; }; interface SlideMenuProps { routers: MetaRouterObject[]; } const SlideMenu: FC<SlideMenuProps> = ({ routers }) => { const location = useLocation(); const navigate = useNavigate(); const [selectedKeys, setSelectedKeys] = useState<string[]>([]); // useAuthMenus 先过滤掉没有权限的路由。再通过 getMenuItems 获得 antd Menu组件需要的格式 const menuItems = getMenuItems(useAuthMenus(routers)); // 默认打开的下拉菜单 const defaultOpenKey = menuItems.find((i) => location.pathname.startsWith(i?.key as string) )?.key as string; // 选中菜单 useEffect(() => { setSelectedKeys([location.pathname]); }, [location.pathname]); return ( <Menu style={{ borderRightColor: "white" }} className="h-full" mode="inline" selectedKeys={selectedKeys} defaultOpenKeys={defaultOpenKey ? [defaultOpenKey] : []} items={menuItems} {/* 选中菜单回调,导航到其路由 */} onSelect={({ key }) => navigate(key)} /> ); }; export default SlideMenu;

      封装页面通用面包屑

      封装一个在 BasicLayout 下全局通用的面包屑。

      // src/components/PageBreadcrumb.tsx import { Breadcrumb } from "antd"; import { FC } from "react"; import { Link, matchRoutes, useLocation, useOutletContext, } from "react-router-dom"; import { MetaRouterObject } from "../router"; const PageBreadcrumb: FC = () => { const location = useLocation(); // 获取在 BasicLayout 中传入的 routers const { routers } = useOutletContext<{ routers: MetaRouterObject[] }>(); // 使用 react-router 的 matchRoutes 方法匹配路由数组 const match = matchRoutes(routers, location); // 处理一下生成面包屑数组 const breadcrumbs = (match || []).reduce((total: MetaRouterObject[], current) => { if ((current.route as MetaRouterObject).name) { total.push(current.route); } return total; }, []); // 最后一个面包屑不能点击,前面的都能点击跳转 return ( <Breadcrumb> {breadcrumbs.map((i, index) => ( <Breadcrumb.Item key={i.path}> {index === breadcrumbs.length - 1 ? ( i.name ) : ( <Link to={i.path as string}>{i.name}</Link> )} </Breadcrumb.Item> ))} </Breadcrumb> ); }; export default PageBreadcrumb;

      这样就能在页面中引入这个组件使用了,如果你想在每个页面中都使用,可以写在 BasicLayout 的 Content 中,并在 routers 配置中加一个 hideBreadcrumb 选项,通过配置来控制是否在当前路由页面显示面包屑。

      function Home() { return ( <div> <PageBreadcrumb /> </div> ); }

      总结

      react 的生态是越来越多样化了,学的东西也越来越多(太卷了)。总的来说,上面所使用的一些库,或多或少都要有所了解。应该都要锻炼自己有具备能搭建一个简易版的后台管理模版的能力github 地址

      以上就是react最流行的生态替代antdpro搭建轻量级后台管理的详细内容,更多关于react生态轻量级后台管理的资料请关注易盾网络其它相关文章!