React 19 + React-Router v7 超级详细、实用、好理解的优雅动态路由懒加载

10,241 阅读3分钟

2025-02-10 10:01:47 更新最新代码

前言

  • 项目地址:github.com/smithyj/exp…
  • 本文仅面向前端用户,其他端请绕道。
  • 项目选型了 RspackReact 19React Router v7 作为项目基础。
  • Rspack 官方教程搭建下来,顺利启动项目,并按 React Router v7 官方文档简单集成。

问题

  • 集成进来的都很丑,而且做懒加载实现方式不够优雅,而且网上也都是如下图一般的教程。
    • 这个还行,比其他的好多了,但是还不够优雅
    • 其他的都如下图一般,他们写的难道不膈应吗?(这个世界上就没有喜欢优雅的吗?)
      • image.png

过程

  • 发现了问题,那么我们就来想办法来解决,给自己先提一个需求

    • 1、可配置化
    • 2、最多配置一个地方
    • 3、支持 middlewares
      • 这里可以喊后端同学看看,这个概念在前端里面可以叫做路由守卫
      • 实现了这个,基本上其他的都很好实现了
  • 第一步,我们在 index.tsx 引入 App

import ReactDOM from 'react-dom/client'
import App from './App'

const rootEl = document.getElementById('root')
if (rootEl) {
  const root = ReactDOM.createRoot(rootEl)
  root.render(<App />)
}
  • 第二步,我们在 App.tsx 中引入 RouterProvider
import { ConfigProvider, App as AppContainer } from 'antd'
import zhCN from 'antd/locale/zh_CN'
import './App.less'
import { RouterProvider } from 'react-router'
import { router } from './router'

const App = () => {
  return (
    <ConfigProvider locale={zhCN}>
      <AppContainer component={false}>
        <RouterProvider router={router} />
      </AppContainer>
    </ConfigProvider>
  )
}

export default App
  • 第三步,我们先实现懒加载组件 components/LazyImport
import { ComponentType, FC, LazyExoticComponent, Suspense } from 'react'
import LazyLoading from '@/components/LazyLoading'

interface LazyImportProps {
  lazy?: LazyExoticComponent<ComponentType>
}

export const LazyImport: FC<LazyImportProps> = ({ lazy }) => {
  const Component = lazy ? lazy : () => null
  return (
    <Suspense fallback={<LazyLoading />}>
      <Component />
    </Suspense>
  )
}
  • 第四步,我们先实现懒加载 Loading 组件 components/LazyLoading
import { Col, Row, Spin } from 'antd'

const LazyLoading = () => {
  return (
    <Row align="middle" justify="center" style={{ minHeight: '100%' }}>
      <Col>
        <Spin spinning />
      </Col>
    </Row>
  )
}

export default LazyLoading
  • 第五步,我们来实现下 router/index.tsx
    • 这里细心点看,应该可以发现爆改了下 element 属性类型
    • 然后导出了个 router 变量给 RouterProvider,主要是为了以后可以在非页面中使用路由能力
import { lazy } from 'react'
import { buildRoutes, RouteConfig } from './utils'
import { createBrowserRouter } from 'react-router'
import ErrorBoundary from '@/components/ErrorBoundary'

const routeConfig: RouteConfig[] = [
  {
    ErrorBoundary: ErrorBoundary,
    children: [
      {
        path: '/login',
        element: lazy(() => import('@/pages/Login')),
      },
      {
        // 应用基础布局
        element: lazy(() => import('@/layouts/BasicLayout')),
        middlewares: [
          // 管理员登录验证中间件
          lazy(() => import('@/middlewares/AdminAuthMiddleware')),
        ],
        children: [
          {
            path: '/',
            index: true,
            element: lazy(() => import('@/pages/Home')),
          },
          {
            // 谷歌验证
            path: '/google-2fa',
            element: lazy(() => import('@/pages/Google2FA')),
          },
          {
            middlewares: [
              // 谷歌验证中间件
              lazy(() => import('@/middlewares/AdminGoogle2FAMiddleware')),
              // 页面权限验证中间件
              lazy(() => import('@/middlewares/AdminPagePermissionMiddleware')),
            ],
            children: [
              {
                // 解绑谷歌验证
                path: '/unbind-google-2fa',
                element: lazy(() => import('@/pages/UnbindGoogle2FA')),
              },
              {
                // 修改密码
                path: '/change-password',
                element: lazy(() => import('@/pages/ChangePassword')),
              },
              {
                // 仪表盘
                path: '/dashboard',
                element: lazy(() => import('@/pages/Dashboard')),
              },
              {
                // 系统设置
                path: '/setting',
                element: lazy(() => import('@/pages/Setting')),
              },
              {
                // 管理员管理
                path: '/admin',
                element: lazy(() => import('@/pages/Admin')),
              },
              {
                // 角色管理
                path: '/admin/role',
                element: lazy(() => import('@/pages/AdminRole')),
              },
              {
                // 权限管理
                path: '/admin/permission',
                element: lazy(() => import('@/pages/AdminPermission')),
              },
            ],
          },
        ],
      },
    ],
  },
]

export const routes = buildRoutes(routeConfig)

export const router = createBrowserRouter(routes)
  • 第六步,为了实现我们的第五步功能,我们来实现下 router/utils.tsx
    • 重点就是这个文件啦
    • 为了更好的实现和IDE智能提示(最讨厌IDE爆红了)
    • 我们自定义了一个 RouteConfig 来重新实现路由配置,直接继承自 RouteObject,并且为了配置组件统一性,减除了不必要的属性(lazy,Component,children,element)
    • 我们自定义了 elementmiddlewareschildren 属性
import { Outlet, RouteObject } from 'react-router'
import { ComponentType, LazyExoticComponent } from 'react'
import { LazyImport } from '@/components/LazyImport'

export type LazyComponent = LazyExoticComponent<ComponentType>

export type RouteConfig = Omit<
  RouteObject,
  'element' | 'children' | 'Component' | 'lazy'
> & {
  element?: LazyComponent
  middlewares?: LazyComponent[]
  children?: RouteConfig[]
}

export const buildRoutes = (routes: RouteConfig[]): RouteObject[] => {
  return routes.map((item) => {
    const { element, middlewares, children, ...restProps } = item

    // 要返回的路由对象
    let routeObject: RouteObject = {
      ...restProps,
    }

    // 递归构建子路由
    if (children) {
      routeObject.children = buildRoutes(children)
    }

    // 异步加载组件
    routeObject.element = element ? <LazyImport lazy={element} /> : undefined

    // 中间件处理
    if (middlewares && middlewares.length > 0) {
      // 从后往前遍历中间件,这样中间件的执行顺序就是从前往后
      // 例如:[A, B, C] => A(B(C()))
      for (let i = middlewares.length - 1; i >= 0; i--) {
        const middleware = middlewares[i]
        routeObject = {
          element: <LazyImport lazy={middleware} />,
          children: [routeObject],
        }
      }
    } else {
      // 如果没有中间件,也没有 element 则传入 <Outlet />
      routeObject.element = routeObject.element ?? <Outlet />
    }

    // 返回路由对象
    return routeObject
  })
}
  • 最终效果

image.png

具体可以将项目拉下来体验下,感谢各位的阅读,也欢迎各位评论区留言指点不足之处。