React路由.md 11 KB

React路由

React框架配套的路由系统插件 是 React Router。目前版本为V6。

1. 基本使用

  1. 创建路由实例

createBroswerRouter或者createHashRouter

  1. 引入RouterProvider组件

  2. 定义相关路由组件

    // 定义路由的模块 router/index.js
    import { createBrowserRouter } from 'react-router-dom';
    import Home from '../pages/Home';
    import About from '../pages/About';
    
    const routes = [
    {
    path: '/',
    element: <Home />,
    },
    {
    path: '/about',
    element: <About />,
    },
    ];
    
    const router = createBrowserRouter(routes);
    
    export default router;
    
    
    // 入口文件 
    import { RouterProvider } from 'react-router-dom';
    import router from './router';
    
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(
    <React.StrictMode>
    <RouterProvider router={router} />
    </React.StrictMode>
    );
    

2. 嵌套路由

实现分2步走:

  1. 在指定父路由对象上添加children属性,值为路由对象数组;
  2. 在父路由组件上通过Outlet组件来指定子路由组件的渲染位置

    // 定义嵌套子路由
    const routes = [
    {
    path: '/',
    element: <Layout />,
    children: [
      // 这是索引路由
      {
        // path: '',
        index: true,
        element: <Home />,
      },
      {
        path: 'about',
        element: <About />,
      },
    ],
    },
    ];
    
    // 父组件 -- 布局组件
    import Header from '../components/Header';
    import { Outlet } from 'react-router-dom';
    
    function Layout() {
    return (
    <div className="layout">
      <header>
        <Header />
      </header>
      <main style={{ padding: '20px' }}>
        {/* 渲染子路由组件 */}
        <Outlet />
      </main>
    </div>
    );
    }
    
    export default Layout;
    

3. 动态路由

当实现一个类似详情页的需求时,我们可能会需要使用到动态路由

