react网络请求菜单解决方案

216 阅读5分钟

doutub_img.png

摘要:本篇文章主要讲解的通过请求获取到后端返回的菜单,并根据菜单路径获取相应的组件。路由使用的是react-router的v6版本,状态管理使用zustand

第一步:创建项目并安装相关依赖(我们这里使用vite来创建项目)

// 创建项目,具体可以前往vite官网查看
npm create vite@latest

我的生成如下

image.png

用代码编辑工具打开项目下载安装相关依赖

// 我在这里使用的包管理器是yarn,可根据实际情况选择合适的管理工具
yarn add react-router-dom zustand antd

安装后可在package.json中看到如下(红色框中)

image.png 这里我再简单提一下黄色框中的type字段,在package.json中不设置该值默认是commonjs(使用vite创建的项目默认设置为"type":"module")(该功能在特定的node版本后才支持),我们在该项目的根目录下创建一个test.js的文件,内容如下

// test.js
const lodash = require("lodash");

const array = [1];
const other = [4, 5, 7];

const concat = lodash.concat(array, other);

console.log("concat", concat);

接下来当我们在终端使用node test.js的时候并不会成功的打印出合并后的数组,会出现以下错误

image.png

错误的提示是require无法在esmodule中使用,建议使用import代替。出现错误是因为我们将package.json中type字段设置为了module,那么我们将require语法改为import,如下

// test.js
- const lodash = require("lodash");
+ import lodash from "lodash";

const array = [1];
const other = [4, 5, 7];

const concat = lodash.concat(array, other);

console.log("concat", concat);

我们可以看到终端中成功打印出了合并后的数组如下

image.png

注意:我们也可以通过删除package.json中的type字段或者修改test.jstest.cjs然后在终端中运行node test.cjs来达到相同的效果

哈哈哈,继续回归我们的正题吧

image.png

第二步:使用zustand创建一个仓库,用于存储全局状态;在src下创建名称为store的文件夹,结构如下

image.png

index.ts用于整合store文件夹下其他ts文件导出的状态实例,并再次统一导出
`common.ts`在本项目中我们将在此存储网络请求得到的菜单数据

具体内容如下

// common.ts文件内容
import { RouteObject } from "react-router";
import create from "zustand";

// element 为字符串
export interface MyRouteObject extends RouteObject {
  element?: string;
  name?: string; // 名称
  children?: MyRouteObject[];
}

type Menus = undefined | MyRouteObject[];

interface CommonStore {
  menus: Menus; // 菜单数据
  setMenus: (menus: Menus) => void; // 修改菜单数据的方法
}

export default create<CommonStore>((set) => ({
  menus: undefined,
  setMenus: (menus) => set({ menus }),
}));



// index.ts文件内容
import useCommonStore from "./common";

export { useCommonStore };

第三步: 在src下创建router文件夹,该文件夹的作用是网络请求菜单,处理菜单数据和路由信息

  • 在store文件夹下创建request.ts文件用于模拟获取后端请求过来的菜单,内容如下
import { MyRouteObject } from "../store/common";
// 注意:element的路径必须是相对src/pages的路径否在在后期将无法识别找到对应的内容
export default () =>
  new Promise((resolve) => {
    const routes: MyRouteObject[] = [
      {
        path: "/dashboard",
        name: "首页",
        element: "/dashboard",
      },
      {
        path: "/m_one",
        name: "主菜单一",
        children: [
          {
            name: "子菜单一",
            path: "moc_one",
            element: "/m_one/moc_one",
          },
          {
            name: "子菜单二",
            path: "moc_two",
            element: "/m_one/moc_two",
          },
          {
            name: "子菜单三",
            path: "moc_three",
            element: "/m_one/moc_three",
          },
        ],
      },
      {
        path: "/m_two",
        name: "主菜单一",
        children: [
          {
            name: "子菜单一",
            path: "mtc_one",
            element: "/m_two/mtc_one",
          },
          {
            name: "子菜单二",
            path: "mtc_two",
            element: "/m_two/mtc_two",
          },
        ],
      },
    ];

    const timer = setTimeout(() => {
      clearTimeout(timer);
      resolve(routes);
    }, 2000);
  });

  • 根据上述菜单信息在src下的pages文件夹中创建对应文件,截图如下

image.png

index.tsx文件中不需要复杂的结构,只需区分内容即可

  • 在router文件夹下创建index.tsx文件用来处理网络请求
import { useCommonStore } from "../store";
import routeRequest from "./request";

export default () => {
  const { menus, setMenus } = useCommonStore();

  // 获取菜单信息并存储在仓库中
  const initMenus = async () => {
    const menuList: any = await routeRequest();
    setMenus(menuList);
  };

  // 判断菜单不存在的时候请求菜单数据
  if (!menus) initMenus();

  return <div>...</div>;
};

然后改造main.tsx文件如下

import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import MyRouter from "./router";

import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <BrowserRouter>
    <MyRouter />
  </BrowserRouter>
);

到这里我们只是实现了菜单的请求,接下来我们需要对网络请求获取到的菜单进行处理,这里使用了vite提供的一个全局导入方法import.meta.glob,(具体使用可点击进入vite官网进行查看)改造router文件夹下的index.tsx代码如下


import { useMemo } from "react";
import { Navigate, RouteObject } from "react-router-dom";
import { useCommonStore } from "../store";
import { Menus, MyRouteObject } from "../store/common";
import Page from "./page";
import routeRequest from "./request";

