vite+react(函数式)+ant-design实现动态路由
·
实现效果:

1.核心内容版本如下:
"react-router-dom": "6",
"antd": "^5.23.2",
"vite": "^6.0.5"
"@ant-design/icons": "^5.6.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
2.项目目录结构如下:

3.具体实现思路:
3.1思路:
1.封装懒加载组件WithLoading。
2.因为是左侧菜单,右侧展示菜单对应的路由内容,这里需要两层路由,这里封装一个layoutComponent组件 作为一级路由里面嵌套一个outlet作为二级路由的出口。还需要一级路由登录页等等自定内容。
3.通过vite的独有的引入功能(import.meta.glob)直接遍历生成最终路由表,引入使用
3.2具体代码:
1.懒加载组件WithLoading
import React from "react";
import { Suspense } from "react";
import MyLoading from "@/layout/Loading/Loading";
//懒加载组件
const WithLoading = (Vnode: React.ReactNode) => {
return <Suspense fallback={<MyLoading />}>{Vnode}</Suspense>;
};
export default WithLoading;
Loading组件--如果页面加载慢会加载Loading效果
import { Spin } from "antd";
function MyLoading() {
return (
<>
<div className="loading-container">
<Spin />
</div>
</>
);
}
export default MyLoading;
2.layoutComponent组件(作为下级路由的出口)
这里在utils文件夹中封装了一个useMenu的hooks去处理左侧菜单,
useMenu内容
//获取左侧菜单
import { MyAppType } from "@/router/type";
// / import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob("../router/modules/**/*.tsx", {
eager: true,
}) as {
[key: string]: { default: MyAppType };
};
const routeModuleList: MyAppType[] = [];
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
const mod = modules[key]?.default || {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
routeModuleList.push(...modList);
});
// 格式化菜单数据
const useMenu = (): MenuType[] => {
let arr: MenuType[] = [];
if (routeModuleList && routeModuleList.length > 0) {
const menuList = routeModuleList.map((item): MenuType => {
const obj: MenuType = {
key: item.path,
title: item.meta?.title,
icon: item.meta?.icon,
};
if (item.children && item.children.length > 0) {
obj.children = item.children.map((child): any => {
return {
key: item.path + "/" + child.path,
title: child.meta?.title,
icon: child.meta?.icon,
path: item.path + "/" + child.path,
};
});
}
return obj;
});
arr = menuList;
}
return arr;
};
export { useMenu };
layoutComponent内容
import { Layout, Menu } from "antd";
import { useState, useEffect } from "react";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import { MenuFoldOutlined } from "@ant-design/icons"; //
import type { GetProp, MenuProps } from "antd";
import { useMenu } from "@/utils/useMenu";
import { Watermark } from "antd";
import { FloatButton } from "antd";
type MenuItem = GetProp<MenuProps, "items">[number];
const { Header, Sider, Content } = Layout;
function LayoutComponent() {
const navigate = useNavigate();
const [collapsed, setCollapsed] = useState(false);
const [menuItems, setMenuItems] = useState<MenuItem[]>([]);
const [selectedKeys, setSelectedKeys] = useState<string[]>(); //子节点
const [openKeys, setOpenKeys] = useState<string[]>(); //父节点
//展开收缩
const toggle = () => {
setCollapsed(!collapsed);
};
const handleClick = (path: string) => {
navigate(path);
};
const location = useLocation();
const menu = useMenu();
useEffect(() => {
let arr: MenuItem[] = [];
if (menu && menu.length > 0) {
const list = menu.map((item) => {
const obj = {
key: item?.key,
label: item?.title,
icon: item?.icon,
children: [],
};
if (item?.children && item?.children.length > 0) {
obj.children = item.children.map((child): any => {
return {
key: child.key,
label: child.title,
onClick: () => handleClick(child.path),
};
});
}
return obj;
});
arr = list;
setMenuItems(arr);
}
}, []);
useEffect(() => {
console.log(location, "location");
setSelectedKeys([location.pathname]);
getMenuKey(location.pathname);
}, [location]);
//获取父级key
const getMenuKey = (path: string) => {
const parts = path.split("/");
setOpenKeys([`/${parts[1]}`]);
};
interface LevelKeysProps {
key?: string;
children?: LevelKeysProps[];
}
const getLevelKeys = (items1: LevelKeysProps[]) => {
const key: Record<string, number> = {};
const func = (items2: LevelKeysProps[], level = 1) => {
items2.forEach((item) => {
if (item.key) {
key[item.key] = level;
}
if (item.children) {
func(item.children, level + 1);
}
});
};
func(items1);
return key;
};
const levelKeys = getLevelKeys(menu as LevelKeysProps[]);
//只展开一个子节点
const onOpenChange: MenuProps["onOpenChange"] = (openKeys) => {
const currentOpenKey = openKeys.find((key) => openKeys.indexOf(key) === -1);
// open
if (currentOpenKey !== undefined) {
const repeatIndex = openKeys
.filter((key) => key !== currentOpenKey)
.findIndex((key) => levelKeys[key] === levelKeys[currentOpenKey]);
setOpenKeys(
openKeys
// remove repeat key
.filter((_, index) => index !== repeatIndex)
// remove current level all child
.filter((key) => levelKeys[key] <= levelKeys[currentOpenKey])
);
} else {
// close
setOpenKeys(openKeys);
}
};
return (
<>
<Layout hasSider>
<Sider trigger={null} collapsible collapsed={collapsed}>
<div className="logo" />
<Menu
// onClick={onClick}
onOpenChange={onOpenChange}
theme="dark"
mode="inline"
defaultOpenKeys={openKeys}
openKeys={openKeys}
defaultSelectedKeys={selectedKeys}
selectedKeys={selectedKeys}
items={menuItems}
inlineCollapsed={collapsed}
/>
</Sider>
<Layout>
<Header style={{ background: "#fff", padding: 0 }}>
<MenuFoldOutlined className="trigger" onClick={toggle} />
</Header>
<Content
style={{
margin: "24px 16px",
padding: 12,
background: "#fff",
minHeight: 280,
overflowY: "scroll",
}}
className="site-layout-background"
>
<Watermark content="outlet为出口组件">
<Outlet />
<FloatButton.BackTop
visibilityHeight={0}
target={() =>
document.querySelector(
".site-layout-background"
) as HTMLElement
}
/>
</Watermark>
</Content>
</Layout>
</Layout>
</>
);
}
export default LayoutComponent;
3.vite动态引入路由
通过import.meta.glob() 直接引入所有的模块 Vite 独有的功能--引入router文件夹下的modules中的所有路由.tsx配置文件遍历处理,生成动态的路由表结构
modules中存放菜单对应的二级路由,和views中的文件呼应

