【山竹记账前端-react】2.React Router 6 实践


大纲链接 §

[toc]


1. 安装、初始化路由 ⇧

安装的是 react-router-dom@6.30.6 ⇧

  • react@18.3.1 + react-router-dom@6.30.6
  • 注:react-router@7+ ~ react-router@8+ 有重大破坏性变更,更适配 react@19+

    1
    
    pnpm add react-router-dom@6.30.6

package.json

1
2
3
4
5
6
7
{
  "dependencies": {
    "react": "18.3.1",
    "react-dom": "18.3.1",
    "react-router-dom": "6.30.6"
  }
}

引入到 main.tsx,初始化路由

 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
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
  createBrowserRouter,
  RouterProvider,
  Link,
} from "react-router-dom";

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
  },
  {
    path: "about",
    element: <div>About</div>,
  },
])

// const div = document.getElementById('root') as HTMLElement
const div = document.getElementById('root')
const root = ReactDOM.createRoot(div!)
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)

参考


2. 404路由 errorElement ⇧

https://reactrouter.com.cn/en/main/start/tutorial#handling-not-found-errors 处理未找到错误

  • 和预想的 404 页面不同,React Router 当做错误处理
  • 在路由表中添加属性 errorElement

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    import {
    createBrowserRouter,
    Link,
    } from "react-router-dom";
    
    const router = createBrowserRouter([
    {
    path: "/",
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
    errorElement: <>访问的页面不存在</>,
    },
    {
    path: "about",
    element: <div>About</div>,
    },
    ])
    

可以创建一个单独的 ErrorPage.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import { useRouteError } from 'react-router-dom'

export const ErrorPage: React.FC = () => {
  const error = useRouteError() as
    Error & { statusText?: string, data?: string }
  console.error(error)

  return (
    <div id="error-page">
      <h1>Oops!</h1>
      <p>Sorry, an unexpected error has occurred.</p>
      <p>
        <i>{error.statusText || error.data}</i>
      </p>
    </div>
  )
}

引入到 src/main.tsx 的 errorElement 中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import { ErrorPage } from '@/components/ErrorPage'

const routes = [
  {
    path: '/',
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about">About Us</Link>
      </div>
    ),
    errorElement: (<ErrorPage />),
  },
  {
    path: 'about',
    element: <div>About</div>,
  },
]

ErrorPage


3. 嵌套路由 children ⇧

在 routes 中

  • 添加属性 children,添加子路由
  • 添加属性 <Outlet />,占位

     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
    
    import { ErrorPage } from '@/components/ErrorPage'
    import { Link } from 'react-router-dom'
    
    export const routes = [
    {
    path: '/',
    element: (
      <div>
        <h1>Hello World</h1>
        <Link to="about"> About Us </Link>
        <Outlet />
      </div>
    ),
    errorElement: (<ErrorPage />),
    children: [
      {index: true, element: (<div>请选择</div>)},
      {path: '1',element: <div>1</div>,},
      {path: '2',element: <div>2</div>,},
      {path: '3',element: <div>3</div>,},
    ],
    },
    {
    path: 'about',
    element: <div>About </div>,
    },
    ]
    
  • 访问路由 http://localhost:5173/1 但却跳转到了根路由

    • React Router 并不知道在哪里显示这个节点元素
    • 需要添加个类似插槽的标签 <Outlet />
    • 子路由元素才会显示在 <Outlet /> 占位处
  • 此时如果只是访问根路径,就没有显示子路由元素了,即未匹配子路由

    • 可以添加默认子路由 {index: true, element: (<div>请选择</div>)} 为显示在<Outlet />处的默认内容
    • 可以直接把这个子路由就当做此路径的内容
    • 其他子路由不写 index 属性的,默认为 false
  • 如果父路径element没有其他结构,只有一个<Outlet />占位,那么 element: (<Outlet />) 可以直接省略不写,效果一样

    • outlet 译为 出口
    • 类似 Vue Router 的 <RouterView />

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      
      export const routes = [
      {path: '/3', element: (<div>先匹配的3</div>)},
      {
      path: '/',
      // element: <Outlet />, // 可省略
      children: [
      {index: true, element: (<div>请选择</div>)},
      {path: '1',element: <div>1</div>,},
      {path: '2',element: <div>2</div>,},
      {path: '3',element: <div>3</div>,},
      ],
      },
      {path: '/3', element: (<div>后匹配的3</div>)},
      ]
      

此时,如果单独路由 /3 的路由,和原来路由中的子路由,是该如何匹配?

  • 写在前面的先会匹配到,后面的写的就忽略,不会再匹配

