当前位置 : 主页 > 网络编程 > JavaScript >

react最流行的生态替代antdpro搭建轻量级后台管理

来源:互联网 收集:自由互联 发布时间:2023-02-08
目录 前言 项目初始化 数据请求 + 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 (权限路由、动态菜单解决方案) 仓库地址 文章地址

    等等...

    数据请求 + 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: "http://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生态轻量级后台管理的资料请关注易盾网络其它相关文章!

    上一篇:vue获取input值的三种常用写法
    下一篇:没有了
    网友评论