// 导入pages下的所有主页面,用于根据路径匹配网络请求菜单对应的组件
const comp: any = import.meta.glob("../pages/**/index.tsx", { eager: true });

// 处理网络请求菜单的方法
const formatterRouter = (menus: Menus) => {
// 接下来我们重点实现该方法
// ..............
// ..............
  return [];
};

export default () => {
  const { menus, setMenus } = useCommonStore();

  // 获取菜单信息并存储在仓库中
  const initMenus = async () => {
    const menuList: any = await routeRequest();
    setMenus(menuList);
  };

  // 判断菜单不存在的时候请求菜单数据
  if (!menus) initMenus();

  // 最终展示的路由配置
  const routes: RouteObject[] = useMemo(
    () => [
      {
        path: "/",
        element: <Navigate to="/login" />,
      },

      {
        path: "/login",
        element: <Page.Login />,
      },
      ...formatterRouter(menus),
      {
        path: "*",
        element: <Page.NoFind />,
      },
    ],
    [menus]
  );

  return <div>...</div>;
};

在标注为最终展示路由的地方我们可以看到,我们所做的是需要将请求得到的菜单中的element字符串变为pages文件夹下相对路径对应的组件,实现方法如下

// 处理网络请求菜单的方法
const formatterRouter = (menus: Menus | RouteObject[]) => {
  // 根据接口返回数据动态导入组件
  const replaceElement = (route: MyRouteObject | RouteObject) => {
    // 匹配路径
    const fitPath: any = Object.keys(comp).find((item: string) =>
      item.replace(/(\/index\.tsx)$/, "").endsWith(route.element as string)
    );
    // 得到匹配路径组件信息
    const Component = comp[fitPath]?.default || Page.NoFind;
    route.element = <Component />;
  };

  // 递归处理菜单方法
  const recursion = (routes: Menus | RouteObject[]) => {
    // 循环遍历请求得到的菜单
    routes?.forEach((fRoute) => {
      if (fRoute.element) replaceElement(fRoute);
      if (fRoute.children) recursion(fRoute.children);
    });
  };

  recursion(menus);

  return menus as RouteObject[];
};

到这里,我们对于网络请求获取的菜单就处理完成了,接下来我们需要对路由进行渲染展示页面,在这里我们使用的是react-router提供的matchRoutesrenderMatches方法,具体方法的使用方式大家可以查看👉react-router官网,代码实现如下

// src/router/index.tsx
export default () => {
  const location = useLocation();
// ......

// 匹配当前的路由
const matchRoute = useMemo(
    () => matchRoutes(routes, location),
    [location.pathname, routes]
);

// 这里添加一个菜单请求中的loading状态
if (!menus?.length && location.pathname !== "/login")
    return <div style={{ textAlign: "center" }}>页面加载中...</div>;

 - return <div>...</div>;
 + return renderMatches(matchRoute);
 }

至此,我们就可以在页面上看到展示的页面了,在这里放一张动态图 example.gif

到这里我们的功能基本就算完成了,还需要做的是在页面上展示相应的菜单信息,并可以通过点击跳转路由,那么我们就继续进行吧!😁

第四步,在src下创建layout文件夹,该文件主要用于实现页面布局

我们在这里使用antd的layout进行布局

// src/layout/index.tsx 
import { Layout } from "antd";

const { Header, Sider, Content } = Layout;

export default ({ children }: any) => {
  return (
    <Layout style={{ height: "100%" }}>
      <Header>Header</Header>
      <Layout>
        <Sider theme="light">水电费</Sider>
        <Content>{children}</Content>
      </Layout>
    </Layout>
  );
};

然后我们改造以下代码

// src/router/index.tsx
const formatterRouter = (menus: Menus | RouteObject[]) => {
// 其他代码......

- route.element = <Component />;
+ route.element = (
    <Layout>
      <Component />
    </Layout>
  );

// 其他代码......

}

效果如下:

example2.gif

接下来我们来渲染菜单,并允许点击跳转,我们在layout文件夹下创建MenuRender.tsx文件,代码如下

import { Menu } from "antd";
import { ItemType } from "antd/lib/menu/hooks/useItems";
import { useMemo } from "react";
import { useCommonStore } from "../store";
import { useLocation, useNavigate } from "react-router-dom";

export default () => {
  const { menus } = useCommonStore();
  const navigate = useNavigate();
  const location = useLocation();

  console.log("menus", menus);
  console.log("location", location);

  const items = useMemo(
    (): ItemType[] =>
      menus?.map((item) => {
        if (!item.children)
          return {
            key: item.path as string,
            label: item.name,
            title: item.name,
          };
        return {
          key: item.path as string,
          label: item.name,
          children: item.children.map((c_item) => ({
            key: `${item.path}/${c_item.path}`,
            label: c_item.name,
            title: c_item.name,
          })),
        };
      }) || [],
    [menus]
  );

  // 点击菜单项的时候触发
  const handleClick = ({ key }: any) => navigate(key);

  return (
    <Menu
      selectedKeys={[location.pathname]} // 刷新页面的时候选中历史内容
      openKeys={["/m_one", "/m_two"]}
      mode="inline"
      items={items}
      onClick={handleClick}
    />
  );
};

haha,我在这里偷个懒,仅仅实现了功能,其实有更好的写法,嗯~比如递归遍历,😅

example3.gif

emmm....,到这里基本文章就算完成了,其实按照常理这篇文章到这里是不应该结束的。还有权限处理等等,等以后有空再补上吧,栓Q(自我感谢)...

最后,补上一下代码地址吧:gitee.com/fanbingtao/…