4. 重定向:useNavigate ⇧

welcome/1,2,3,4 的最佳实践,改造路由表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ErrorPage } from '@/components/ErrorPage'
import { Outlet } from 'react-router-dom'

export const routes = [
  {
    path: '/',
    errorElement: (<ErrorPage />),
    children: [
      { index: true, element: (<div>root</div>) },
      {
        path: 'welcome',
        element: (<Outlet />),
        children: [
          { index: true, element: <div>welcome root</div> },
          { path: '1', element: <div>welcome 1</div> },
          { path: '2', element: <div>welcome 2</div> },
          { path: '3', element: <div>welcome 3</div> },
          { path: '4', element: <div>welcome 4</div> },
        ],

      },
    ],
  },
]

重命名 errorElement 组件 <ErrorPage /> 为 <RedirectToWelcome1 /> ,并修改为强行跳转重定向到 /welcome/1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'

export const RedirectToWelcome1: React.FC = () => {
  const nav = useNavigate()
  useEffect(() => {
    nav('/welcome/1')
    // eslint-disable-next-line react/exhaustive-deps
  }, [])

  return null
}
  • 使用 useNavigate 获取导航函数 nav
  • nav必须在 useEffect 中使用
  • 即可以在 errorElement 中做重定向路由
  • 目前就可以实现在子路由 /welcome 中任意不匹配的路由都重定向到 /welcome/1

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    import { RedirectToWelcome1 } from '@/components/RedirectToWelcome1'
    import { Outlet } from 'react-router-dom'
    
    export const routes = [
    {
    path: '/',
    errorElement: (<RedirectToWelcome1 />),
    children: [
      { index: true, element: (<div>root</div>) },
      {
        path: 'welcome',
        children: [
          { index: true, element: <div>welcome root</div> },
          { path: '1', element: <div>welcome 1</div> },
          { path: '2', element: <div>welcome 2</div> },
          { path: '3', element: <div>welcome 3</div> },
          { path: '4', element: <div>welcome 4</div> },
        ],
    
      },
    ],
    },
    ]
    

5. 路由页面过渡动画:react-spring 入门 ⇧

 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
import { RedirectToWelcome1 } from '@/components/RedirectToWelcome1'
import { NavLink, Outlet } from 'react-router-dom'

export const routes = [
  {
    path: '/',
    errorElement: (<RedirectToWelcome1 />),
    children: [
      { index: true, element: (<div>root</div>) },
      {
        path: 'welcome',
        element: (<Outlet />),
        children: [
          { index: true, element: <div>welcome root</div> },
          { path: '1', element: (
            <div>
              welcome 1
              <NavLink to="/welcome/2">下一页</NavLink>
            </div>
          ) },
          { path: '2', element: (
            <div>
              welcome 2
              <NavLink to="/welcome/3">下一页</NavLink>
            </div>
          ) },
          { path: '3', element: (
            <div>
              welcome 3
              <NavLink to="/welcome/4">下一页</NavLink>
            </div>
          ) },
          { path: '4', element: (
            <div>
              welcome 4
              <NavLink to="/welcome/1">下一页</NavLink>
            </div>
          ) },
        ],

      },
    ],
  },
]

react-spring ⇧

由于使用 react@18.3.1,按照官网区分@19±不同版本,需要安装 pnpm add @react-spring/web@9

1
pnpm add @react-spring/web@9.7.5

添加 @/components/Demo.tsx

 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
import { animated, useSpring } from '@react-spring/web'

export const Demo: React.FC = () => {
  const [springs, api] = useSpring(() => ({
    from: { x: 0 },
    to: { x: 100 },
  }))

  const onClick = () => {
    api.start({
      x: Math.random() * 100,
    })
  }

  return (
    <animated.div
      onClick={onClick}
      style={{
        width: 80,
        height: 80,
        background: '#ff6d6d',
        borderRadius: 8,
        ...springs,
      }}
    />
  )
}
  • <animated.div style={...} /> 添加部分默认样式
  • useSpring 传参为一个函数 () => ({from: { x: 0 }, to: { x: 100 },})
    • 解构出 [springs, api]
    • springs 添加到 style 中,动画配置属性
    • api 控制操作
    • api.start({ from: {x: 0}, to: {x: 100} })

引入到 routes.tsx 查看效果

 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
