React router 路由

128 阅读3分钟

demo-react-router:react router 路由学习笔记,适合 react-router v6 和 v7 版本

1. 代码仓库

github.com/chuxin-cs/r…

2. 主线任务

2.1. 依赖下载

pnpm add react-router@7 -S
// or
pnpm add react-router@6 -S

2.2. 配置式路由

1、目录结构如下:

2、router/index.tsx:

import React from 'react';
import { RouterProvider } from 'react-router/dom';
import { Navigate, createHashRouter, Outlet } from 'react-router';

// config
import { MenulevelConfig } from './modules/menulevel';
import { PUBLIC_ROUTE, ERROR_ROUTE, NO_MATCHED_ROUTE } from './sys';

export const Router: React.RC = () => {
  // 受保护的路由(业务路由)
  const PROTECTED_ROUTE = {
    path: '/',
    element: (
      <>
        <Outlet />
      </>
    ),
    children: [
      // 默认的重定向
      { index: true, element: <Navigate to={'/403'} replace /> },
      // 业务模块
      MenulevelConfig,
    ],
  };

  const routes = [
    PUBLIC_ROUTE, // 公共路由
    PROTECTED_ROUTE, // 受保护的路由(业务路由)
    ERROR_ROUTE, // 错误路由
    NO_MATCHED_ROUTE, // 上面都没有匹配到的路由,放最后
  ];
  const router = createHashRouter(routes);
  return <RouterProvider router={router} />;
};

export default Router;

3、router/modules/menulevel:

import { Suspense } from 'react';
import { Outlet, Navigate } from 'react-router';

export const MenulevelConfig = {
  order: 2,
  path: 'menu_level',
  meta: {},
  element: (
    <Suspense fallback={<div>loading...</div>}>
      <Outlet />
    </Suspense>
  ),
  children: [
    {
      path: 'menu_level_1a',
      element: <div>lervel 1a</div>,
    },
    {
      path: 'menu_level_1b',
      children: [
        {
          index: true,
          element: <Navigate to='menu_level_2a' replace />,
        },
        {
          path: 'menu_level_2a',
          element: <div>lervel 2</div>,
        },
        {
          path: 'menu_level_2b',
          element: <div>lervel 2</div>,
        },
      ],
    },
  ],
};

export default MenulevelConfig;

4、router/sys/index.ts:

// index.ts
export { ERROR_ROUTE } from './error-routes';
export { PUBLIC_ROUTE } from './public';
export { NO_MATCHED_ROUTE } from './no-matched';

// error-routes.tsx
import { Suspense, lazy } from 'react';
import { Outlet } from 'react-router';
const Page403 = lazy(() => import('@/pages/sys/error/Page403'));
const Page404 = lazy(() => import('@/pages/sys/error/Page404'));
const Page500 = lazy(() => import('@/pages/sys/error/Page500'));
export const ERROR_ROUTE = {
  element: (
    <div>
      <Suspense fallback={<div>loading...</div>}>
        <Outlet />
      </Suspense>
    </div>
  ),
  children: [
    { path: '403', element: <Page403 /> },
    { path: '404', element: <Page404 /> },
    { path: '500', element: <Page500 /> },
  ],
};

// public.tsx
import Login from '@/pages/sys/login';
export const PUBLIC_ROUTE = {
  path: '/login',
  element: <Login />,
};


// no-matched.tsx
import { Navigate } from 'react-router';
export const NO_MATCHED_ROUTE = {
  path: '*',
  element: <Navigate to='/404' replace />,
};

5、App.tsx:

import Router from './router';

export default function App() {
  return (
    <>
      <Router />
    </>
  );
}

6、访问 http://localhost:5173/#/404

3. 支线任务

3.1. 关于路由鉴权

1、router/components/ProtectedRoute.tsx:

import { Navigate } from 'react-router';

const ProtectedRoute = ({ children }) => {
  // TODO: 鉴权逻辑,当前演示是从本地存储中获取token
  const token = localStorage.getItem('token') || false;
  console.log(token,"===")
  if (!token) {
    return <Navigate to='/login' replace />; // 如果没有token,跳转到登录页面
  }
  return <>{children}</>;
};

export default ProtectedRoute;

2、router/index.tsx

