前言
看了一圈 react router 文章,发现写 v6 的文章好像不多,也大多是 v6.4 之前的,比较碎片化。然后看了文档,感觉官方文档写的也不是很简单清晰。每次大版本迭代 API 变动还是蛮大的,学习成本不小。
所以基于自己的学习和理解,梳理一下 react router v6 快速上手教程,没有去讲解 Data API 和偏底层的 API。当然详细的还是要看 官方文档 和 blog 以及 examples 去学习。
版本历史
v3 到 v4/v5 到 v6 之间的 API 变化都是很大的,先了解一下 react router 的进化史:
6.x
目前 react-router 最新的版本为 v6.16.0(2023年9月),下个几个重要版本的发布节点,最重要的是 v6.4.0:
- 2021年11月发布的 v6.0.0 版本,基于 hooks 重构,体积更小。
 - 2022 年9月发布的 v6.4.0 版本,引入 
createBrowserRouter/createHashRouter、<RouterProvider>等Data API - 2023年 3月发布的 v6.9.0版本,引入 
Component、lazy等 
5.x/4.x
- roadmap
 - 2017年3月发布的 v4.0.0 版本
 - 2019年3月 发布的 v5.0.0 版本,其实是4.4。由于4.3及以下版本的
react-router-dom的依赖"react-router": "^4.3.1",由于^,安装时会依赖安装最新4.4依赖,但其API并不兼容,所以不得不得升级大版本😂,你会发现5.x以后的依赖都是固定版本号的。 - 2019年9月发布的 v5.1.0版本,引入了一些 hooks 以及
<Route children> - 2022 年10月更新的 v5.3.4,目前 v5 的最新版本
 
3.x
- 生命周期:v3.0.0(2016年10月) - v3.2.6(2020年3月)
 - css-tricks.com/learning-re...
 - 集中式配置的路由,v6 配置路由的方式其更像 v3
 
            
            
              jsx
              
              
            
          
          const PrimaryLayout = props => (
  <div className="primary-layout">
    <header>router v3</header>
    <main>
      {props.children} // 嵌套路由
    </main>
  </div>
)
// v3
const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
) 
        而v4 不再使用集中式配置的方式,路由规则可以分散到页面和布局中,可实现动态路由。
            
            
              jsx
              
              
            
          
          const PrimaryLayout = () => (
  <div className="primary-layout">
   <header>router v3</header>
    <main>
     <Switch>
        <Route path="/" exact component={HomePage} />
        <Route path="/users" component={UsersPage} />
      </Switch>
    </main>
  </div>
)
// v4
const App = () => (
  <BrowserRouter>
    <PrimaryLayout />
  </BrowserRouter>
)
        v4 的路由匹配的特点(槽点):
- 默认为包容性路由,多个
<Router>匹配会同时渲染 - 使用
<Switch>包裹才会只匹配一个路由,但是按Route排列的先后顺序匹配的 - 如果访问 
/users,path"/"和"/users"都会匹配,所以需要exact以确保path 要完全匹配 
react router
@reach/router 相比同期的 react-router 更简洁轻量,react-router v6 吸收了 reach-router 的很多想法,可以看作 reach-router 的 2.x 版本,所以 API 风格更接近 reach-router
最后一次更新为 2020 年 2 月发布的 1.3.4 版本
小结
React Router v6 is the successor(后续版本) to all previous versions of React Router including v3 and v4/5. It is also the successor to Reach Router.
react-router v6 就是吸收了 v3 v4/5 以及 @reach/router 的最佳想法,才有了现在的 v6。
- 支持 v3 的集中配置嵌套路由,以及基于对象配置路由的方式,可读性更好。
 - 使用
<Routes>代替<Switch>,也无需指定exact;<Routes>也可以定义多个; - 支持 reach router 的最佳路径匹配和相对路径等
 
从 v6.4 后又融合了 remix 框架了的 Data API,但不得不说 react-router 6.4 后越来越复杂了,如果想用个基本的路由功能,那可以先不用 Data API。
想了解 Data API 的可以先看这篇博客,个人感觉要用 Data API 自己用的话,不如就直接上 Remix 框架了( 9月14号刚发布了Remix v2)。
NPM包构成
根据平台选择
- Web应用:
react-router-dom - React Native:
react-router-native 
两者都依赖react-router核心包,不必手动安装。
最底层依赖包:
v6.4 之前的是依赖的 history,之后依赖的@remix-run/router,和 remix (v1.8.0+) 共用,代替了history,也就支持了 Data API
创建路由的方式
安装
            
            
              bash
              
              
            
          
          npm i react-router-dom
        路由器分为 Browser、Hash、Memory、Static 类型的路由器,web 端常用的路由类型就是 history 和 hash,本文以 history 路由为例讲解,也就是 BrowserRouter。
