react最流行的生態(tài)替代antdpro搭建輕量級(jí)后臺(tái)管理
前言
你是否經(jīng)歷過(guò)公司的產(chǎn)品和 ui 要求左側(cè)菜單欄要改成設(shè)計(jì)圖上的樣子? 苦惱 antd-pro 強(qiáng)綁定的 pro-layout 菜單欄不能自定義?你可以使用 umi,但是就要根據(jù)它的約定來(lái)開(kāi)發(fā),捆綁全家桶等等。手把手教你搭一個(gè)輕量級(jí)的后臺(tái)模版,包括路由的權(quán)限、動(dòng)態(tài)菜單等等。
為方便使用 antd 組件庫(kù),你可以改成任意你喜歡的。數(shù)據(jù)請(qǐng)求的管理使用 react-query,類似 useRequest,但是更加將大。樣式使用 tailwindcss 加 styled-components,因?yàn)?antd v5 將使用 css in js。路由的權(quán)限和菜單管理使用 react-router-auth-plus。。。
項(xiàng)目初始化
# npm 7+ npm create vite spirit-admin -- --template react-ts
react-router-auth-plus (權(quán)限路由、動(dòng)態(tài)菜單解決方案) 倉(cāng)庫(kù)地址 文章地址
等等...
數(shù)據(jù)請(qǐng)求 + mock
配置 axios
設(shè)置攔截器,并在 main.ts 入口文件中引入這個(gè)文件,使其在全局生效
// src/axios.ts
import axios, { AxiosError } from "axios";
import { history } from "./main";
// 設(shè)置 response 攔截器,狀態(tài)碼為 401 清除 token,并返回 login 頁(yè)面。
axios.interceptors.response.use(
function (response) {
return response;
},
function (error: AxiosError) {
if (error.response?.status === 401) {
localStorage.removeItem("token");
// 在 react 組件外使用路由方法, 使用方式會(huì)在之后路由配置時(shí)講到
history.push("/login");
}
return Promise.reject(error);
}
);
// 設(shè)置 request 攔截器,請(qǐng)求中的 headers 帶上 token
axios.interceptors.request.use(function (request) {
request.headers = {
authorization: localStorage.getItem("token") || "",
};
return request;
});
配置 react-query
在 App 外層包裹 QueryClientProvider,設(shè)置默認(rèn)選項(xiàng),窗口重新聚焦時(shí)和失敗時(shí)不重新請(qǐng)求。
// 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>
);
我們只有兩個(gè)請(qǐng)求,登錄和獲取當(dāng)前用戶,src 下新建 hooks 文件夾,再分別建 query、mutation 文件夾,query 是請(qǐng)求數(shù)據(jù)用的,mutation 是發(fā)起數(shù)據(jù)操作的請(qǐng)求用的。具體可以看 react-query 文檔
獲取當(dāng)前用戶接口
// src/hooks/query/useCurrentUserQuery.ts
import { useQuery } from "@tanstack/react-query";
import axios from "axios";
import { queryClient } from "../../main";
// useQuery 需要唯一的 key,react-query v4 是數(shù)組格式
const currentUserQueryKey = ["currentUser"];
// 查詢當(dāng)前用戶,如果 localStorage 里沒(méi)有 token,則不請(qǐng)求
export const useCurrentUserQuery = () =>
useQuery(currentUserQueryKey, () => axios.get("/api/me"), {
enabled: !!localStorage.getItem("token"),
});
// 可以在其它頁(yè)面獲取 useCurrentUserQuery 的數(shù)據(jù)
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
數(shù)據(jù)請(qǐng)求使用 react-query + axios, 因?yàn)橹挥袃蓚€(gè)請(qǐng)求,/login(登錄) 和 /me(當(dāng)前用戶),直接使用 express 本地 mock 一下數(shù)據(jù)。新建 mock 文件夾,分別建立 index.js 和 users.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: "用戶名或密碼錯(cuò)誤" });
}
}, 2000);
});
// 當(dāng)前用戶接口,請(qǐng)求時(shí)需在 headers 中帶上 authorization,若不正確返回 401 狀態(tài)碼。根據(jù)用戶類型返回權(quán)限和用戶名
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: "請(qǐng)登錄" });
} 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());
// 接口前綴統(tǒng)一加上 /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,用來(lái)熱更新 mock 文件的。npm run mock 啟動(dòng) express 服務(wù)。
"scripts": {
...
"mock": "nodemon mock/index.js"
}
現(xiàn)在在項(xiàng)目中還不能使用,需要在 vite 中配置 proxy 代理
// vite.config.ts
export default defineConfig({
plugins: [react()],
server: {
proxy: {
"/api": {
target: "http://localhost:3000",
changeOrigin: true,
},
},
},
});
路由權(quán)限配置
路由和權(quán)限這塊使用的方案是 react-router-auth-plus,具體介紹見(jiàn)上篇
路由文件
新建一個(gè) router.tsx,引入頁(yè)面文件,配置項(xiàng)目所用到的所有路由,配置上權(quán)限。這里我們擴(kuò)展一下 AuthRouterObject 類型,自定義一些參數(shù),例如左側(cè)菜單的 icon、name 等。設(shè)置上 /account/center 和 /application 路由需要對(duì)應(yīng)的權(quán)限。
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[];
}
// 只需在需要權(quán)限的路由配置 auth 即可
export const routers: MetaRouterObject[] = [
{ path: "/", element: <Navigate to="/home" replace /> },
{ path: "/login", element: <Login /> },
{
element: <BasicLayout />,
children: [
{
path: "/home",
element: <Home />,
name: "主頁(yè)",
icon: <HomeOutlined />,
},
{
path: "/account",
name: "個(gè)人",
icon: <UserOutlined />,
children: [
{
path: "/account",
element: <Navigate to="/account/center" replace />,
},
{
path: "/account/center",
name: "個(gè)人中心",
element: <Center />,
},
{
path: "/account/setting",
name: "個(gè)人設(shè)置",
element: <Setting />,
// 權(quán)限
auth: ["setting"],
},
],
},
{
path: "/application",
element: <Application />,
// 權(quán)限
auth: ["application"],
name: "應(yīng)用",
icon: <AppstoreOutlined />,
},
],
},
{ path: "*", element: <NotFound /> },
];
main.tsx
使用 HistoryRouter,在組件外可以路由跳轉(zhuǎn),這樣就可以在 axios 攔截器中引入 history 跳轉(zhuǎn)路由了。
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();
// 獲取當(dāng)前用戶,localStorage 里沒(méi) token 時(shí)不請(qǐng)求
const { data, isFetching } = useCurrentUserQuery();
// 第一次進(jìn)入程序,不是 login 頁(yè)面且沒(méi)有 token,跳轉(zhuǎn)到 login 頁(yè)面
useEffect(() => {
if (!localStorage.getItem("token") && location.pathname !== "/login") {
navigate("/login");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// 第一次進(jìn)入程序,若是 login 頁(yè)面,且 token 沒(méi)過(guò)期(code 為 0),自動(dòng)登錄進(jìn)入 home 頁(yè)面。使用 useLayoutEffect 可以避免看到先閃一下 login 頁(yè)面,再跳到 home 頁(yè)面。
useLayoutEffect(() => {
if (location.pathname === "/login" && data?.data.code === 0) {
navigate("/home");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.data.code]);
return useAuthRouters({
// 傳入當(dāng)前用戶的權(quán)限
auth: data?.data.data.auth || [],
// 若正在獲取當(dāng)前用戶,展示 loading
render: (element) =>
isFetching ? (
<div className="flex justify-center items-center h-full">
<Spin size="large" />
</div>
) : (
element
),
// 若進(jìn)入沒(méi)權(quán)限的頁(yè)面,顯示 403 頁(yè)面
noAuthElement: () => <NotAuth />,
routers,
});
}
export default App;
頁(yè)面編寫(xiě)
login 頁(yè)面
html 省略,antd Form 表單賬號(hào)密碼輸入框和一個(gè)登錄按鈕
// 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);
// 請(qǐng)求當(dāng)前用戶
await queryClient.refetchQueries(currentUserQueryKey);
navigate("/home")
message.success("登錄成功");
} else {
message.error(data.message);
}
};
return ...
};
BasicLayout
BasicLayout 這里簡(jiǎn)寫(xiě)一下,具體可以看源碼。BasicLayout 會(huì)接收到 routers,在 routers.tsx 配置的 children 會(huì)自動(dòng)傳入 routers,不需要像這樣手動(dòng)傳入<BasicLayout routers={[]} />。Outlet 相當(dāng)于 children,是 react-router v6 新增的。
將 routers 傳入到 Outlet 的 context 中。之后就可以在頁(yè)面中用 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 }) => {
// 樣式省略簡(jiǎn)寫(xiě)
return (
<Layout>
<Header>
...頂部
</Header>
<Layout hasSider>
<Slider>
...左側(cè)菜單
</Slider>
<Layout>
<Layout.Content>
<Outlet context={{ routers }} />
</Layout.Content>
</Layout>
</Layout>
</Layout>
);
};
動(dòng)態(tài)菜單欄
把左側(cè)菜單欄單獨(dú)拆分成一個(gè)組件,在 BasicLayout 中引入,傳入 routers 參數(shù)。
// 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";
// 轉(zhuǎn)化成 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 先過(guò)濾掉沒(méi)有權(quán)限的路由。再通過(guò) getMenuItems 獲得 antd Menu組件需要的格式
const menuItems = getMenuItems(useAuthMenus(routers));
// 默認(rèn)打開(kāi)的下拉菜單
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}
{/* 選中菜單回調(diào),導(dǎo)航到其路由 */}
onSelect={({ key }) => navigate(key)}
/>
);
};
export default SlideMenu;
封裝頁(yè)面通用面包屑
封裝一個(gè)在 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 方法匹配路由數(shù)組
const match = matchRoutes(routers, location);
// 處理一下生成面包屑數(shù)組
const breadcrumbs =
(match || []).reduce((total: MetaRouterObject[], current) => {
if ((current.route as MetaRouterObject).name) {
total.push(current.route);
}
return total;
}, []);
// 最后一個(gè)面包屑不能點(diǎn)擊,前面的都能點(diǎn)擊跳轉(zhuǎn)
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;
這樣就能在頁(yè)面中引入這個(gè)組件使用了,如果你想在每個(gè)頁(yè)面中都使用,可以寫(xiě)在 BasicLayout 的 Content 中,并在 routers 配置中加一個(gè) hideBreadcrumb 選項(xiàng),通過(guò)配置來(lái)控制是否在當(dāng)前路由頁(yè)面顯示面包屑。
function Home() {
return (
<div>
<PageBreadcrumb />
</div>
);
}
總結(jié)
react 的生態(tài)是越來(lái)越多樣化了,學(xué)的東西也越來(lái)越多(太卷了)??偟膩?lái)說(shuō),上面所使用的一些庫(kù),或多或少都要有所了解。應(yīng)該都要鍛煉自己有具備能搭建一個(gè)簡(jiǎn)易版的后臺(tái)管理模版的能力 github 地址
以上就是react最流行的生態(tài)替代antdpro搭建輕量級(jí)后臺(tái)管理的詳細(xì)內(nèi)容,更多關(guān)于react生態(tài)輕量級(jí)后臺(tái)管理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
React中的render何時(shí)執(zhí)行過(guò)程
這篇文章主要介紹了React中的render何時(shí)執(zhí)行過(guò)程,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-04-04
react.js實(shí)現(xiàn)頁(yè)面登錄跳轉(zhuǎn)示例
本文主要介紹了react.js實(shí)現(xiàn)頁(yè)面登錄跳轉(zhuǎn)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-01-01
聊聊ant?design?charts?獲取后端接口數(shù)據(jù)展示問(wèn)題
今天在做項(xiàng)目的時(shí)候遇到幾個(gè)讓我很頭疼的問(wèn)題,一個(gè)是通過(guò)后端接口成功訪問(wèn)并又返回?cái)?shù)據(jù),但拿不到數(shù)據(jù)值。其二是直接修改state中的data,console中數(shù)組發(fā)生變化但任然數(shù)據(jù)未顯示,這篇文章主要介紹了ant?design?charts?獲取后端接口數(shù)據(jù)展示,需要的朋友可以參考下2022-05-05
react-native DatePicker日期選擇組件的實(shí)現(xiàn)代碼
本篇文章主要介紹了react-native DatePicker日期選擇組件的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,有興趣的可以了解下2017-09-09
react實(shí)現(xiàn)頁(yè)面水印效果的全過(guò)程
大家常常關(guān)注的是網(wǎng)站圖片增加水印,而很少關(guān)注頁(yè)面水印,其實(shí)這個(gè)需求也是比較常見(jiàn)的,比如公文系統(tǒng)、合同系統(tǒng)等,這篇文章主要給大家介紹了關(guān)于react實(shí)現(xiàn)頁(yè)面水印效果的相關(guān)資料,需要的朋友可以參考下2021-09-09
Redux thunk中間件及執(zhí)行原理詳細(xì)分析
redux的核心概念其實(shí)很簡(jiǎn)單:將需要修改的state都存入到store里,發(fā)起一個(gè)action用來(lái)描述發(fā)生了什么,用reducers描述action如何改變state tree,這篇文章主要介紹了Redux thunk中間件及執(zhí)行原理分析2022-09-09

