前言

你是否经历过公司的产品和 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生态轻量级后台管理的资料请关注Devmax其它相关文章!

react最流行的生态替代antdpro搭建轻量级后台管理的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – 如何使用MagicalRecord设置Core Data轻量级迁移?

    解决方法MagicalRecord的重点在于为您管理:查看有关CoreData堆栈设置here的文档.

  6. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  7. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  8. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  9. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  10. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部