React + Router

React + Router

这个只是专门讲解 React Router 新开的例子。

教程来源:https://reactrouter.com/en/main/start/tutorial

创建新项目

yarn create vite my-react-router-app --template react-ts
cd my-react-router-app
yarn

安装 React Router 依赖:

yarn add react-router-dom localforage match-sorter sort-by
# 对于 ts 要添加 @types/sort-by, 否则报错找不到模块【不影响运行】
yarn add -D @types/sort-by

运行项目

yarn dev

# VITE v4.4.11  ready in 358 ms

# ➜  Local:   http://localhost:5173/
# ➜  Network: use --host to expose
# ➜  press h to show help

获取该项目所需要的文件

这里找到 css 文件,将其粘贴到 src/index.css 文件中: Copy/Paste the tutorial CSS

这里找到 js 文件,将其添加到 src/contacts.js:Copy/Paste the tutorial CSS

Js 文件主要的用途:将创建、阅读、搜索、更新和删除数据。一个典型的Web应用程序可能会与Web服务器上的API对话,将使用浏览器存储并伪造一些网络延迟来保持这一点。这些代码都与React Router无关,所以直接复制/粘贴即可。

2个文件拷贝到项目中后,你可以将无关文件删除,可以删除任何其他内容(如 App.jsassets 等)。

项目主要文件(文件目录):

src
├── contacts.js # 这里最好转换成 ts 文件,即添加上类型,方便后面
├── index.css
└── main.tsx

添加路由器

main.tsx 中创建并渲染浏览器路由器

// 导入路由模块
import {
  createBrowserRouter,
  RouterProvider
} from "react-router-dom"

// 配置路由
const router = createBrowserRouter([
  {
    // 根路由 root router
    path: "/",
    // 之后会替换成组件
    element: 
Hello world!
} ]) // 渲染:将其添加到 render 函数中作为参数传递即可 ReactDOM.createRoot(document.getElementById('root')!).render( , )

创建根路由

创建 src/routessrc/routes/root.tsx

rout.tsx 文件如下:

export default function Root() {
  return (
    <>
      

回到 src/main.tsx 的 router 变量处,将

Hello World
替换成组件。

import Root from './routes/root'

// 配置路由
const router = createBrowserRouter([
  {
    path: "/",
    element: 
  }
])

好了,基本结构搭建完毕!运行项目看看吧。

yarn dev

处理错误页面

以前在写 React 的时候,因为是单页面应用,所以地址栏的变化不会影响程序的影响。

但是导入了 React Router 组件后,地址栏会被它所监听到,从而出现 React Router 默认的错误屏幕。

创建 src/error-page.tsx 文件。

import { useRouteError } from "react-router-dom";

export default function ErrorPage() {
  const error = useRouteError();
  console.error(error);

  return (
    

Oops!

Sorry, an unexpected error has occurred.

{error.statusText || error.message}

); }

回到 src/main.tsx 文件。

将根路由上的 设置为 errorElement

import ErrorPage from './error-page'

// 配置路由
const router = createBrowserRouter([
  {
    path: "/",
    element: ,
    errorElement: 
  }
])

然后我们随机访问一个页面,http://localhost:5173/contacts/1 会显示我们创建的错误页面(ErrorPage 组件)。

useRouteError 提供了抛出的错误。当用户导航到不存在的路由时,您将得到一个错误响应,并显示“Not Found” statusText

创建联系人路由

有了根路由,那么其他页面呢?

创建一个 src/routes/contact.tsx文件

import { Form } from "react-router-dom";

export default function Contact() {
  const contact = {
    first: "Your",
    last: "Name",
    avatar: "https://placekitten.com/g/200/200",
    twitter: "your_handle",
    notes: "Some notes",
    favorite: true,
  };

  return (
    

{contact.first || contact.last ? ( <> {contact.first} {contact.last} ) : ( No Name )}{" "}

{contact.twitter && (

{contact.twitter}

)} {contact.notes &&

{contact.notes}

}
{ if ( !confirm( "Please confirm you want to delete this record." ) ) { event.preventDefault(); } }} >
); } function Favorite({ contact }) { // yes, this is a `let` for later let favorite = contact.favorite; return (
); }

然后我们到 src/main.tsx 进行添加路由信息,还是配置那里。

// 配置路由
const router = createBrowserRouter([
  //....
  {
    path: "contacts/:contactId",
    element: ,
    errorElement: 
  }
])

会发现,它不在我们的根布局中(右侧没有 组件,即侧边导航栏没有看到)

如果希望contact组件在 布局中呈现。

我们通过使联系路由成为根路由的子路由来实现这一点。