type.ts
--这里其实应该再修改一下但是懒得改了 凑合用
import React from "react";
export interface MyAppType {
path: string;
name: string;
element?: React.ReactNode;
children?: Array<MyAppType>;
meta?: {
title: string;
icon?: React.ReactNode;
needAtuhor?: boolean;
};
}
list.ts
通过import.meta.glob() 引入router文件夹下的modules文件中每个路由表.tsx文件 ,将得到的数组进行扁平化处理,在同级下的index.tsx路由文件夹中引入使用
import { MyAppType } from "./type";
// / import.meta.glob() 直接引入所有的模块 Vite 独有的功能
const modules = import.meta.glob("./modules/**/*.tsx", { eager: true }) as {
[key: string]: { default: MyAppType };
};
const routeModuleList: MyAppType[] = [];
// 加入到路由集合中
Object.keys(modules).forEach((key) => {
const mod = modules[key].default || {};
const modList = Array.isArray(mod) ? [...mod] : [mod];
routeModuleList.push(...modList);
});
console.log(routeModuleList,'--------------------');
//路由扁平化处理
const disPoseArr = routeModuleList.flatMap((item) => {
return item.children.map((child) => {
return {
path: item.path + "/" + child.path,
name: child.name,
element: child.element,
};
});
});
export default disPoseArr;
modules文件夹下路由表示例--home.tsx
import { MyAppType } from "@/router/type";
import { lazy } from "react";
import WithLoading from "@/layout/WithLoading";
import { HomeOutlined } from "@ant-design/icons";
const HomeOne = lazy(() => import("@/views/Home/HomeOne"));
const HomeTwo = lazy(() => import("@/views/Home/HomeTwo"));
const home: MyAppType = {
path: "/home",
name: "home",
meta: {
icon: <HomeOutlined />,
title: "网站管理",
},
children: [
{
path: "one",
name: "homeOne",
element: WithLoading(<HomeOne />),
meta: {
title: "网站一",
},
},
{
path: "two",
name: "homeTwo",
meta: {
title: "网站二",
},
element: WithLoading(<HomeTwo />),
},
],
};
export default home;
router文件中index.tsx文件如下最终路由实现文件,到处RouterView组件
import NotFound from "../views/Notfound";
//useRoutes钩子可以根据路由表自动生成路由匹配规则
//Navigate内置组件,路由重定向组件,只要页面渲染Navigate就重新执行跳转
import { Routes, Route, useRoutes, Navigate } from "react-router-dom";
import { MyAppType } from "./type";
import disPoseArr from "./list";
import LayoutComponent from "@/layout/LayoutComponent";
import { lazy } from "react";
import WithLoading from "@/layout/WithLoading";
const DefaultPage = lazy(() => import("@/views/DefaultPage"));
//这里的element放成layout组件 里面放outlet出口即可
const router: Array<MyAppType> = [
{
path: "/",
name: "/",
element: <Navigate to="/defaultPage" />, //当访问到/时,跳转到defaultPage,这就是路由重定向,Navigate组件只要加载了就会重新跳转一次
},
{
path: "/",
name: "/",
element: <LayoutComponent />,
children: [
{
path: "/defaultPage",
name: "DefaultPage",
element: WithLoading(<DefaultPage />),
},
//将处理后的菜单对应的路由结构到这里
...disPoseArr,
],
},
{
path: "/login",
name: "Login",
element: <div>登录</div>,
},
{
path: "*", //*表示匹配不到任何一个路由规则
name: "*",
element: <NotFound />,
},
];
//也可以选择使用useRoutes函数 推荐自己进行配置
function RouterView() {
return (
<Routes>
{router.map((item, index) => (
<Route key={index} path={item.path} element={item.element}>
{item.children &&
item.children.map((item2, index2) => {
return (
<Route
key={index2}
path={item2.path}
element={item2.element}
></Route>
);
})}
{/* 匹配子路由地址错误 */}
{item.children && (
<Route key={`${index}-notfound`} path="*" element={<NotFound />} />
)}
</Route>
))}
</Routes>
);
}
export default RouterView;
在app.tsx文件中引入路由组件
import RouterView from "./router/index";
import "./App.css";
function App() {
return (
<>
<div className="app">
<RouterView></RouterView>
</div>
</>
);
}
export default App;
import { createRoot } from "react-dom/client";
import "./index.css";
import { HashRouter } from "react-router-dom";
import App from "./App.tsx";
createRoot(document.getElementById("root")!).render(
<HashRouter>
<App />
</HashRouter>
);
通过import.meta.glob() 直接引入所有模块,可以避免手动的去import引入每个菜单对应的文件,一定的提高效率。
4. 参考地址如下:
git clone
myReactRouterOnlyRead: 仅供参考
https://gitee.com/zhang-hana/my-react-router-only-read.git node.js的版本 最好在 v18.18.0 或以上
更多推荐
所有评论(0)