主要是分为两种创建方式:
- 使用 
createBrowserRouter函数,v6.4 推出的支持 Data API - 使用 
<BrowserRouter>组件,经典方式,不支持 Data API 
使用createBrowserRouter函数
createXXRouter是在 v6.4.0 版本推出的,官方称为 Data Router,支持 Data API 。
最新的官网是使用createBrowserRouter 创建 Router 实例,传入RouterProvider 使用。
            
            
              jsx
              
              
            
          
          // App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";
// import route components
const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "/about",
    element: <About />,
  },
]);
export default function App() {
  return <RouterProvider router={router} />;
}
        默认是 JS 对象形式创建的,如果习惯使用 JSX 创建,则需要套上createRoutesFromChildren/createRoutesFromElements
            
            
              jsx
              
              
            
          
          const router = createBrowserRouter(
  createRoutesFromChildren(
    <>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
    </>
  )
);
        使用 RouterProvider 时,如果声明 React 元素element: <Home />但并不需要传参,可以使用声明组件的方式Component: Home,看起来更简洁。
使用<BrowserRouter>组件
要用BrowserRouter包裹Routes,用来提供上下文。
<Routes>包裹多个<Route>,当路径改变时<Routes> 会检查其所有 <Route> 找到最佳匹配,并渲染出来。
- 经典的JSX方式:
 
            
            
              jsx
              
              
            
          
          import { BrowserRouter, Routes, Route } from "react-router-dom";
// import route components
export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}
        - 如果喜欢用 JS 对象的方式声明路由则可以用 
useRoutes,在 v5 版本中要使用react-router-config包才可以。 
            
            
              jsx
              
              
            
          
          import { BrowserRouter, useRoutes } from "react-router-dom";
// import route components
function Routes() {
  return useRoutes([
    {
      path: "/",
      element: <Home />,
    },
    {
      path: "/about",
      element: <About />,
    },
  ]);
}
export default function App() {
  return (
    <BrowserRouter>
      <Routes />
    </BrowserRouter>
  );
}
        使用钩子 useRoutes 其实就是 <Routes> 的替代 ,只是风格不同。<Routes>就是基于useRoutes 实现的。
            
            
              jsx
              
              
            
          
          export function Routes({
  children,
  location,
}: RoutesProps): React.ReactElement | null {
  return useRoutes(createRoutesFromChildren(children), location);
}
        路由匹配
:动态参数
        
            
            
              jsx
              
              
            
          
          function Team() {
  let params = useParams();
  console.log(params.teamId); // "hotspur"
}
<Route
  // this path will match URLs like
  // - /teams/hotspur
  path="/teams/:teamId"
  element={<Team />}
/>;
        ?可选参数
        
            
            
              jsx
              
              
            
          
          <Route
  // this path will match URLs like
  // - /categories
  // - /en/categories
  path="/:lang?/categories"
  element={<Categories />}
/>;
function Categories() {
  let params = useParams();
  console.log(params.lang);
}
        * 通配符
通配符只能在path末尾使用
            
            
              jsx
              
              
            
          
          <Route
  // this path will match URLs like
  // - /files
  // - /files/one
  // - /files/one/two
  // - /files/one/two/three
  path="/files/*"
  element={<Team />}
/>;
// and the element through `useParams`
function Team() {
  let params = useParams();
  console.log(params["*"]); // "one/two"
}
        path="*",可用于404页面
            
            
              jsx
              
              
            
          
          <Routes>
  <Route path="/" element={<Home />} />
  <Route path="*" element={<NotFound />} />
</Routes>
        嵌套路由
相对路径
path 不以/开头的路径为相对路径:
<Route path>相对于父路由,不必补全path<Link to>相对父路由
当然 v6 也支持绝对路径
Outlet
嵌套<Route>的情况下,需要使用 Outlet 渲染子路由,相当于占位元素。相比 v3 用的 props.children 更直观。
            
            
              jsx
              
              
            
          
          <Routes>
    <Route path="parent" element={<Parent />}>
        <Route path="child" element={<Child />} />
    </Route>
