# React-Router 6

# React Router6 对比 React Router5

React-Router6对比React-router5,更像Vue-Router了

  • 首先5的Route path都是绝对路径,不支持相对路径,而6则支持相对路径

Route组件属性

  • path:定义路由的路径,可以是字符串或字符串数组。例如,path="/users" 或 path={['/users', '/admins']}。
  • element:指定路由对应的组件。例如,element={<Home />}。
  • children:在路由匹配时渲染的子元素。与 element 属性互斥,不能同时使用。
  • caseSensitive:指定路由路径是否区分大小写,默认为 false。
  • sensitive:指定路由是否对 URL 参数区分大小写,默认为 false。
  • mergeParams:在嵌套路由中,是否将父路由的参数合并到子路由中,默认为 false。
  • index:指定路由为默认路由,即没有匹配的路径时显示的组件。
  • fallback:指定当路由匹配时渲染的后备组件(通常在路由懒加载时使用)。

其中没有5的component属性了,取而代之的是element属性

# Outlet组件

在 React Router 6 中,<Outlet> 组件用于渲染嵌套路由的子路由内容。它充当了父级路由组件的占位符,用于显示嵌套路由的子路由。

类似于Vue Router中的router-view

对应hooks 为 useOutlet,二者功能相同

即<router-link />

# 编程式导航

useNavigate

const navigate = useNavigate()
navigate("/b"); // 跳转
1
2
export declare function useNavigate(): NavigateFunction;
export interface NavigateFunction {
    (to: To, options?: NavigateOptions): void;
    (delta: number): void;
}

export interface NavigateOptions {
    replace?: boolean;
    state?: any;
    preventScrollReset?: boolean;
    relative?: RelativeRoutingType;
}
1
2
3
4
5
6
7
8
9
10
11
12

<NavLink>是<Link>的一个特定版本,会在匹配上当前的url的时候给已经渲染的元素添加参数

<a href="/b">to b</a>

<a aria-current="page" class="active" href="/">a</a>
1
2
3

上面是Link渲染出来的,下面是NavLink渲染的,可以看到多出来一个class="active"

这样就可以设置默认激活,做上色等操作

aria-content (opens new window)是无障碍属性

# URL prarms传递

import { Routes, Route, useParams } from "react-router-dom";

function App() {
  return (
    <Routes>
      <Route
        path="invoices/:userid"
        element={<Invoice />}
      />
    </Routes>
  );
}