export const Router: React.RC = () => {
  // 受保护的路由(业务路由)
  const PROTECTED_ROUTE = {
    path: '/',
    element: (
      <ProtectedRoute>
        <>
          <Outlet />
        </>
      </ProtectedRoute>
    ),
  };
}
export default Router;

3.2. layouts 布局

1、由于layouts 不在当前文章中,所以组件先只做简单的处理,layouts/index.tsx

import { Outlet } from 'react-router';

const DashboardLayout = () => {
  // layouts的内容省略...
  return <><Outlet /></>;
};

export default DashboardLayout;

2、router/index.tsx

import DashboardLayout from '@/layouts';
import ProtectedRoute from './components/ProtectedRoute';
export const Router: React.RC = () => {
  // 受保护的路由(业务路由)
  const PROTECTED_ROUTE = {
    path: '/',
    element: (
      <ProtectedRoute>
        <DashboardLayout />
      </ProtectedRoute>
    ),
  };
};
export default Router;

3.3. 基于 react-error-boundary 异常页面捕获

1、当页面发生错误时:

以下代码尝试直接使用了未定义的abc

import { Suspense } from 'react';
import { Outlet, Navigate } from 'react-router';

function LevelPage({ children }){
  // 这里的 abc 未定义 会报错
  return <div>{ abc }{ children }</div>
}

export const MenulevelConfig = {
  order: 2,
  path: 'menu_level',
  meta: {},
  element: (
    <Suspense fallback={<div>loading...</div>}>
      <Outlet />
    </Suspense>
  ),
  children: [
    {
      path: 'menu_level_1a',
      element: <LevelPage>lervel 1a</LevelPage>,
    },
  ],
};

export default MenulevelConfig;

2、React 抛出异常,为了不影响用户

3、借助 react-error-boundary

React 应用里,一旦某个组件在渲染、生命周期方法或者构造函数中抛出未捕获的错误,整个应用可能会崩溃,导致页面白屏,给用户带来糟糕的体验

pnpm add react-error-boundary -S

4、修改 router/components/ProtectedRoute.tsx

import { Navigate } from 'react-router';
import { ErrorBoundary } from "react-error-boundary";
import PageError from "@/pages/sys/error/PageError";

const ProtectedRoute = ({ children }) => {
  // ...其他代码
  return <ErrorBoundary FallbackComponent={PageError}>{children}</ErrorBoundary>;
};
export default ProtectedRoute;

5、pages/sys/error/PageError

const PageError = () => {
  return (
    <div>
      <h1>我是捕获的错误页面:PageError</h1>
    </div>
  );
};
export default PageError;

6、访问:http://localhost:5173/#/menu_level/menu_level_1a

错误被 react-error-boundary 捕获了

3.4. DOM 嵌套方式

1、App.tsx

import { Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router';

export default function App() {
  return (
    <Router>
      <Suspense fallback={<div>loading...</div>}>
        <Routes>
          <Route path='/' element={<div>home</div>} />
          <Route path='/about' element={<div>about</div>} />
        </Routes>
      </Suspense>
    </Router>
  );
}

3.5. 路由懒加载

使用 React.lazy + Suspense 实现路由级代码分割

1、App.tsx

import { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router';

// 使用动态导入实现懒加载
const Home = lazy(() => import('./pages/home'));
const Login = lazy(() => import('./pages/sys/login'));

export default function App() {
  return (
    <Router>
      <Suspense fallback={<div>loading...</div>}>
        <Routes>
          <Route path='/' element={<Home />} />
          <Route path='/login' element={<Login />}></Route>
          <Route path='/about' element={<div>about</div>} />
        </Routes>
      </Suspense>
    </Router>
  );
}

3.6. LazyImportComponent 组件

import { Suspense, LazyExoticComponent } from "react";

interface ILazyImportComponent {
  children: LazyExoticComponent<() => JSX.Element>;
}

const LazyImportComponent = ({ children }: ILazyImportComponent) => {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <children />
    </Suspense>
  );
};

1、main.tsx

import ReactDOM from "react-dom/client";
import { lazy } from "react";
import {createHashRouter, RouterProvider} from "react-router";

function Router() {
  const router = createHashRouter([
    {
      path: "/login",
      element: (
        <LazyImportComponent
          children={lazy(() => import("@/pages/Login/index.tsx"))}
          />
      ),
    },
  ]);
  return <RouterProvider router={router} />;
}
ReactDOM.createRoot(document.getElementById("root")!).render( <Router />);