</Routes>
function Parent() {
    return (
        <div>
            <h1>Parent Component</h1>
            <Outlet />  {/* 当访问 "parent/child" 时,Child 组件会在这里渲染 */}
        </div>
    );
}
        如果父路由不指定element 则默认为<Outlet>
布局路由
父路由不指定path时,仅作为布局,子路由元素将在Outlet的位置显示。
            
            
              jsx
              
              
            
          
          <Route element={<PageLayout />}>
  <Route path="/" element={<Section content="Home" />} />
  <Route path="/about" element={<Section content="About" />} />
</Route>
function PageLayout({ children }) {
    return (
        <div>
            <header>Header Section</header>
            <Outlet />  {/* 子路由 (如 Section) 会在这里渲染 */}
        </div>
    );
}
        索引路由
没有path的子路由;访问父路由时渲染,如访问/teams 时将渲染TeamsIndex
            
            
              jsx
              
              
            
          
          <Route path="/teams" element={<Teams />}>
  <Route index element={<TeamsIndex />} />
  <Route path=":id" element={<Team />} />
</Route>
        多级Routes
v6 也可以定义多级Routes, 但父Route的path要以*结尾才会匹配
            
            
              javascript
              
              
            
          
            <Routes>
    <Route path="blog/**" element={<Blog />} />
  </Routes>;
  function Blog() {
    return (
      <Routes>
        <Route path="post/:id" element={<Post />} />
      </Routes>
    );
  }
        hooks
useNavigate()
- 用于编程方式导航,具体看文档
 
            
            
              jsx
              
              
            
          
          function Nav() {
  const navigate = useNavigate();
  return (
    <div>
      <button onClick={() => navigate("/")}>home</button>
      <button onClick={() => navigate("/about")}>about</button>
    </div>
  );
}
        useParams()
- 获取动态参数的对象
 
            
            
              jsx
              
              
            
          
          function ProfilePage() {
  // Get the userId param from the URL.
  let { userId } = useParams();
  // ...
}
function App() {
  return (
    <Routes>
      <Route path="users">
        <Route path=":userId" element={<ProfilePage />} />
        <Route path="me" element={...} />
      </Route>
    </Routes>
  );
        useSearchParams()
- 用于读取和修改当前url的查询字符串,返回 searchParams 对象
 
            
            
              jsx
              
              
            
          
          const [searchParams, setSearchParams] = useSearchParams();
        useLocation()
- 返回 Location 对象
 
            
            
              jsx
              
              
            
          
          // history栈中的一条记录
interface Location {
  pathname: string; // 继承自Path
  search: string; // 继承自Path
  hash: string; // 继承自Path
  state: unknown; // 当前 location 关联的任意值
  key: string; // 当前 location 关联的唯一字符串
}
        组件
Link和NavLink
<Link />和<NavLink />用于导航,to可指定绝对路径(/开头)和相对路径,相对路径可以像cd命令一样用,如..NavLink在Link基础上实现,可判断当前路由是否被激活,具体看文档。
            
            
              jsx
              
              
            
          
          <NavLink
  to="about"
  className={({ isActive, isPending }) => {
    return isActive ? "active" : isPending ? "pending" : "";
  }}
/>
        Navigate
<Navigate />用于路由跳转,当渲染该组件时,立即跳转指定路由;可用于权限判断的重定向。
            
            
              jsx
              
              
            
          
          function App() {
  return (
    <Routes>
      <Route path="/public" element={<PublicPage />} />
      <Route
        path="/protected"
        element={
          <RequireAuth redirectTo="/login">
            <ProtectedPage />
          </RequireAuth>
        }
      />
    </Routes>
  );
}
function RequireAuth({ children, redirectTo }) {
  let isAuthenticated = getAuth();
  return isAuthenticated ? children : <Navigate to={redirectTo}  replace={true} />;
}
        懒加载
使用React.lazy() 和动态 import()以及Suspense 实现,可自己封装成一个 Wrapper 使用
            
            
              jsx
              
              
            
          
          const About = React.lazy(() => import("./pages/about"));
<Route
  path="/about"¬
  element={
    <React.Suspense fallback={<Loading />}>
      <About />
    </React.Suspense>
  }
/>