Preface
Have you ever experienced the company's products and ui requirements that the menu bar on the left should be changed to the design drawing? Trouble The strongly bound pro-layout menu bar of antd-pro cannot be customized? You can use umi, but you have to develop according to its agreement, bundled with family buckets, etc. I will teach you step by step to build a lightweight background template, including routing permissions, dynamic menus, etc.
To facilitate the use of the antd component library, you can change it to whatever you like. Management and use of data requestsreact-query
,similaruseRequest
, but it will be even bigger. Style usagetailwindcss
addstyled-components
, because antd v5 will use css in js. Route permissions and menu management usagereact-router-auth-plus
。。。
Warehouse address
Project Initialization
vite
# npm 7+ npm create vite spirit-admin -- --template react-ts
antd
tailwindcss
styled-components
react-query
axios
react-router
react-router-auth-plus (Permission routing, dynamic menu solution)Warehouse address Article address
etc...
Data request + mock
Configure axios
Set up the interceptor and introduce this file in the entry file to make it effective globally
// src/ import axios, { AxiosError } from "axios"; import { history } from "./main"; // Set the response interceptor, the status code is 401 to clear the token, and return to the login page.( function (response) { return response; }, function (error: AxiosError) { if (?.status === 401) { ("token"); // Use the routing method outside the react component, and the usage method will be mentioned later when routing configuration ("/login"); } return (error); } ); // Set the request interceptor, bring the tokens in the request(function (request) { = { authorization: ("token") || "", }; return request; });
Configure react-query
In the App outer layer wrapping QueryClientProvider, set default options, no re-requests are requested when the window is refocused and fails.
// import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false, }, }, }); (("root") as HTMLElement).render( <> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </> );
We only have two requests, log in and get the current user, create a new hooks folder under src, and then create a query and mutation folder respectively. query is used to request data, and mutation is used to initiate data operations. You can see for detailsreact-query documentation
Get the current user interface
// src/hooks/query/ import { useQuery } from "@tanstack/react-query"; import axios from "axios"; import { queryClient } from "../../main"; // useQuery requires a unique key, react-query v4 is the array formatconst currentUserQueryKey = ["currentUser"]; // Query the current user. If there is no token in localStorage, no requestexport const useCurrentUserQuery = () => useQuery(currentUserQueryKey, () => ("/api/me"), { enabled: !!("token"), }); // You can get the data of useCurrentUserQuery on other pagesexport const getCurrentUser = () => { const data: any = (currentUserQueryKey); return { username: data?., }; };
Login interface
// src/hooks/mutation/ import { useMutation } from "@tanstack/react-query"; import axios from "axios"; export const useLoginMutation = () => useMutation((data) => ("/api/login", data));
mock
Data requests use react-query + axios, because there are only two requests,/login
(Login) and/me
(Current user), use express local mock to directly use the data. Create a new mock folder and create it separatelyand
// Store two types of usersexport const users = [ { username: "admin", password: "admin" }, { username: "employee", password: "employee" }, ];
// Main fileimport express from "express"; import { users } from "./"; const app = express(); const port = 3000; const router = (); // Log in to the interface. If you successfully return token, there are only two cases of simulated tokens.("/login", (req, res) => { setTimeout(() => { const username = ; const password = ; const user = ((user) => === username); if (user && password === ) { (200).json({ code: 0, token: === "admin" ? "admin-token" : "employee-token", }); } else { (200).json({ code: -1, message: "Error in username or password" }); } }, 2000); }); // For the current user interface, you need to bring authorization in the headers when requesting, and if it is incorrect, return a 401 status code. Return permissions and username according to user type("/me", (req, res) => { setTimeout(() => { const token = ; if (!["admin-token", "employee-token"].includes(token)) { (401).json({ code: -1, message: "Please log in" }); } else { const auth = token === "admin-token" ? ["application", "setting"] : []; const username = token === "admin-token" ? "admin" : "employee"; (200).json({ code: 0, data: { auth, username } }); } }, 2000); }); (()); // Add /api to the interface prefix uniformly("/api", router); // Disable 304 cache("etag"); (port, () => { (`Example app listening on port ${port}`); });
existIn-house
scripts
Add a mock command, and you need to install nodemon to hot update the mock file.npm run mock
Start the express service.
"scripts": { ... "mock": "nodemon mock/" }
It is not available in the project yet, so you need to configure the proxy proxy in vite
// export default defineConfig({ plugins: [react()], server: { proxy: { "/api": { target: "http://localhost:3000", changeOrigin: true, }, }, }, });
Routing permission configuration
The route and permissions are used inreact-router-auth-plus, see the detailsPrevious article
Routing files
Create a new one, introduce the page file, configure all routes used by the project, and configure permissions. Let's expand hereAuthRouterObject
Type, customize some parameters, such as icon, name, etc. of the menu on the left. Settings/account/center
and/application
The routing requires corresponding permissions.
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?: ; hideInMenu?: boolean; hideChildrenInMenu?: boolean; children?: MetaRouterObject[]; } // Just configure auth on the route that requires permissionsexport const routers: MetaRouterObject[] = [ { path: "/", element: <Navigate to="/home" replace /> }, { path: "/login", element: <Login /> }, { element: <BasicLayout />, children: [ { path: "/home", element: <Home />, name: "Homepage", icon: <HomeOutlined />, }, { path: "/account", name: "personal", icon: <UserOutlined />, children: [ { path: "/account", element: <Navigate to="/account/center" replace />, }, { path: "/account/center", name: "Personal Center", element: <Center />, }, { path: "/account/setting", name: "Personal Settings", element: <Setting />, // Permissions auth: ["setting"], }, ], }, { path: "/application", element: <Application />, // Permissions auth: ["application"], name: "application", icon: <AppstoreOutlined />, }, ], }, { path: "*", element: <NotFound /> }, ];
Using HistoryRouter, you can route jumps outside the component, so that history jump routes can be introduced in the axios interceptor.
import { createBrowserHistory } from "history"; import { unstable_HistoryRouter as HistoryRouter } from "react-router-dom"; export const history = createBrowserHistory({ window }); (("root") as HTMLElement).render( <> <QueryClientProvider client={queryClient}> <HistoryRouter history={history}> <App /> </HistoryRouter> </QueryClientProvider> </> );
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(); // Get the current user and do not request it when there is no token in localStorage const { data, isFetching } = useCurrentUserQuery(); // The first time I entered the program, it was not a login page and there was no token, jump to the login page useEffect(() => { if (!("token") && !== "/login") { navigate("/login"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // If you enter the program for the first time, if it is a login page and the token has not expired (code is 0), you will automatically log in to the home page. Use useLayoutEffect to avoid seeing the login page flash first and then jump to the home page. useLayoutEffect(() => { if ( === "/login" && data?. === 0) { navigate("/home"); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data?.]); return useAuthRouters({ // Pass in the current user's permissions auth: data?. || [], // If you are getting the current user, display loading render: (element) => isFetching ? ( <div className="flex justify-center items-center h-full"> <Spin size="large" /> </div> ) : ( element ), // If you enter a page without permission, page 403 will be displayed noAuthElement: () => <NotAuth />, routers, }); } export default App;
Page writing
login page
html omitted, antd Form form account password input box and a login button
// src/pages/login/ const Login: FC = () => { const navigate = useNavigate(); const { mutateAsync: login, isLoading } = useLoginMutation(); // Form Submit const handleFinish = async (values: any) => { const { data } = await login(values); if ( === 0) { ("token", ); // Request the current user await (currentUserQueryKey); navigate("/home") ("Login successfully"); } else { (); } }; return ... };
BasicLayout
BasicLayout Here is abbreviation, you can see the source code for details. BasicLayout will receive routers,The configured children will be automatically passed in routers, and there is no need to pass them manually like this
<BasicLayout routers={[]} />
。Outlet
Equivalent tochildren
,yesreact-router v6
New.
Pass routers inOutlet
in the context. Then it can be used on the pageuseOutletContext
Get routers.
// src/layouts import { Layout } from "antd"; import { Outlet } from "react-router-dom"; import styled from "styled-components"; // Overwrite styles with styled-componentsconst Header = styled()` height: 48px; line-height: 48px; padding: 0 16px; `; // Same as aboveconst Slider = styled()` .ant-layout-sider-children { display: flex; flex-direction: column; } `; interface BasicLayoutProps { routers?: MetaRouterObject[]; } const BasicLayout: FC<BasicLayoutProps> = ({ routers }) => { // Abbreviation of style omitted return ( <Layout> <Header> ...top </Header> <Layout hasSider> <Slider> ...Left menu </Slider> <Layout> <> <Outlet context={{ routers }} /> </> </Layout> </Layout> </Layout> ); };
Dynamic menu bar
Split the left menu bar into a component, introduce it in BasicLayout, and pass the routers parameter.
// src/layouts/BasicLayout/components/ 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"; // Convert to the format required by the antd Menu component. Only those that are configured with name and not hidden will be displayedconst getMenuItems = (routers: MetaRouterObject[]): ItemType[] => { const menuItems = ((total: ItemType[], router) => { if ( && !) { total?.push({ key: as string, icon: , label: , children: && > 0 && ! ? getMenuItems() : 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 filter out routes without permissions first. Then use getMenuItems to obtain the format required by the antd Menu component const menuItems = getMenuItems(useAuthMenus(routers)); // The drop-down menu that opens by default const defaultOpenKey = ((i) => (i?.key as string) )?.key as string; // Select menu useEffect(() => { setSelectedKeys([]); }, []); return ( <Menu style={{ borderRightColor: "white" }} className="h-full" mode="inline" selectedKeys={selectedKeys} defaultOpenKeys={defaultOpenKey ? [defaultOpenKey] : []} items={menuItems} {/* Select the menu callback and navigate to its route */} onSelect={({ key }) => navigate(key)} /> ); }; export default SlideMenu;
Package page universal bread crumbs
Encapsulate a bread crumb that is universally common under BasicLayout.
// src/components/ 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(); // Get the routers passed in BasicLayout const { routers } = useOutletContext<{ routers: MetaRouterObject[] }>(); // Use react-router's matchRoutes method to match the route array const match = matchRoutes(routers, location); // Process it to generate a breadcrumb array const breadcrumbs = (match || []).reduce((total: MetaRouterObject[], current) => { if (( as MetaRouterObject).name) { (); } return total; }, []); // The last bread crumb cannot be clicked, the previous one can be clicked and redirected return ( <Breadcrumb> {((i, index) => ( < key={}> {index === - 1 ? ( ) : ( <Link to={ as string}>{}</Link> )} </> ))} </Breadcrumb> ); }; export default PageBreadcrumb;
This will introduce this component to the page and use it. If you want to use it in each page, you can write it in the BasicLayout Content and add one in the routers configuration.hideBreadcrumb
Options to control whether to display breadcrumbs on the current routing page through configuration.
function Home() { return ( <div> <PageBreadcrumb /> </div> ); }
Summarize
The ecology of react is becoming more and more diverse, and we learn more and more (too much). In general, some of the libraries used above need to be more or less understood. Everyone should exercise their ability to build a simple version of the background management template.github address
The above is the detailed content of react's most popular ecological replacement for antidpro to build lightweight backend management. For more information about react's ecological lightweight backend management, please follow my other related articles!