Antd5.x
Antd5 官网
吸引我的点
- 定制主题,随心所欲
- 组件丰富,选用自如
开发后台管理整个流程
创建模版
pnpm create vite@latest react-admin-vite-antd5 -- --template react-tsnpm installnpm run dev
配置vite和tsconfig
-
vite-plugin-compression 打包增加gzip文件 提高首屏访问速度
-
配置 "@" 别名,不用写 ../ 这种形式了
-
proxy 解决开发环境跨域问题
-
build / rollupOptions / 拆分包大小,也可以提升首屏访问速度
-
import react from "@vitejs/plugin-react"; import { fileURLToPath } from "url"; import { defineConfig } from "vite"; // https://vitejs.dev/config/ import vitePluginCompression from "vite-plugin-compression"; const baseUrl = "react-admin-vite-antd5"; export default defineConfig(config => { console.log(config, "config"); return { plugins: [ react(), vitePluginCompression({ threshold: 1024 * 10, // 对大于 10kb 的文件进行压缩 // deleteOriginFile: true, }), ], resolve: { alias: { // for TypeScript path alias import like : @/x/y/z "@": fileURLToPath(new URL("./src", import.meta.url)), }, }, server: { open: true, port: 5793, proxy: { "/api": { target: "http://localhost:8080", secure: false, rewrite: path => path.replace(/^\/api/, ""), }, }, }, base: config.mode === "development" ? "/" : `/${baseUrl}/`, build: { outDir: baseUrl, rollupOptions: { output: { chunkFileNames: "js/[name]-[hash].js", // 引入文件名的名称 entryFileNames: "js/[name]-[hash].js", // 包的入口文件名称 assetFileNames: "[ext]/[name]-[hash].[ext]", // 资源文件像 字体,图片等 manualChunks(id) { if (id.includes("node_modules")) { return id .toString() .split("node_modules/")[1] .split("/")[0] .toString(); } }, }, }, }, }; }); -
ts类型规则配置
-
{ "compilerOptions": { "target": "esnext", "module": "esnext", "lib": ["dom", "dom.iterable", "esnext", "scripthost"], "allowJs": false, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "isolatedModules": true, "jsx": "react-jsx", "moduleResolution": "Node", "noEmit": true, "noFallthroughCasesInSwitch": true, "resolveJsonModule": true, "skipLibCheck": true, "strict": true, "sourceMap": true, "types": ["vite/client", "node"], "baseUrl": ".", "paths": { "@/*": ["src/*"] } }, "include": ["src", "vite.config.ts"] }
添加d.ts解决大部分引入文件报错的问题
declare module "*.css";
declare module "*.scss";
declare module "*.sass";
declare module "*.svg";
declare module "*.png";
declare module "*.jpg";
declare module "*.jpeg";
declare module "*.gif";
declare module "*.tiff";
添加需要用到的包
- antd、lodash、sass、redux、react-redux、@reduxjs/toolkit
文件主入口
import "./main.css";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { BrowserRouter } from "react-router-dom";
import App from "./app";
import store from "./store";
const container = document.getElementById("root");
const root = createRoot(container as HTMLDivElement);
root.render(
<Provider store={store}>
<BrowserRouter
// 生产环境配置二级路径
basename={"/" + import.meta.env.BASE_URL.replaceAll("/", "")}
>
<App />
</BrowserRouter>
</Provider>
);
添加状态管理
import { configureStore } from "@reduxjs/toolkit";
import common from "./reducers/common";
import user from "./reducers/user";
const store = configureStore({
reducer: {
user,
common,
},
});
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export default store;
import { createSlice } from "@reduxjs/toolkit";
import { TOKEN } from "@/common/utils/contans";
import { getStorage } from "@/common/utils/storage";
import { MenuItem } from "@/components/Layout/layout";
export interface IUserInitialState {
role: string[];
token: string;
menu: MenuItem[];
[key: string]: any;
}
export interface Type {
type: string;
}
// 默认状态
const initialState: IUserInitialState = {
role: [],
token: getStorage(TOKEN) ?? "",
menu: [],
};
export const userSlice = createSlice({
name: "user",
initialState: initialState,
reducers: {
setUserToken: (state, action) => {
state.token = action.payload;
},
setMenu: (state, action) => {
state.menu = action.payload;
},
},
});
export const { setUserToken, setMenu } = userSlice.actions;
export default userSlice.reducer;
添加登陆界面
主界面
路由
-
路由、路由懒加载
-
import { DashboardOutlined } from "@ant-design/icons"; import { Alert, Button, Result, Spin } from "antd"; import { lazy, Suspense } from "react"; import { Link, Navigate } from "react-router-dom"; import { TOKEN } from "@/common/utils/contans"; import { getStorage } from "@/common/utils/storage"; import Layout from "@/components/Layout"; import { MenuItem } from "@/components/Layout/layout"; import OutletLayoutRouter from "@/components/OutletLayoutRouter"; import Dashboard from "@/pages/dashboard"; import ErrorPage from "@/pages/error-page"; import Login from "@/pages/login"; const Permissions = ({ children }: any) => { const token = getStorage(TOKEN); return token ? children : <Navigate to="/login" />; }; export const baseRouterList = [ { label: "Dashboard", key: "dashboard", path: "dashboard", icon: <DashboardOutlined />, filepath: "pages/dashboard/index.tsx", }, ]; export const defaultRoutes: any = [ { path: "/", element: <Permissions>{<Layout />}</Permissions>, errorElement: <ErrorPage />, children: [ { path: "/", element: <Navigate to="dashboard" />, }, { path: "dashboard", element: <Dashboard />, }, { path: "/*", element: ( <ErrorPage> <Result status="404" title="404" subTitle="Sorry, the page you visited does not exist." extra={ <Link to={"/"}> <Button type="primary">Back Home</Button> </Link> } /> </ErrorPage> ), }, ], }, { path: "/login", element: <Login />, }, ]; // /**/ 表示二级目录 一般二级目录就够了 不够在加即可 export const modules = import.meta.glob("../pages/**/*.tsx"); function pathToLazyComponent(Ele: string) { const path = modules[`../${Ele}`] as any; if (!path) return ( <ErrorPage> <Alert message={ Ele + ":Cannot find the path, please configure the correct folder path" } type="error" /> </ErrorPage> ); const Components = lazy(path); return ( <Suspense fallback={<Spin size="small" />}> <Components /> </Suspense> ); } // 递归算法 export const filepathToElement = (list: MenuItem[]) => list.map(item => { if (item.children) { return { path: item.path, key: item.key, children: item.children?.map(c => ({ key: c.key, path: c.path, element: pathToLazyComponent(c.filepath), })), element: <OutletLayoutRouter />, }; } else { return { key: item.key, path: item.path, element: pathToLazyComponent(item.filepath), }; } }); -
动态路由主要hooks:useRoutes
-
import "./App.scss"; import { cloneDeep } from "lodash"; import React, { useEffect } from "react"; import { useRoutes } from "react-router-dom"; import { AuthContext, signIn, signOut } from "@/common/context"; import { useAppDispatch, useAppSelector, useLocationListen, } from "@/common/hooks"; import { MenuData } from "@/common/mock"; import { ADMIN } from "@/common/utils/contans"; import { Settings } from "@/config/defaultSetting"; import { setMenu } from "@/store/reducers/user"; import { defaultRoutes, filepathToElement } from "./routes"; function App() { const dispatch = useAppDispatch(); const { user: { token, menu }, } = useAppSelector(state => state); const cloneDefaultRoutes = cloneDeep(defaultRoutes); cloneDefaultRoutes[0].children = [ ...filepathToElement(menu), ...cloneDefaultRoutes[0].children, ]; // console.log(cloneDefaultRoutes, "cloneDefaultRoutes"); useLocationListen(r => { document.title = `${Settings.title}: ${r.pathname.replace("/", "")}`; }); const element = useRoutes(cloneDefaultRoutes); useEffect(() => { // console.log(token, "token"); if ((token as unknown as { username: string })?.username === ADMIN) { dispatch(setMenu([...MenuData.admin])); } else { dispatch(setMenu([...MenuData.user])); } }, [token]); return ( <AuthContext.Provider value={{ signIn, signOut }}> {element} </AuthContext.Provider> ); } export default App; -
模拟数据
-
import { DesktopOutlined, TableOutlined, UserOutlined, } from "@ant-design/icons"; import { MenuItem } from "@/components/Layout/layout"; export const MenuData: { user: MenuItem[]; admin: MenuItem[]; } = { user: [ { label: "User", key: "user", path: "/user", icon: <DesktopOutlined />, filepath: "pages/user/index.tsx", }, ], admin: [ { label: "User", key: "user", path: "user", icon: <DesktopOutlined />, filepath: "pages/user/index.tsx", }, { label: "List Page", key: "list-page", path: "list-page", icon: <TableOutlined />, filepath: "pages/list-page/index.tsx", }, { label: "System Management", key: "systemManagement", path: "systemManagement", icon: <UserOutlined />, filepath: "components/OutletLayoutRouter/index.tsx", children: [ { label: "User Management", key: "userManagement", path: "userManagement", filepath: "pages/systemManagement/userManagement/index.tsx", }, { label: "Role Management", key: "roleManagement", path: "roleManagement", filepath: "pages/systemManagement/roleManagement/index.tsx", }, ], }, ], };
KeepAlive组件(实验性组件,网上看别人写的,不知道性能如何)
import React, { memo, useEffect, useMemo, useReducer, useRef } from "react";
import { useLocation, useOutlet } from "react-router-dom";
const KeepAlive = (props: { include: any; keys: any }) => {
const outlet = useOutlet();
const { include, keys } = props;
const { pathname } = useLocation();
const componentList = useRef(new Map());
// @ts-ignore
const forceUpdate = useReducer(bool => !bool)[1]; // 强制渲染
const cacheKey = useMemo(
() => pathname + "__" + keys[pathname],
[pathname, keys]
);
const activeKey = useRef<string>("");
useEffect(() => {
componentList.current.forEach(function (value, key) {
const _key = key.split("__")[0];
if (!include.includes(_key) || _key === pathname) {
// @ts-ignore
this.delete(key);
}
}, componentList.current);
activeKey.current = cacheKey;
if (!componentList.current.has(activeKey.current)) {
componentList.current.set(activeKey.current, outlet);
}
forceUpdate();
}, [cacheKey, include]); // eslint-disable-line
return (
<>
{Array.from(componentList.current).map(([key, component]) => (
<div key={key}>
{key === activeKey.current ? (
<div>{component}</div>
) : (
<div style={{ display: "none" }}>{component}</div>
)}
</div>
))}
</>
);
};
export default memo(KeepAlive);
- 组件使用
封装主题定制
-
<ConfigProvider theme={{ token: { borderRadius: 4, fontSize: 14, colorPrimary: "pink", }, }}
暗黑模式
- @ant-design/pro-components 提供了 ProConfigProvider 组件
- 用的是
import ProLayout from "@ant-design/pro-layout";组件 -
import { GithubFilled, InfoCircleFilled, LoginOutlined, PlusCircleFilled, QuestionCircleFilled, SearchOutlined, } from "@ant-design/icons"; import { ProBreadcrumb, ProConfigProvider, ProSettings, } from "@ant-design/pro-components"; import ProLayout from "@ant-design/pro-layout"; import { Input, Switch, Tooltip } from "antd"; import ErrorBoundary from "antd/es/alert/ErrorBoundary"; import { useContext, useState } from "react"; import { Link, useLocation, useNavigate } from "react-router-dom"; import { AuthContext } from "@/common/context"; import KeepAlive from "@/common/hocs/keepAlive"; import { useAppDispatch, useAppSelector, useLocationListen, } from "@/common/hooks"; import { treeRouter } from "@/common/utils/common"; import { Settings } from "@/config/defaultSetting"; import { baseRouterList } from "@/routes"; export default () => { const { user } = useAppSelector(state => state); const navigate = useNavigate(); const location = useLocation(); const [pathname, setPathname] = useState(location.pathname); const dispatch = useAppDispatch(); const { signOut } = useContext(AuthContext); const [dark, setDark] = useState(false); useLocationListen(listener => { // console.log(listener, "listener"); setPathname(listener.pathname); }); const settings: ProSettings | undefined = { title: Settings.title.slice(0, 11), // fixSiderbar: true, layout: "mix", // splitMenus: true, }; return ( <ProConfigProvider dark={dark}> <div id="admin-pro-layout" style={{ height: "100vh", }} > <ProLayout fixSiderbar siderWidth={245} logo={Settings.logo} ErrorBoundary={false} route={{ path: "/", routes: treeRouter([...baseRouterList, ...user.menu]), }} {...settings} location={{ pathname, }} waterMarkProps={{ content: Settings.title, }} appList={[ { icon: "https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg", title: "Blog", desc: "杭州市较知名的 UI 设计语言", url: "https://hzdjs.cn", }, ]} avatarProps={{ src: "https://joeschmoe.io/api/v1/random", size: "small", title: ( <div> {(user.token as unknown as { username: string })?.username} </div> ), }} headerContentRender={() => <ProBreadcrumb />} actionsRender={props => { if (props.isMobile) return []; return [ props.layout !== "side" && document.body.clientWidth > 1400 ? ( <div key="SearchOutlined" aria-hidden style={{ display: "flex", alignItems: "center", marginInlineEnd: 24, }} onMouseDown={e => { e.stopPropagation(); e.preventDefault(); }} > <Input style={{ borderRadius: 4, marginInlineEnd: 12, backgroundColor: "rgba(0,0,0,0.03)", }} prefix={ <SearchOutlined style={{ color: "rgba(0, 0, 0, 0.15)", }} /> } placeholder="搜索方案" bordered={false} /> <PlusCircleFilled style={{ color: "var(--ant-primary-color)", fontSize: 24, }} /> </div> ) : undefined, <InfoCircleFilled key="InfoCircleFilled" />, <QuestionCircleFilled key="QuestionCircleFilled" />, <Tooltip placement="bottom" title={"Github"}> <a href="https://github.com/frontend-winter/react-admin-vite-antd5" target="_blank" rel="noreferrer" > <GithubFilled key="GithubFilled" /> </a> </Tooltip>, <Tooltip placement="bottom" title={"Sign Out"}> <a> <LoginOutlined onClick={async () => { await signOut(dispatch); navigate("/login"); }} /> </a> </Tooltip>, ]; }} menuFooterRender={props => { if (props?.collapsed || props?.isMobile) return undefined; return ( <div style={{ textAlign: "center" }}> <Switch checkedChildren="🌜" unCheckedChildren="🌞" defaultChecked={false} onChange={v => setDark(v)} /> </div> ); }} menuItemRender={(item, dom) => ( <Link to={item?.path || "/"} onClick={() => { setPathname(item.path || "/"); }} > {dom} </Link> )} onMenuHeaderClick={() => navigate("/")} > <ErrorBoundary> <KeepAlive include={[]} keys={[]} /> </ErrorBoundary> </ProLayout> </div> </ProConfigProvider> ); };
结尾
这篇先讲这么多,说的比较模糊,大家可以去看下源码,还有很多可以探讨的问题,欢迎大家留言。
线上预览地址hzdjs.cn/react-admin…
至此,一个对新手友好的管理后台项目就构建好了,而且还在不断完善中,未来会补全Node后端服务项目,敬请期待,有问题可以随时留言。。