以下例子是根据vite+react+ts构建的,使用路由前先安装好这些环境!!!!
1、路由的简单使用
首先要创建一个浏览器路由器并配置我们的第一个路由。这将为我们的 Web 应用启用客户端路由。
该main.jsx
文件是入口点。打开它,我们将把 React Router 放到页面上。
import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "./index.css";
const router = createBrowserRouter([
{
path: "/",
element: <div>Hello world!</div>,
},
]);
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
我们通常将第一个路由称为“根路由”,因为其余路由将在其中呈现。它将作为 UI 的根布局,随着我们进一步深入,我们将拥有嵌套布局。
正常访问的页面因该是:
如果访问不存在的路由页面将是如下:
2、创建根路径和错误页面
2.1、创建根布局组件(src/routes/root.tsx)
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<a href={`/contacts/1`}>Your Name</a>
</li>
<li>
<a href={`/contacts/2`}>Your Friend</a>
</li>
</ul>
</nav>
</div>
<div id="detail"></div>
</>
);
}
目前还没有任何特定于 React Router 的内容,因此请随意复制/粘贴所有内容。
注意:其实一般是挂在app上的,实际开发中应该是app作为根路由的!!!
2.2、设置<Root>
为根路由element
import React from 'react'
import ReactDOM from 'react-dom/client'
import Root from './routes/root'
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom'
const router = createBrowserRouter([
{
path: "/",
element: <Root></Root>,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
)
2.3、创建错误页面组件挂载
路径:src/error/error-page.tsx
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
将设置<ErrorPage>
为errorElement根路由
import React from 'react'
import ReactDOM from 'react-dom/client'
import Root from './routes/root'
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom'
import ErrorPage from './error/error-page'
const router = createBrowserRouter([
{
path: "/",
element: <Root></Root>,
errorElement: <ErrorPage></ErrorPage>,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
)
当你访问不存的地址的时候就跳转到错误页:
3、添加其他组件路由
src/routes/contact.jsx
import { Form } from "react-router-dom";
export default function Contact() {
const contact = {
first: "Your",
last: "Name",
avatar: "https://robohash.org/you.png?size=200x200",
twitter: "your_handle",
notes: "Some notes",
favorite: true,
};
return (
<div id="contact">
<div>
<img
key={contact.avatar}
src={
contact.avatar ||
`https://robohash.org/${contact.id}.png?size=200x200`
}
/>
</div>
<div>
<h1>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
<Favorite contact={contact} />
</h1>
{contact.twitter && (
<p>
<a
target="_blank"
href={`https://twitter.com/${contact.twitter}`}
>
{contact.twitter}
</a>
</p>
)}
{contact.notes && <p>{contact.notes}</p>}
<div>
<Form action="edit">
<button type="submit">Edit</button>
</Form>
<Form
method="post"
action="destroy"
onSubmit={(event) => {
if (
!confirm(
"Please confirm you want to delete this record."
)
) {
event.preventDefault();
}
}}
>
<button type="submit">Delete</button>
</Form>
</div>
</div>
</div>
);
}
function Favorite({ contact }) {
const favorite = contact.favorite;
return (
<Form method="post">
<button
name="favorite"
value={favorite ? "false" : "true"}
aria-label={
favorite
? "Remove from favorites"
: "Add to favorites"
}
>
{favorite ? "★" : "☆"}
</button>
</Form>
);
}
导入联系人组件并创建新路线
import React from 'react'
import ReactDOM from 'react-dom/client'
// import Root from './routes/root'
import App from './App'
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom'
import ErrorPage from './error/error-page'
import Contact from './routes/contact'
const router = createBrowserRouter([
{
path: "/",
element: <App></App>,
errorElement: <ErrorPage></ErrorPage>,
},
{
path: "/contact/:contactId",
element: <Contact></Contact>,
},
]);
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>,
)
现在访问:http://localhost:3000/contact/1,就是跳转的新页面啦!!!
虽然可以跳转,但是,它不在我们的根布局中😠,继续往下走!!!
4、嵌套路由
我们希望联系人组件像这样在布局内部呈现。<Root>
我们通过将联系路由设为根路由的子路由来实现这一点。
👉将联系人路由移至根路由的子路由-children
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
此时操作点击Name的时候如下:
点击前:
点击后:
很明显,虽然url地址变了,但是页面结构只是增多了,原来的ui结构仍然存在,我们将这样的操作称为子组件跳转,可以理解为局部跳转展示!!!!
5、客户端路由(a改成Link的使用)
您可能已经注意到了,也可能没有,但是当我们单击侧边栏中的链接时,浏览器正在对下一个 URL 执行完整文档请求,而不是使用 React Router。
客户端路由允许我们的应用更新 URL,而无需从服务器请求另一个文档。相反,应用可以立即呈现新的 UI。让我们用 来实现它<Link>。
👉将侧边栏更改<a href>
为<Link to>
import { Outlet,Link } from "react-router-dom";
export default function Root() {
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
<div>
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div
id="search-spinner"
aria-hidden
hidden={true}
/>
<div
className="sr-only"
aria-live="polite"
></div>
</form>
<form method="post">
<button type="submit">New</button>
</form>
</div>
<nav>
<ul>
<li>
<Link to={`contacts/1`}>Your Name</Link>
</li>
<li>
<Link to={`contacts/2`}>Your Friend</Link>
</li>
</ul>
</nav>
</div>
<div id="detail">
<Outlet />
</div>
</>
);
}
您可以在浏览器开发者工具中打开网络选项卡,看到它不再请求文档。(其实是因为a链接的问他,详细的问题可以去了解a链接为什么会请求刷新)
6、加载数据中(loader)
URL 段、布局和数据通常结合在一起(三重?)。我们已经可以在这个应用中看到它了:
URL 段 | 成分 | 数据 |
---|---|---|
/ | <Root> | 聯絡人清單 |
联系人/:id | <Contact> | 个人联系方式 |
由于这种自然的耦合,React Router 具有数据约定,可以轻松地将数据放入路由组件中。
我们将使用两个 API 来加载数据,loader和useLoaderData。首先,我们将在根模块中创建并导出加载器函数,然后将其连接到路由。最后,我们将访问和呈现数据。
loader
函数是一个异步函数,用于在组件渲染前获取数据。它通常被用来预加载数据,比如从服务器上获取数据,然后将这些数据传递给组件,使得组件可以在首次渲染时就拥有所有必要的数据。
👉从以下位置导出加载器root.jsx
import { Outlet, Link } from "react-router-dom";
import { getContacts } from "./contact";
export async function loader() {
const contacts = await getContacts();
return { contacts };
}
👉在路由上配置 loader
/* other imports */
import Root, { loader as rootLoader } from "./routes/root";
const router = createBrowserRouter([
{
path: "/",
element: <APP />,
errorElement: <ErrorPage />,
loader: rootLoader,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
👉访问并呈现数据 root.tsx
import {
Outlet,
Link,
useLoaderData,
} from "react-router-dom";
import { getContacts } from "../contacts";
/* other code */
export default function Root() {
const { contacts } = useLoaderData();
return (
<>
<div id="sidebar">
<h1>React Router Contacts</h1>
{/* other code */}
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<Link to={`contacts/${contact.id}`}>
{contact.first || contact.last ? (
<>
{contact.first} {contact.last}
</>
) : (
<i>No Name</i>
)}{" "}
{contact.favorite && <span>★</span>}
</Link>
</li>
))}
</ul>
) : (
<p>
<i>No contacts</i>
</p>
)}
</nav>
{/* other code */}
</div>
</>
);
}
就是这样!React Router 现在会自动将数据与您的 UI 保持同步。我们目前还没有任何数据,因此您可能会得到一个空白列表,如下所示:
7、更新数据(action)
我们刚刚创建的编辑路由已经呈现了一个表单。要更新记录,我们需要做的就是将操作连接到路由。表单将发布到操作,数据将自动重新验证。
src/routes/edit.tsx
👉向编辑模块添加操作
import {
Form,
useLoaderData,
redirect,
} from "react-router-dom";
import { updateContact } from "../contacts";
export async function action({ request, params }) {
const formData = await request.formData();
const updates = Object.fromEntries(formData);
await updateContact(params.contactId, updates);
return redirect(`/contacts/${params.contactId}`);
}
👉将操作连接到路线 main.ts
/* existing code */
import EditContact, {
action as editAction,
} from "./routes/edit";
const router = createBrowserRouter([
{
path: "/",
element: <Root />,
errorElement: <ErrorPage />,
loader: rootLoader,
action: rootAction,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
loader: contactLoader,
},
{
path: "contacts/:contactId/edit",
element: <EditContact />,
loader: contactLoader,
action: editAction,
},
],
},
]);
/* existing code */
填写表格,点击保存,然后你就会看到类似这样的内容!(除了看起来更舒服,而且可能没有那么毛茸茸的。)
注意:其实就是action里面的回调方法,它会在表单提交时被调用,而不是在页面加载时。
action
函数接收一个ActionFunctionArgs
类型的参数,这个参数包含了请求方法、请求体、URL 参数等信息。action
函数应该返回一个 Promise,这个 Promise 会解析成一个对象,这个对象可以包含状态码和数据,它们将被用来构建响应。特别注意:
action
和loader
是独立的概念,它们可以同时存在或只存在其中一个。action
主要用于处理表单提交,而loader
用于在页面加载时预取数据。
8、动态路由
其实之前的案例已经使用过了,这里再详细讲一下,所谓的动态路由就是一个模块展示不同的内容,比如根据id展示不同的信息等操作:
路由配置:
chotacts就是动态路由:通过<Link to={`contacts/1`}>Your Name</Link>方式就可以访问了
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{
path: "contacts/:contactId",
element: <Contact />,
},
],
},
]);
注意:contactId
URL 段。冒号 ( :
) 具有特殊含义,将其转换为“动态段”。动态段将匹配 URL 中该位置的动态(变化)值,例如联系人 ID。我们将 URL 中的这些值称为“URL 参数”,或简称为“params”。
9、路由重定向
现在我们知道了如何重定向,让我们更新创建新联系人的操作以重定向到编辑页面:
👉重定向到新记录的编辑页面 root.tsx
import {
Outlet,
Link,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
import { getContacts, createContact } from "../contacts";
export async function action() {
const contact = await createContact();
return redirect(`/contacts/${contact.id}/edit`);
}
现在,当我们点击“新建”时,我们应该进入编辑页面:
👉添加一些记录
我将使用第一届 Remix 大会的杰出演讲者阵容 😁
10、活动链接样式
现在我们有了一堆记录,但我们不清楚侧边栏中显示的是哪一条。我们可以用它NavLink来修复这个问题。
👉使用NavLink
侧边栏中的 root.tsx
import {
Outlet,
NavLink,
useLoaderData,
Form,
redirect,
} from "react-router-dom";
export default function Root() {
return (
<>
<div id="sidebar">
{/* other code */}
<nav>
{contacts.length ? (
<ul>
{contacts.map((contact) => (
<li key={contact.id}>
<NavLink
to={`contacts/${contact.id}`}
className={({ isActive, isPending }) =>
isActive
? "active"
: isPending
? "pending"
: ""
}
>
{/* other code */}
</NavLink>
</li>
))}
</ul>
) : (
<p>{/* other code */}</p>
)}
</nav>
</div>
</>
);
}
请注意,我们正在将一个函数传递给className
。当用户位于 中的 URL 时NavLink
,isActive
则为真。当它即将激活时(数据仍在加载),则为isPending
真。这使我们能够轻松指示用户所在的位置,以及对已点击但仍在等待数据加载的链接提供即时反馈。
NavLink
是react-router-dom
库中的一个组件,用于创建具有激活高亮效果的导航链接。当用户浏览到与NavLink
配置的to
属性相匹配的路由时,NavLink
可以自动应用特定的样式或类名,以指示当前的导航位置。
NavLink
提供了以下主要属性:
to
: 指定链接的目标地址。className
: 默认的 CSS 类名,始终应用在链接上。activeClassName
: 当链接与当前路由匹配时应用的 CSS 类名。activeStyle
: 直接应用在链接上的内联样式,当链接与当前路由匹配时生效。exact
: 如果设置为true
,则只有当完整路径与to
属性完全匹配时,链接才会被视为“激活”状态。否则,任何包含to
属性路径的部分匹配也会激活链接。end
: 如果设置为true
,并且to
属性是一个索引路由(即没有子路由的父路由),那么即使to
不完全匹配,链接也会被视为“激活”。这与exact
属性相反。下面是一个简单的
NavLink
使用示例:import { NavLink } from 'react-router-dom'; function Navigation() { return ( <nav> <ul> <li> <NavLink to="/" exact activeClassName="selected"> Home </NavLink> </li> <li> <NavLink to="/about" activeClassName="selected"> About </NavLink> </li> <li> <NavLink to="/contact" activeClassName="selected"> Contact </NavLink> </li> </ul> </nav> ); }
在这个例子中,当用户位于首页 (
/
) 时,“Home”链接将获得selected
类名,从而应用相应的样式。同样地,当用户在 “About” 或 “Contact” 页面时,相应的链接也将获得高亮显示。
NavLink
是创建响应式和用户友好的导航菜单的理想选择,因为它可以自动处理链接的激活状态,无需手动编写逻辑来检查当前的路由。
11、全局待处理 UI
当用户浏览应用程序时,React Router 会保留旧页面,因为正在加载下一页的数据。您可能已经注意到,当您在列表之间单击时,应用程序感觉有点无响应。让我们为用户提供一些反馈,以便应用程序不会感觉无响应。
React Router 在后台管理所有状态,并揭示构建动态 Web 应用所需的部分内容。在本例中,我们将使用钩子useNavigation。
👉useNavigation
添加全局待处理 UI-routes/roots.tsx
import {
// existing code
useNavigation,
} from "react-router-dom";
// existing code
export default function Root() {
const { contacts } = useLoaderData();
const navigation = useNavigation();
return (
<>
<div id="sidebar">{/* existing code */}</div>
<div
id="detail"
className={
navigation.state === "loading" ? "loading" : ""
}
>
<Outlet />
</div>
</>
);
}
useNavigation返回当前导航状态: 可以是以下之一"idle" | "submitting" | "loading"
。
在我们的例子中,"loading"
如果我们不处于空闲状态,我们会向应用程序的主要部分添加一个类。然后,CSS 会在短暂延迟后添加一个漂亮的淡入淡出效果(以避免快速加载时 UI 闪烁)。不过,您可以做任何您想做的事情,比如在顶部显示一个旋转器或加载栏。
请注意,我们的数据模型 ( src/contacts.js
) 具有客户端缓存,因此第二次导航到同一联系人的速度很快。此行为不是React Router,它会重新加载更改路线的数据,无论您之前是否去过那里。但是,它确实避免在导航期间调用不变路线(如列表)的加载器。
12、取消按钮
在编辑页面上,我们有一个取消按钮,但它目前还没有任何作用。我们希望它能发挥与浏览器后退按钮相同的作用。
我们需要按钮上的点击处理程序以及useNavigateReact Router。
👉使用以下代码添加取消按钮点击处理程序useNavigate
import {
Form,
useLoaderData,
redirect,
useNavigate,
} from "react-router-dom";
export default function EditContact() {
const { contact } = useLoaderData();
const navigate = useNavigate();
return (
<Form method="post" id="contact-form">
{/* existing code */}
<p>
<button type="submit">Save</button>
<button
type="button"
onClick={() => {
navigate(-1);
}}
>
Cancel
</button>
</p>
</Form>
);
}
现在,当用户点击“取消”时,他们将被送回浏览器历史记录中的一个条目。
event.preventDefault
🧐 为什么按钮上没有?
尽管看似多余,但这<button type="button">
是阻止按钮提交其表单的 HTML 方式。
还有两个功能要实现。我们已经进入最后冲刺阶段!
13、URL 搜索参数和 GET 提交
到目前为止,我们所有的交互式 UI 要么是更改 URL 的链接,要么是将数据发布到操作的表单。搜索字段很有趣,因为它是两者的混合:它是一个表单,但它只更改 URL,而不会更改数据。
现在它只是一个普通的 HTML <form>
,而不是 React Router <Form>
。让我们看看浏览器默认对它做了什么:
👉在搜索栏中输入姓名,然后按回车键
请注意,浏览器的 URL 现在以URLSearchParams的形式包含您的查询:
http://127.0.0.1:5173/?q=ryan
如果我们查看搜索表单,它看起来像这样:
<form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</form>
正如我们之前所见,浏览器可以通过name
其输入元素的属性序列化表单。此输入的名称是q
,这就是 URL 为的原因?q=
。如果我们将其命名为,则search
URL 将是?search=
。
请注意,此表单与我们使用过的其他表单不同,它没有<form method="post">
。默认值为method
。"get"
这意味着当浏览器创建下一个文档的请求时,它不会将表单数据放入请求 POST 主体中,而是放入URLSearchParamsGET 请求的。
14、使用客户端路由进行 GET 提交
让我们使用客户端路由来提交此表单并过滤我们现有加载器中的列表。
👉更改<form>
为<Form>
<Form id="search-form" role="search">
<input
id="q"
aria-label="Search contacts"
placeholder="Search"
type="search"
name="q"
/>
<div id="search-spinner" aria-hidden hidden={true} />
<div className="sr-only" aria-live="polite"></div>
</Form>
👉如果有 URLSearchParams,则过滤列表
export async function loader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get("q");
const contacts = await getContacts(q);
return { contacts };
}
因为这是 GET 而不是 POST,所以 React Router不会调用action
。提交 GET 表单与单击链接相同:只有 URL 会发生变化。这就是为什么我们为过滤添加的代码位于 中loader
,而不是action
此路由的 中。
这也意味着这是正常的页面导航。您可以点击后退按钮返回到原来的位置。