// ...
export const routes = [
  {
    path: '/',
    errorElement: (<RedirectToWelcome1 />),
    children: [
      { index: true, element: (<div>root</div>) },
      {
        path: 'welcome',
        element: (<Outlet />),
        children: [
          { index: true, element: <div>welcome root</div> },
          {
            path: '1',
            element: (
              <div>
                welcome 1
                <Demo />
                <NavLink to="/welcome/2">下一页</NavLink>
              </div>
            ),
          },
          // ...
        ],
      },
    ],
  },
]
  • 以上 Demo 只实现了对于一个元素的动画
  • 路由需要实现两个页面之间的过渡
  • 删掉 Demo

参考


重构代码:拆分路由 ⇧

单页应用只有一个 router,提取路由表到文件 @/route/routes.tsx

 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
import { RedirectToWelcome1 } from '@/components/RedirectToWelcome1'
import { NavLink, Outlet } from 'react-router-dom'

export const routes = [
  {
    path: '/',
    errorElement: (<RedirectToWelcome1 />),
    children: [
      { index: true, element: (<div>root</div>) },
      {
        path: 'welcome',
        element: (<Outlet />),
        children: [
          { index: true, element: <div>welcome root</div> },
          {
            path: '1',
            element: (<div> welcome 1 <NavLink to="/welcome/2">下一页</NavLink> </div>),
          },
          {
            path: '2',
            element: (<div> welcome 2 <NavLink to="/welcome/3">下一页</NavLink> </div>),
          },
          {
            path: '3',
            element: (<div> welcome 3 <NavLink to="/welcome/4">下一页</NavLink> </div>),
          },
          {
            path: '4',
            element: (<div> welcome 4 <NavLink to="/welcome/1">下一页</NavLink> </div>),
          },
        ],
      },
    ],
  },
]

重构新建 @/route/router.tsx

1
2
3
4
import { routes } from '@/route/routes'
import { createBrowserRouter } from 'react-router-dom'

export const router = createBrowserRouter(routes)

重构 @/main.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { router } from '@/route/router'

// const div = document.getElementById('root') as HTMLElement
const div = document.getElementById('root')

const root = ReactDOM.createRoot(div!)

root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>,
)
  • @/main.tsx 渲染根组件
    • 根组件引入 <RouterProvider router={router} />

重构代码:抽取组件到 @/layouts/ @/pages/ ⇧

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import { Outlet } from 'react-router-dom'

export const MainLayout: React.FC = () => {
  return (
    <div>
      <Outlet />
    </div>
  )
}

// `@/components/layouts/WelcomeMainLayout.tsx`
import { Outlet } from 'react-router-dom'

export const WelcomeMainLayout: React.FC = () => {
  return (
    <div>
      <Outlet />
    </div>
  )
}

提取四个页面组件 @/pages/Welcome1.tsx

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import { NavLink } from 'react-router-dom'

export const Welcome1: React.FC = () => {
  return (
    <div>
      welcome 1
      <NavLink to="/welcome/2">下一页</NavLink>
    </div>
  )
}

重构 @/route/routes.tsx

  • Ctrl + Shift + J 合并成一行 JoinLine

     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
    
    import { NavLink } from 'react-router-dom'
    import { RedirectToWelcome1 } from '@/components/RedirectToWelcome1'
    import { MainLayout } from '@/components/layouts/MainLayout'
    import { WelcomeMainLayout } from '@/components/layouts/WelcomeMainLayout'
    import { Welcome1 } from '@/pages/Welcome1'
    import { Welcome2 } from '@/pages/Welcome2'
    import { Welcome3 } from '@/pages/Welcome3'
    import { Welcome4 } from '@/pages/Welcome4'
    
    export const routes = [
    {
    path: '/',
    element: <MainLayout />,
    errorElement: (<RedirectToWelcome1 />),
    children: [
      {
        path: 'welcome',
        element: <WelcomeMainLayout />,
        children: [
          { path: '1', element: (<Welcome1 />) },
          { path: '2', element: (<Welcome2 />) },
          { path: '3', element: (<Welcome3 />) },
          { path: '4', element: (<Welcome4 />) },
        ],
      },
    ],
    },
    ]
    

重构代码:目录结构变化 ⇧

目前目录结构变得清爽

  • @/components/ 放通用组件
  • @/layouts/ 放布局组件
  • @/pages/ 放页面组件
  • @/router/ 放路由相关

在 VSCode 中重构

  • 例如重命名、移动组件,对应路径自动改变,需要装 Auto Import 之类的插件
  • 如果错误 typescript 会立马报错
  • 分目录

·未完待续·

参考文章

相关文章


  • 作者: Joel
  • 文章链接:
  • 版权声明
  • 非自由转载-非商用-非衍生-保持署名