function Invoice() {
  let params = useParams();
  return <h1>Invoice {params.userid}</h1>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 嵌套路由

<Outlet /> 组件即 Vue-Router中的 router-view

function Header() {
  return (
    <ul className="navs">
      <li className="nav">
        <Link to="/invoices/1">to invoice 1</Link> |
        <Link to="/invoices/2">to invoice 2</Link>
      </li>
    </ul>
  );
}
function View() {
  return (
    <Routes>
      <Route path="a" element={<Invoices />}></Route>
      <Route path="b/:userid" element={<Invoice />}></Route>
      <Route path="/invoices" element={<Invoices />}>
        <Route path=":invoiceid" element={<Invoice />}></Route>
        <Route path="2" element={<SendInvoice />}></Route>
      </Route>
    </Routes>
  );
}
function Invoices() {
  return (
    <>
      <h3>Invoices</h3>
      <Outlet />
    </>
  );
}

function Invoice() {
  const params = useParams();
  return (
    <>
      <div>Invoice id</div>
      <div>{params.invoiceid}</div>
    </>
  );
}

function SendInvoice() {
  return <div>SendInvoice</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# React Router6嵌套路由

# 1.使用Route组件嵌套实现

<Router>
  <Suspense fallback={<div>loading...</div>}>
    {/* <RouterProvider router={router} /> */}
    <Routes>
      <Route path="/" element={<Navigate to="/dashboard" />}></Route>
      <Route path="/dashboard" element={"about"} />
      <Route path="/settings" element={<div>settings <Outlet /></div>}>
        {/* 这里为了解决重定向需要新增一条带index的路由 */}
        <Route index element={<Navigate to='profile' />} />
        <Route path="profile" element={"profile"} />
        <Route path="profile2" element={"profile2"} />
      </Route>
    </Routes>
  </Suspense>
</Router>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

使用这种方式能够很好的解决redirect重定向问题

# 2.使用createBrowserRouter创建配置式路由

const routes: RouteObject[] = [
  {
    path: "/",
    element: <AppLayout />,
    children: [
      { path: "", element: <span>/</span> },
      { path: "/about", element: <span>About</span> },
      { path: "test", element: <span>Test</span> },
      {
        path: "layout",
        element: <Layout />,
        children: [
          { path: "1", element: <span>layout1</span> },
          { path: "2", element: <span>layout2</span> },
          {
            path: "3",
            element: <Layout3 />,
            children: [
              { path: "1", element: <span>layout/3-1</span> },
              { path: "2", element: <span>layout/3-2</span> }
            ]
          }
        ]
      }
    ]
  },
  { path: "/login", element: <span>Login</span> }
];
const router = createBrowserRouter(routes);

const App = () => <RouterProvider router={router} />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
### 3.使用useRoutes创建配置式路由
const routes: RouteObject[] = []

function Index() {
  const element = useRoutes(routes);
  return element;
  // 或者直接 return useRoutes(routes)
}

export default function App() {
  return (
    <div className="App">
      {/* <RouterProvider router={router} /> */}
      <Router>
        <Index />
      </Router>
    </div>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

==useRoutes() may be used only in the context of a <Router> component.==

useRoutes 必须在 <Router>组件的上下文中使用

RouterProvider对比useRoutes

RouterProvider 用于整个应用的静态路由配置,而 useRoutes 用于在特定组件内部进行动态路由匹配和渲染

# token无效时跳转登录页

# 定义高阶组件 Auth

实现类似于beforeEach功能的导航守卫

/**
 * 路由守卫
 * @description 验证用户token是否在有效期。
 */
const RouterAuth = ({ children }: { children: JSX.Element }) => {
    const location = useLocation();
    const token = useUser((state) => state.user.token);

    if (!token) {
        return <Navigate to="/login" state={location} replace />;
    }

    return children;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

而token则存储到全局store中,在axios拦截器中设置token清除token

service.interceptors.response.use(
    (response) => {
        if (response.data.errCode === 1004001) {
            useUserStore.getState().setLoginStatus(false);
        }
        return response;
    },
    (error: AxiosError) => {
        if (error.response?.status === 401 || error.response?.status === 500) {
            message.error("登录凭证无效!");
            useUserStore.getState().setToken('')
            return Promise.reject((error.response.data as ResponseData).error);
        }
        return Promise.reject(error);
    }
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 获取URL查询参数

在Vue-Router中我们可以通过$route.query获取URL查询参数,而在React-Router中没有对此进行封装,可以通过

useLocation拿到未序列化的查询参数类似于?name=zs&age=20这种格式

const { search } = useLocation()
const queryParams = Object.fromEntries(new URLSearchParams(location.search).entries())
// { name: 'zs', age: 20 }
1
2
3

封装一下

/**
 * 获取序列化后的URL查询参数 ?a=1&b=2 返回{ a:1, b:2 }
 * @returns
 */
export const useQueryParams = () =>
    Object.fromEntries(new URLSearchParams(location.search).entries());
1
2
3
4
5
6

当TypeScript提示Property 'entries' does not exist on type 'URLSearchParams'.ts(2339)时,

这是因为tsconfig.json的lib字段缺少DOM.Iterable

使用Ts加上类型声明

type QueryParams<T> = { [K in keyof T]: string };
// 用Record代替
type QueryParams<T> = Record<keyof T, string>

/**
 * 获取序列化后的URL查询参数 ?a=1&b=2 返回{ a:1, b:2 }
 * @returns
 */
export const useQueryParams = <T>(): QueryParams<T> =>
    Object.fromEntries(new URLSearchParams(location.search).entries()) as QueryParams<T>;
1
2
3
4
5
6
7
8
9
10

# React Router 6有类似于VueRouter的addRoute方法吗

在 React Router 中,路由配置是静态的,通常是在组件中声明的。

然而,你可以通过使用 useRoutes 钩子以编程的方式动态创建路由

const App = () => {
  // 定义动态路由配置
  const dynamicRoutes = {
    path: '/',
    element: <Home />, // 默认首页
    children: [
      { path: 'about', element: <About /> },
      // 可以根据需要添加更多的动态路由
    ],
  };

  // 使用 useRoutes 钩子将动态路由配置应用到实际路由中
  const routeElement = useRoutes(dynamicRoutes);

  return (
    <Router>
      <Routes>{routeElement}</Routes>
    </Router>
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# React Router6实现全局导航守卫BeforeEach

import { useContext } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { globalContext } from '../App';
import { rotuesMap } from '.';

type RouterBeforeEachProps = {
  children: JSX.Element;
};

const RouterBeforeEach: React.FC<RouterBeforeEachProps> = ({ children }) => {
  const { isLogin } = useContext(globalContext);
  const location = useLocation();

  const to = rotuesMap.get(location.pathname);
  console.log('to: ', to);

  // 如果未登录,且去的页面不是登录页,则重定向到登录页
  if (!isLogin && to?.path !== '/login') {
    return <Navigate to='/login' />;
  }

  // 如果已登录,不可直接去登录页,应通过logout退出登录来跳转登录页
  if (to?.path === '/login' && isLogin) {
    return <Navigate to='/' />;
  }
  // 需要鉴权的页面
  if (to?.meta?.requireAuth && !isLogin) {
    // 判断有没有这个页面的权限
    // ...
    // 没有则跳转至指定页
    return <Navigate to='/unpermission' />;
  }
  if (to?.redirect) {
    // 这里需要判断redirect是绝对路径,还是相对路径
    if (to.redirect.startsWith('/')) {
      return <Navigate to={to.redirect} />;
    } else {
      const redirect = `${to.fullPath}/${to.redirect}`.replace(/\/\//, '/');
      return <Navigate to={redirect} />;
    }
  }

  return children;
};
export default RouterBeforeEach;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

使用时给routes内需要拦截的路由element包起来即可

由于我这里使用的是嵌套路由,所以只要包第一层的路由即可

const routes: RouteConfig[] = [
  {
    path: '/login',
    element: (
      <RouterBeforeEach>
        <Login />
      </RouterBeforeEach>
    )
  },
  {
    path: '/',
    errorElement: <Error />,
    element: (
      <RouterBeforeEach>
        <AppLayout />
      </RouterBeforeEach>
    ),
    children: [
      { path: '', element: <span>/</span> },
      { path: 'about', element: <About /> },
      { path: 'test', element: <Test /> },
      {
        path: 'layout',
        // element: <Layout />,
        componentPath: 'pages/layout1/index',
        children: [
          { path: '1', componentPath: 'pages/layout1-1/index' },
          { path: '2', componentPath: 'pages/layout1-2/index' },
          {
            path: '3',
            componentPath: 'pages/layout1-3/index',
            children: [
              { path: '1', componentPath: 'pages/layout1-3-1/index' },
              { path: '2', componentPath: 'pages/layout1-3-2/index' }
            ]
          }
        ]
      }
    ]
  },
  { path: '*', element: <NotFound /> }
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

详细代码:https://github.com/fncheng/react-router6-guard/blob/master/src/router/index.tsx

# 如果使用Vue框架

<template>
  <div :style="{ color: 'red' }">Here is the number: {{ number }}</div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { useNumber } from '../pages/useNumber'

const number = ref()
const getNumber = async () => {
  const value = await useNumber()
  number.value = value
}

getNumber()
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16