例如,在商品列表页中点击某一商品获取详情。

  1. 假设 商品列表 对应的 路由是 /products
  2. 需要单独页面显示某一商品的详情,此时 路由为这么定义:/products/details/{product-id} 因此这就是动态路由。
  3. 定义路由时,使用":"去定义路由中的参数,即{path: '/products/details/:productId'}
  4. 访问时,会自动匹配。成功后,会将路由参数与值解析出来,存储在params对象中
  5. 通过路由Hook useParams 来获取上面解析后的路由参数对象

    // 列表页代码
    function Products() {
    let [products] = useState(initialProducts);
    return (
    <div className="products">
      <table>
        <thead>
          <tr>
            <th>#</th>
            <th>Name</th>
            <th>Price</th>
            <th>Action</th>
          </tr>
        </thead>
        <tbody>
          {products.map((p, i) => (
            <tr key={p.id}>
              <td>{i + 1}</td>
              <td>{p.name}</td>
              <td>{p.price}</td>
              <td>
                <Link to={{ pathname: `/products/details/${p.id}` }}>详情</Link>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
    );
    }
    
    export default Products;
    // 详情页代码
    import { useParams } from 'react-router-dom';
    
    function ProductDetail() {
    const { productId } = useParams();
    // console.log(params);
    return (
    <div className="detail">
      <h3>详情页</h3>
      <p>您正在浏览的商品ID:{productId}</p>
    </div>
    );
    }
    
    export default ProductDetail;
    

4. 查询参数

在实际开发中,可以通过路由参数实现页面间数据传递;当然也可以通过查询参数。

下面代码演示,如果通过查询参数实现页面间数据传递:

<tbody>
  {products.map((p, i) => (
    <tr key={p.id}>
      <td>{i + 1}</td>
      <td>{p.name}</td>
      <td>{p.price}</td>
      <td>
        {/* 1 路由参数 在页面间传递数据 */}
        {/* <Link to={{ pathname: `/products/details/${p.id}` }}>详情</Link> */}
        {/* 2 查询参数 在页面间传递数据 */}
        {/* <Link to={`/products/detail?pid=${p.id}`}>详情</Link> */}
        <Link to={{ pathname: '/products/detail', search: '?id=1' }}>
          详情
        </Link>
      </td>
    </tr>
  ))}
</tbody>
let [searchParams] = useSearchParams();
<div className="detail">
  {/* <NavLink to="..">Back</NavLink> */}
  <a
    href="#"
    onClick={() => {
      // go('/products');
      // go({ pathname: '/products' });
      go(-1);
    }}
  >
    Back
  </a>
  <h3>详情页</h3>
  <p>
    您正在浏览的商品ID:{searchParams.get('pid')}
  </p>
</div>

5. 页面导航

5.1 声明式导航

  1. Link组件
  2. NavLink组件

NavLink在使用时,更容易实现选中效果。

const handleLinkStyle = ({ isActive, isPending }) =>
  isPending ? 'pending' : isActive ? 'actived' : 'link';

function Header() {
  return (
    <div className="header">
      <ul>
        <li>
          <NavLink className={handleLinkStyle} to="/">
            主页
          </NavLink>
        </li>
        <li>
          <NavLink className={handleLinkStyle} to={{ pathname: '/about' }}>
            关于
          </NavLink>
        </li>
        <li>
          <NavLink className={handleLinkStyle} to={{ pathname: '/products' }}>
            商品
          </NavLink>
        </li>
      </ul>
    </div>
  );
}

5.2 编程式导航

通过 useNavigate()Hook 获取到 navigate函数,接着通过调用navigate函数实现页面跳转。

const navigate = useNavigate();

// navigate(to: Path, option?)
// navigate(delta: number)
navigate(-1)
navigate(1)
navigate('/login')
navigate({pathname: '/login'})
navigate({pathname: '/login', search: '?id=1'})
navigate({pathname: '/login'}, {replace: true, state: {}})

6. 数据获取

在路由系统中,如何选择时机去获取数据?

我们知道在VueRouter中,已经提供了两种方式,都是用户能够接收并且体验良好的方案

  1. 在组件渲染后,添加Loading效果后去获取数据
  2. 在路由开启导航时先去获取数据,这样组件渲染时就会连同数据一起渲染出来了

6.1 Loading效果

  1. 在路由组件中定义对应数据的状态对象
  2. 在组件渲染后,获取数据并修改状态
  3. 组件模板中通过条件渲染实现当数据没有回来时渲染Loading组件,而数据返回来后在做列表渲染即可。

6.2 Data Loading

  1. 在定义路由的时候,给路由对象添加loader数据加载器(值为函数),它会在在导航过程中启动数据加载。
  2. 在路由组件中,通过useLoaderData()Hook函数来获取上面loader返回的数据

7. 页面访问权限

Vue Router中可以通过导航守卫钩子来拦截到所有的页面导航,根据当前用户权限决定是否继续导航。

但是React Router需要根据不同的路由定义方式来自行实现。

7.1 自定义hook

/**
 *! 自定义hook
 ** 1 hook 函数的名称 必须 以 'use'开头去命名
 ** 2 自定义hook中可以任意使用内置的所有hook函数
 */

import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
const whiteList = ['/', '/about', '/login'];

export function useAuth() {
  let location = useLocation();
  let navigate = useNavigate();
  let token = localStorage.getItem('token');

  useEffect(() => {
    if (!token && !whiteList.includes(location.pathname)) {
      navigate(`/login?from=${location.pathname}`);
    }
  }, [location]);
}

7.2 Router Provider实现

import { useNavigate } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

export default function Protected() {
  const navigate = useNavigate();
  // 页面访问鉴权
  useAuth();

  return (
    <div className="Protected">
      <h3>受保护页面——需要登录后才可以访问</h3>
      <div>
        <button
          onClick={() => {
            if (window.confirm('确定退出系统吗')) {
              localStorage.clear();
              navigate('/login');
            }
          }}
        >
          退出登录
        </button>
      </div>
    </div>
  );
}

8. 其他

8.1 路由懒加载

为了提高React项目首次加载速度,推荐使用路由懒加载。

实现时,需要从React核心库中引入lazy函数和Suspense组件。像这样,

import { createBrowserRouter } from 'react-router-dom';
import Home from '../pages/Home';
import { lazy, Suspense } from 'react';

const About = lazy(() => import('../pages/About'));

const router = createBrowserRouter([
  { path: '/', Component: Home },
  {
    path: '/about',
    element: (
      <Suspense fallback={<h3>加载中...</h3>}>
        <About />
      </Suspense>
    ),
  },
]);

export default router;

在定义路由时,React Router还提供了一系列组件:

import './App.css';
import { NavLink, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';
import Home from './pages/Home';

// import About from './pages/About';
// import User from './pages/User';
import UserProfile from './pages/UserProfle';
import UserPost from './pages/UserPost';

const About = lazy(async () => {
  await new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 1000);
  });
  return import('./pages/About.jsx');
});

const User = lazy(() => import('./pages/User'));

// 实现路由懒加载的高阶组件
function withSuspense(Component) {
  return (
    <Suspense fallback={<h3>组件疯狂加载中...</h3>}>
      <Component />
    </Suspense>
  );
}

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <NavLink className="App-link" to={'/'}>
          主页
        </NavLink>{' '}
        <NavLink className="App-link" to={'/about'}>
          关于
        </NavLink>
        <NavLink className="App-link" to={'/user'}>
          用户
        </NavLink>
      </header>
      <main>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={withSuspense(About)} />
          <Route path="/user" element={withSuspense(User)}>
            <Route index />
            <Route path="profile" element={<UserProfile />} />
            <Route
              path="post"
              // 并不会起作用。它只能在RouterProvider API中使用
              lazy={() => import('./pages/UserPost.jsx')}
              element={<UserPost />}
            />
          </Route>
          <Route path="/list/:lid" />
        </Routes>
      </main>
    </div>
  );
}

export default App;

其中,Route组件就是定义路由的。当其path属性和当前地址匹配成功后,就会在其位置下渲染element绑定的React元素。

同时可以定义多个Route组件,这样就可以在页面中定义多个路由。并且这些Route组件必须作为Routes组件的直接子代元素。

为了将路由功能注入到项目中,需要在App组件中将整个项目代码包裹在一个BrowserRouter或者HashRouter组件中。

  • BrowserRouter 会实现一个基于History 模式的路由
  • HashRouter 会实现一个基于hash 模式的路由