「超详细React项目搭建教程五」集成 React-Router/Antd Menu

5,053 阅读6分钟

React 是一个用于构建用户界面的 JavaScript 库,它是单页面应用(SPA). 单页面应用,顾名思义:只有一个页面,它是没有路由导航机制的. 这时候往往需要一种路由机制,以便在不同的视图之间切换而不用刷新整个网页. React-Router 就是一个扩展 React 从而实现多页面跳转的第三方库。

React-Router 入门可以参考这篇文章React Router 入门完全指南(包含 Router Hooks)🛵

在这篇文章中, 我们不借助任何脚手架。从零到一实现下图中的效果!

react-router-antd

安装依赖

yarn add react-router-dom

yarn add @types/react-router-dom --dev

路由基础配置

我们先实现一个简单的路由配置页面跳转功能

import React from "react";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";

export default function BasicExample() {
  return (
    <Router>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>

        <hr />
        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

function Home() {
  return (
    <div>
      <h2>Home</h2>
    </div>
  );
}

function About() {
  return (
    <div>
      <h2>About</h2>
    </div>
  );
}

function Dashboard() {
  return (
    <div>
      <h2>Dashboard</h2>
    </div>
  );
}

我们通过导入三个组件,并渲染在浏览器中。这三个组件就对应了三个动态路由

  • Home组件对应路径/
  • About组件对应路径/about
  • Dashboard组件对应路径/dashboard

Link组件用于切换页面并且不会刷新整个页面,React Router 还会使 URL 保存到浏览器历史记录 📝 中,并且可以通过浏览器的回退,前进 按钮进行操作

<Switch>会遍历它下面的所有<Route>,然后渲染第一个路径与当前网址匹配的组件。

<Switch>匹配到第一个路由就不会继续匹配了,如果不加Route 里不加 exact,那么凡是Link里面 to 的路径包含了/,那么就会被匹配到 exact 是精准匹配,只有路径完全一致才能被匹配到

路由嵌套

在真实的前端场景中,路由下面还有它的子路由-即路由嵌套

比如/user 路径下有 /user/login/user/register

那么当出现这样的场景时,路由应该怎么配置呢?

先来看一个 React Router 官方的路由嵌套例子

React-Router-nest

可以发现

  • 首先在最外层组件有一份Switch包裹的路由配置-包含了/的子路径页面
  • 然后在Topics组件内有一份Switch包裹的路由配置-包含了/topics的子路径页面. 在Topics组件就用到了路由嵌套的逻辑,即在组件内部再声明一份Switch包裹的路由配置,此配置是当前路径对应的子路径

因为路由也是 React 组件,因此Switch/Route可以渲染在代码中的任何位置,包括作为子元素。当需要对应用进行代码拆分为多个包时,这是非常有用的

所谓代码代码拆分,就是将不同业务进行代码拆分,相同业务的代码装载在一起。

那么如何区别业务相同与不同呢?

这时就可以通过路由来进行区别拆分, 然后把子路由也渲染到拆分的包里面去(在子组件内部再声明一份Switch包裹的路由配置)。 这就非常符合代码分离的思想了,不是吗?ლ(′◉❥◉ ` ლ)

配置式路由

真实开发场景,我们往往只需要一个路由配置文件生成路由及菜单,以方便管理所有的路由,这也符合 React 中 UI=F(data)的数据驱动视图思想

主要思路是: 路由配置文件作为一份静态数据存储

  • 通过循环遍历生成<Switch><Route> ,即路由配置
  • 通过循环遍历生成侧边栏菜单

先来看一个 React Router 官方的路由配置例子

route-config

侧边栏菜单

有一种最常见的左右布局方式-左边菜单,右边内容。

我们结合 React-Router 应该如何实现呢。其实只需要修改一下我们前面代码的布局样式即可

主要思路是: 路由配置文件作为一份静态数据存储

  • 通过循环遍历生成<Switch><Route> ,即路由配置
  • 通过循环遍历生成侧边栏菜单
  • 修改 CSS 样式实现左右布局

先来看一个 React Router 官方的侧边栏菜单

siderbar

React Router + Antd 实现侧边栏菜单

从前面路由嵌套的例子 🌰 中

const routes = [
  {
    path: "/sandwiches",
    component: Sandwiches,
  },
  {
    path: "/tacos",
    // 公共布局组件
    component: Tacos,
    routes: [
      {
        path: "/tacos/bus",
        component: Bus,
      },
      {
        path: "/tacos/cart",
        component: Cart,
      },
    ],
  },
];

function Tacos({ routes }) {
  return (
    <div>
      <h2>Tacos</h2>
      <ul>
        <li>
          <Link to="/tacos/bus">Bus</Link>
        </li>
        <li>
          <Link to="/tacos/cart">Cart</Link>
        </li>
      </ul>

      <Switch>
        <Route path="/tacos/bus" component={Bus} />
        <Route path="/tacos/cart" component={Cart} />
      </Switch>
    </div>
  );
}

我们发现 /tacos对应的组件主要作用是界面公共布局和渲染子路由,如果了解过Antd Design Pro路由配置的同学应该很快就能理解。

那么接下来让我们改造代码,让我们的项目看起来更像Antd Design Pro

布局文件

Antd Design Pro中有多个布局文件

  • BasicLayout:用于有主页界面的布局
  • UserLayout:用于用户登录/注册界面的布局
  • BlankLayout:空白布局

路由文件

真正的嵌套路由配置可以这样理解

react-config-file

渲染路由

const RouteWithSubRoutes = (routeDatas: menuType[]) => {
  if (Array.isArray(routeDatas) && routeDatas.length > 0) {
    return routeDatas.map((item) => {
      const bool = Array.isArray(item.routes) && item.routes.length > 0;
      if (bool) {
        return (
          <Route
            key={item.path}
            path={item.path}
            render={(props) =>
              Array.isArray(item.routes) && item.routes.length > 0 ? (
                // item.component 是 BasicLayout,UserLayout,BlankLayout其中一个布局
                <item.component {...props}>
                  <Switch>
                    <Route exact path={item.path}>
                      <Redirect to={item.routes[0].path} />
                    </Route>
                    {/*当路由有多级的时候,递归渲染出多份路由配置文件*/}
                    {RouteWithSubRoutes(item.routes)}
                    <Route path="*">
                      <NoMatch />
                    </Route>
                  </Switch>
                </item.component>
              ) : (
                <Redirect
                  to={{
                    pathname: "/user/login",
                    state: { from: props.location },
                  }}
                />
              )
            }
          />
        );
      }
      //  当路由只有一级的时候,直接渲染界面
      return (
        <Route path={item.path} key={item.path}>
          <item.component />
        </Route>
      );
    });
  }
  return null;
};

渲染侧边栏菜单

侧边栏菜单主要在 用户登录成功后,访问主页界面时才显示。也就是在BackLayout中进行显示

const Index: React.FC = (props) => {
  const handleClick: MenuClickEventHandler = (e) => {
    console.log("click ", e);
  };
  // 递归生成菜单
  const generateMenu = (menus: menuType[]) => {
    return menus.map((item) => {
      if (Array.isArray(item.routes) && item.routes.length > 0) {
        return (
          <SubMenu key={item.path} icon={<SettingOutlined />} title={item.name}>
            {generateMenu(item.routes)}
          </SubMenu>
        );
      }
      return (
        <Menu.Item key={item.path} icon={<AppstoreOutlined />}>
          <Link to={item.path}>{item.name}</Link>
        </Menu.Item>
      );
    });
  };

  // 从routes配置中提取出 “/home” 下的子路由, 来进行菜单渲染
  const renderMenu = (datas: menuType[]) => {
    if (
      Array.isArray(datas) &&
      datas.length > 0 &&
      Array.isArray(datas[0].routes) &&
      datas[0].routes.length > 0
    ) {
      const homeMenus = datas[0].routes.filter((item) => item.path === "/home");
      if (
        Array.isArray(homeMenus) &&
        homeMenus.length > 0 &&
        Array.isArray(homeMenus[0].routes) &&
        homeMenus[0].routes.length > 0
      ) {
        const realHomeMenus = homeMenus[0].routes;
        return generateMenu(realHomeMenus);
      }
    }
    return null;
  };

  return (
    <Layout style={{ minHeight: "100vh" }}>
      <Sider collapsible collapsed={false}>
        <div className="logo" />
        <Menu onClick={handleClick} mode="inline" theme="dark">
          {renderMenu(routes)}
        </Menu>
      </Sider>

      <Layout className="site-layout">
        <Header className="site-layout-background" style={{ padding: 0 }} />
        <Content style={{ margin: "16px 0 0 16px" }}>
          <div
            className="site-layout-background"
            style={{ padding: 24, minHeight: 360 }}
          >
            {props.children}
          </div>
        </Content>
        <Footer style={{ textAlign: "center" }}>
          Ant Design ©2023 Created by Ant UED
        </Footer>
      </Layout>
    </Layout>
  );
};

export default Index;

常见问题

手动刷新子页面出现 404

修改开发环境的 webpack 配置

const confog = {
  output: {
    publicPath: "/",
  },
};

不能在 if/循环语句中使用 React Hooks

界面会出现报错。因为 React Hooks 的底层是通过有序链表实现的,如果放到 if/循环语句就可能导致执行顺序发生改变,从而导致意料之外的错误 ❎

最后

这里我们总结一下本篇文章做过的事

  • 手动创建路由配置文件
  • 根据路由文件生成路由
  • 根据路由文件生成菜单

其实还有路由权限校验的知识这里没有说明,感兴趣的同学欢迎在评论区发表你的见解

参考文章

  1. 静态路由配置
  2. babel-plugin-syntax-dynamic-import
  3. reactrouter-sidebar
  4. ant-design-layout