回到我们的 src/main,tsx 的配置路由信息:

// 将添加的 contacts 路由信息移动到 Root 的 children 属性上
// 配置路由
const router = createBrowserRouter([
  {
    //....
    children: [
      {
        path: "contacts/:contactId",
        element: 
      }
    ]
  },
])

再次看到根布局,但右侧是一个空白页面。我们需要告诉根路由我们希望它在哪里渲染其子路由。

回到我们的 组件中(src/routes/rout.tsx

Render an Outlet 渲染一个

// 导入
import { Outlet } from "react-router-dom"

// 在哪里进行渲染

客户端路由 CSR

Client Side Routing 。

在路由跳转之间,我们发现,浏览器是请求整个网页文档。而不是使用 React Router。

客户端路由允许我们的应用更新URL,而无需从服务器请求另一个文档。【立即呈现新的UI】

使用 组件

import { Link } from "react-router-dom"

// 将 a 标签替换成  

加载数据

前面我们只是将数据写死,该节内容就是如何通过 React Router 调用 API 去获取数据。

注意:这是一个 Demo,真实的情况请采用 axios 去获取后端接口。

到我们的根路由组件()

import { getContacts } from "../contacts"

export async function loader() {
    const contacts = await getContacts();
    return { contacts };
}

再来到 src/main.tsx 文件,进行配置 loader

import Root, { loader as rootLoader } from './routes/root'

// 配置路由
const router = createBrowserRouter([
  {
    // ...
    loader: rootLoader,
    // ...
  },
])

然后回到我们的根路由组件()

import { Outlet, Link, useLoaderData } from "react-router-dom"

// 其他代码...

export default function Root() {
  const { contacts }: any = useLoaderData();
  
  return (
  	<>
    	{* 其他代码... *}
			
			 {* 其他代码... *}
    
  )
}

好了,看看页面吧~

创建联系人

在页面中我们可以看到没有联系人列表。

这个时候我们需要完成新建功能,看到 New 按钮了吗?

点击一下会发生什么?呜?出错了?找不到 localhost 的网页

这里正常情况会报 405 错误,找不到对应的请求方式。

这里可以采取两种方式: 给 vite 增加 post 提交功能,或者采用 CSR 客户端路由方式【本节重点】。

回到我们的根路由组件()

import { Outlet, Link, useLoaderData, Form } from "react-router-dom"
import { getContacts, createContact } from "../contacts";

// 新增联系人
export async function action() {
    const contact = await createContact();
    return { contact };
}

// ...

return (
	// ...
  
)

回到 src/main.tsx 配置 action

import Root, { loader as rootLoader, action as rootAction } from './routes/root'

// 配置路由
const router = createBrowserRouter([
  {
    path: "/",
    element: ,
    errorElement: ,
    loader: rootLoader,
    action: rootAction,
    //...
])

但是思考一下。

当我们点击 New 后,页面如何更新的? action 在哪里?重新获取数据的代码在哪里? useStateonSubmituseEffect 在哪里?!

阻止浏览器向服务器发送请求,而是将其发送到路由 action 。在Web语义中,POST通常意味着某些数据正在更改。按照惯例,React Router将此作为提示,在操作完成后自动重新验证页面上的数据。这意味着你所有的 useLoaderData 钩子都会更新,UI会自动与你的数据保持同步!

所以,原理在于 React Router 根据 POST (意味着要更新数据了),去自动帮助我们更新源代码。

获取 Loader 中 URL 参数

当点击联系人列表的某个名称时,会跳转到某个页面,只是 id 变成我们模拟出来的了,而不是写死的。

但是这里有个问题,右侧还是 组件,数据没有进行更新。

在 也需要 loader ,这里与根路由一样,不再阐述,只会给关键代码。

// src/routes/contact.tsx
import { Form, useLoaderData } from "react-router-dom"
import { getContact } from "../contacts"

export async function loader({ params }: any) {
    const contact = await getContact(params.contactId)
    return { contact }
}

export default function Contact() {
    const { contact }: any = useLoaderData(); 
    // ...
}
// src/main.tsx
import Contact, { loader as contactLoader } from './routes/contact'

// 配置路由
const router = createBrowserRouter([
  {
    //....
    children: [
      {
        path: "contacts/:contactId",
        element: ,
        loader: contactLoader
      }
    ]
  },
])

更新数据

一个新文件 src/routes/edit.tsx

import { Form, useLoaderData } from "react-router-dom";

// 自己补充 loader ,参考 contact.tsx

export default function EditContact() {
  const { contact } = useLoaderData();

  return (
    
      

Name