React中级教程(六)– 使用React Route进行路由

本文介绍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项目。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注