实现效果:

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 或以上

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