本文介绍React路由,使用流行库React Router,内容包括设置路由、处理错误、页面导航、动态路由、嵌套路由以及私有路由等。
设置路由
安装:
npm i react-router-dom@6.10.0
在src/routing下新建 routes.tsx文件。
调用React Route的 createBrowserRouter函数创建router,参数是个列表,每个列表项表示路径与组件的对应关系。
// routes.tsx
import { createBrowserRouter } from "react-router-dom";
import HomePage from "./HomePage";
import UserListPage from "./UserListPage";
const router = createBrowserRouter([
{ path: "/", element: <HomePage /> },
{ path: "/users", element: <UserListPage /> },
]);
export default router;
接着到main.tsx中,将<App>组件替换成<RouterProvider>组件,并提供router属性为刚创建的router。
// main.tsx
import "bootstrap/dist/css/bootstrap.css";
import React from "react";
import ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import "./index.css";
import router from "./routing/routes";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
我们可以像之前一样,指定显示某个组件比如App,也可以由RouterProvider根据实际的路由打开相应的组件。
打开浏览器查看效果。首页HomePage显示简单的随机文字及Users链接,点击Users可以导航到UserListPage组件。
页面导航
我们从HomePage转到UserListPage时,打开谷歌浏览器开发工具,能看到它是整个页面重载。
实际是没必要重载全面页面元素的,我们只需将组件需要替换即可,这就用到了React的一个组件 Link。
修改HomePage.tsx,将 <a href…>改成<Link to=…>。
// HomePage.tsx
import { Link } from "react-router-dom";
const HomePage = () => {
return (
......
<Link to={"/users"}>Users</Link>
......
);
};
export default HomePage;
再测试转到 /users ,就不会整体加载网页了。
很多时候,我们需要用程序跳转页面。就需要用 useNavigate了。
先在routes.tsx添加一条路由。
// routes.tsx
......
const router = createBrowserRouter([
......
{ path: "/contact", element: <ContactPage /> },
]);
export default router;
修改 ContactPage.tsx。
调用React Router的useNavigate得到 navigate实例,然后在 onSubmit中使用 navigate(“/”)回到HomePage.
// ContactPage.tsx
import { useNavigate } from "react-router-dom";
const ContactPage = () => {
const navigate = useNavigate();
return (
<form
onSubmit={(event) => {
event.preventDefault();
navigate("/");
}}
>
<button className="btn btn-primary">Submit</button>
</form>
);
};
export default ContactPage;
使用路由参数传递数据
比如要查看具体的用户的信息,路径可能是 /users/1 ,其中 1就是传递的参数,表示用户ID。我们来看看怎么传递。
在routes.tsx中添加一条路由。:id 加冒号的id表示这个为参数。
// routes.tsx
......
const router = createBrowserRouter([
......
{ path: "/users/:id", element: <UserDetailPage /> },
]);
export default router;
再修改UserListPage.tsx,每一个用户的链接改用Link组件:
<Link to = {`/users/${user.id}`}>…
这样在/users页面点面具体用户链接时,会链到 /users/1 、/users/2 …..。
// UserListPage.tsx
......
{users.map((user) => (
<li className="list-group-item" key={user.id}>
<Link to={`/users/${user.id}`}>{user.name}</Link>
</li>
))}
</ul>
);
};
export default UserListPage;
获取当前路由的数据
主要有3个hook来获取:
useParams, useSearchParams, useLocation。
具体信息我们可以让它输出来查看。
修改UserDetailPage.tsx,使用相应的Hook,并用console.log输出相应信息。
// UserDetailPage.tsx
import { useLocation, useParams, useSearchParams } from "react-router-dom";
const UserDetailPage = () => {
const params = useParams();
console.log(params);
const [searchParams, setSearchParams] = useSearchParams();
console.log(searchParams.toString());
console.log(searchParams.get("name"));
const location = useLocation();
console.log(location);
return <p>User</p>;
};
export default UserDetailPage;
之后,我们给出url:
http://localhost:5174/users/1?name=kelemi&age=18
输出的结果也是一目了然的,就可知道这3个Hook是获取当前路径的哪些信息了。
㠌套路由
现实的项目经常是这样,上方有一导航条,导航条上有各个链接,点击可以在主区域展示不同的内容。
查看我们的源码,有一个组件 src/routing/layout.tsx。我们添加 Outlet组件。该组件相当于一个占位符,可以动态显示子组件。
import { Outlet } from "react-router-dom";
import NavBar from "./NavBar";
const Layout = () => {
return (
<>
<NavBar />
<div id="main">
<Outlet />
</div>
</>
);
};
export default Layout;
查看下NavBar,没什么特别的,就是些导航链接。
// NavBar.tsx
const NavBar = () => {
return (
<nav
className="navbar navbar-expand-lg"
style={{ background: '#f0f0f0', marginBottom: '1rem' }}
>
<div className="container-fluid">
<a className="navbar-brand" href="#">
My App
</a>
<div className="collapse navbar-collapse" id="navbarNav">
<ul className="navbar-nav">
<li className="nav-item">
<a className="nav-link active" href="#">
Home
</a>
</li>
<li className="nav-item">
<a className="nav-link" href="#">
Users
</a>
</li>
</ul>
</div>
</div>
</nav>
);
};
export default NavBar;
修改下routes.tsx。
添加 根组件为 Layout,再设置属性 children,并将之前的route放进children,注意children里的path去掉之前的 斜杠 / ,比如 “/”改成 空白””, “/users”改成”users“。
另外 {path:””, element: <HomePage />} 由于是空白 path,也可以改成用index表示:
{index: true, element: <HomePage />}
这两种方法没有区别,都是可以的。
import { createBrowserRouter } from "react-router-dom";
import HomePage from "./HomePage";
import UserListPage from "./UserListPage";
import UserDetailPage from "./UserDetailPage";
import Layout from "./Layout";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ path: "", element: <HomePage /> },
{ path: "users", element: <UserListPage /> },
{ path: "users/:id", element: <UserDetailPage /> },
],
},
]);
export default router;
到页面上查看效果。我们看到,在children里的path 是 显示在占位符 <Outlet>组件里的。
接着将NavBar.tsx的链接改成用Link组件表示。Link组件前面有介绍,不细列了。
练习:使用㠌套路由
我们做个练习,将UserListPage和UserDetailPage合成一个UserPage,左侧显示 用户列表,右侧显示详细信息。
先将UserListPage改名为UserList, UserDetailPage改名为UserDetail,因为他们目前只是页面的一部分,不再是单独页面,改名后更加清晰。
创建UsersPage.tsx. 左侧是UserList,右侧展示一个 Outlet,动态展示内容。
// UsersPage.tsx
import UserList from "./UserList";
import { Outlet } from "react-router-dom";
const UserPage = () => {
return (
<div className="row">
<div className="col">
<UserList />
</div>
<div className="col">
<Outlet />
</div>
</div>
);
};
export default UserPage;
修改routes.tsx. “users” 的路由指向UserPage, 并且使用 children属性,指向UserDetail,注意这个path只用 “:id” , 父级的 “users” 不能再列上去。
// routes.tsx
import { createBrowserRouter } from "react-router-dom";
import HomePage from "./HomePage";
import Layout from "./Layout";
import UserDetail from "./UserDetail";
import UsersPage from "./UsersPage";
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
children: [
{ path: "", element: <HomePage /> },
{
path: "users",
element: <UsersPage />,
children: [{ path: ":id", element: <UserDetail /> }],
},
],
},
]);
export default router;
为了更清晰,修改UserDetail加上id。
// UserDetail.tsx
import { useLocation, useParams, useSearchParams } from "react-router-dom";
const UserDetail = () => {
const params = useParams();
return <p>User {params.id}</p>;
};
export default UserDetail;
页图是这样的。
设置活动链接的样式
目前在NavBar选择Home 和 Users链接时,链接的样式并不会改变,我们无法从导航条上获知哪个链接被选中。
其实只需将Link改成 NavLink就可达到这一目的。
// NavBar.tsx
......
<li className="nav-item">
<NavLink className="nav-link" to={"/users"}>
Users
</NavLink>
</li>
......
使用NavLink,会有默认的样式会标识选中的链接与未选中的链接的差别,如果想自定议样式也是可以的, 只需提供给className一个函数,并返回样式即可。如下面代码,我们将Props解构出 isActive,通过判断isActive分别赋于不同的样式即可。
// NavBar.tsx
......
<NavLink
className={({ isActive }) =>
isActive ? "nav-link active" : "nav-link"
}
to={"/"}
>
Home
</NavLink>
......
处理错误
当尝试访问一个不存在的网站时,我们会得到一个通用的错误提示。比如我们访问 /userex ,就会下面的响应。
可以自定义错误页面。在src/routing里一个ErrorPage.tsx,我们只需在routes.tsx中的根路径 “/” 加入errorElement属性指向ErrorPage
// routes.tsx
......
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
errorElement: <ErrorPage />,
children: [
.....
再访问不存在的网页时,就由ErroraPage的内容代替通用的错误提示了。
这个也带来另一个好处,当然在网页真出现异常了,也会向用户显示这个ErrorPage,而不是异常中断的不友好提示。
我们在HomePage.tsx中,抛出一个异常,访问它,也会转到 ErrorPage.
// HomePage.tsx
import { Link } from "react-router-dom";
const HomePage = () => {
throw Error("something failured.");
return (
......
实际场景可能要捕捉错误并记录日志,这就可以用到 useRouteError, 我们可以用console.log查看一下信息。
另外,我们需要辨别到底是无效的页面路径还是实际网页的错误,可以使用 isRouteErrorResponse,判断该错误,如果是 true,表示是无效页面,否则就是网页的不知预知错误。
//
import { isRouteErrorResponse, useRouteError } from "react-router-dom";
const ErrorPage = () => {
const error = useRouteError();
console.log(error);
return (
<>
<h1>Oops...</h1>
<p>{isRouteErrorResponse(error) ? "Invalid page" : "Unexpected error"}</p>
</>
);
};
export default ErrorPage;
错误的页面路径及页面实际的异常就能分辨了。
私有路由
有时候是需要限制某些路径的访问的,比如可能只允许授权用户或已登录用户才能访问用户信息页面,这个就叫做私有路由。
查看并修改 src/routing/hooks/userAuth.ts。返回一个空用户,我们用这个来模拟未登录。
// useAuth.ts
const useAuth = () => ({ user: null });
export default useAuth;
接着修改 UsersPage.tsx,通过useAuth获取user对象,如果未登录,导航到 “/login”,注意这里跳转一般不能用 useNavigate,否则组件就不纯了,一般只能在submit或 useEffect中才使用。所以返回组件 <Navigate>,Navigate实际上也是包装了useNavigate。
// UsersPage.tsx
......
const UsersPage = () => {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
return (
......
再修改 routes.tsx,添加 “/login”路径。
// routes.tsx
......
const router = createBrowserRouter([
{
path: "/",
......
children: [
.......
{ path: "login", element: <LoginPage /> },
],
},
]);
export default router;
加到页面,我们访问 “/users”时,就跳到 ”/login”了,因为未登陆不允许访问。
布局 Routes
前面是在UsersPage里判断用户是否登录,这种方法需要在每个需要控制访问权限的页面上都作同样的工作,这是非常重复的工作,需要找到一种能在一个地方设置控制逻辑而在其他页面生效的方法。
新建src/routing/hooks/PrivateRoutes.tsx. 将之前在UsersPage.tsx中的判断用户的逻辑移到这里,用户未录登转到 “/login”,否则返回<Outlet>.
import { Navigate, Outlet } from "react-router-dom";
import useAuth from "./useAuth";
const PrivateRoutes = () => {
const { user } = useAuth();
if (!user) return <Navigate to="/login" />;
return <Outlet />;
};
export default PrivateRoutes;
修改routes.tsx,在与根路径 “/” 并级的地方添加 对象,注意该对象只用于规则判断,不加 path。element 使用 刚建的 PrivateRoutes, children就是要保护的相关路径,这时就是 “/users”。其它要保护的页面也加进去即可。
// routes.tsx
.....
const router = createBrowserRouter([
{
path: "/",
......
},
{
element: <PrivateRoutes />,
children: [
{
path: "users",
element: <UsersPage />,
children: [{ path: ":id", element: <UserDetail /> }],
},
],
},
]);
export default router;
小结
本文介绍了React路由的知识。
下一篇将这些知识运用到game-hub项目。