什么,React Router已经到V6了 ??

16,736 阅读6分钟

上周突然心血来潮,去 react-router 的 github 看了下,好家伙,最新版本居然已经到了 6.0.0-beta.8 (已于2021.11.4发布 6.0.0 正式版),且全部用 ts 重写(表示好评!!),具体变化可看 Migrating React Router v5 to v6。不看不知道,一看吓一跳,发现变化还是挺大的,不过,很多 api 也变得比之前好用。那么,下面我会尽可能提供各种例子介绍一些常用 api 的改动,让大家更快熟悉最新的用法。

<Switch>全部改为<Routes>

Switch 相比,Routes 的主要优点是:

  • Routes 内的所有 <Route><Link> 是相对的。这使得 <Route path><Link to> 中的代码更精简、更可预测
  • 路由是根据最佳匹配而不是按顺序选择的
  • 路由可以嵌套在一个地方,而不是分散在不同的组件中(当然也可以写在子组件中),而且嵌套的 parent route 的 path 不用加*
import {
  BrowserRouter,
  Routes,
  Route,
  Link,
  Outlet
} from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        {* 上面的优点一:path是相对的 *}
        {* 上面的优点三:path 不用加'*' *}
        <Route path="users" element={<Users />}>
          {* 上面的优点二: 无需按顺序 *}
          {* 上面的优点三: 路由可以嵌套在一个地方 *}
          <Route path="me" element={<OwnUserProfile />} />
          <Route path=":id" element={<UserProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        {* 上面的优点一: <Link to>是相对的 *}
        <Link to="me">My Profile</Link>
      </nav>
      {* Outlet后面会讲 *}
      <Outlet />
    </div>
  );
}

注意上面的第三点,嵌套的 parent route 的 path 不用加*。但如果不是嵌套,而是分散在子组件中,就需要尾部加上*


function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        {* 不是嵌套就需要尾部加上* *}
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}
function Users() {
  return (
    <Routes>
      <Route element={<UsserLayout />}>
        <Route path="me" element={<OwnUserProfile />} />
        <Route path=":id" element={<UserProfile />} />
      </Route>
    </div>
  );
}
function UsersLayout() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
        <Link to="2">User Profile</Link>
      </nav>
      <Outlet />
    </div>
  );
}

<Route>

<Route element>

Route 的 render 或 component 改为 element

   <Route path=":userId" element={<Profile animate={true} /> />

   function Profile({ animate }) {
     const params = useParams();
     const location = useLocation();
   }

通过这种形式:

  • 可以向组件传 props,如上面的 animate={true}
  • 因为有了 hook 的出现,所以不必再通过 renderProps 向组件传递路由的一些 props,我们可以通过useParamsuseLocation 就可以拿到这些信息

当然还有另外一个重要的原因是因为 v6 Route 的 children 是用于嵌套路由,如下

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users" element={<Users />}>
          <Route path="me" element={<OwnUserProfile />} />
          <Route path=":id" element={<UserProfile />} />
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

那么我们要显示 OwnUserProfile 组件的话通过 /users/me 就可以定位到了,这看起来相当的直观

<Route path>

v6 简化了 path 的格式,只支持两种动态占位符:

  • :id 样式参数
  • * 通配符,只能在 path 的末尾使用,如users/*

举个 🌰

以下的 path 是正确的:

path = '/groups'
path = '/groups/admin'
path = '/users/:id'
path = '/users/:id/messages'
path = '/files/*' // 通配符放在末尾
path = '/files/:id/*'
path = '/files-*'

以下的 path 是错误的:

path = '/users/:id?' // ? 不满足上面两种格式
path = '/tweets/:id(\d+)' // 有正则,不满足上面两种格式
path = '/files/*/cat.jpg'// 通配符不能放中间

<Route caseSensitive>

caseSensitive: boolean,用于正则匹配 path 时是否开启 ignore 模式,即匹配时是否忽略大小写

源码中部分

// 根据path生成是regexpSource是否要ignore
const matcher = new RegExp(regexpSource, caseSensitive ? undefined : "i");

<Route index>

index 即表示是否是主路由,如果设置为 true 的话不能有 children,如下面的<Route index element={<Home />} />

function App() {
  return (
    <Routes>
      <Route path='/' element={<Layout />}>
        <Route path='auth/*' element={<Auth/> } />
        <Route path='basic/*' element={<Basic/> } />
      </Route>
    </Routes>
  )
}
function Home() {
  return (
    <h2> Home </h2>
  )
}
function Basic() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <h2>下面()中的就是真实的Link组件</h2>
      <Routes>
        <Route element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />}/>
          ...
        </Route>
      </Routes>
    </div>
  );
}

那么/basic会自动显示<Home />

<Layout /> 与 <Outlet />

上面的很多例子中都有 Layout,这个究竟有什么用?和Outlet又有什么关系?

我们看下代码(和下面的图片搭配看效果更佳)

function App() {
  return (
    <Routes>
      <Route path='/' element={<Layout />}>
        <Route path='auth/*' element={<Auth/> } />
        <Route path='basic/*' element={<Basic/> } />
      </Route>
    </Routes>
  )
}

// 下图的最外面的红色框
function Layout() {
  return (
    <>
     <p>主页面</p>
      <ul>
        <li>
          <Link to='auth'>auth</Link>
        </li>
         <li>
          <Link to='basic'>basic</Link>
        </li>
      </ul>
      <hr />
      {* 下图蓝色框 *}
      <Outlet />
    </>
  )
}

function Basic() {
  return (
    <div>
      <h1>Welcome to the app!</h1>
      <h2>下面()中的就是真实的Link组件</h2>
      <Routes>
        <Route element={<BasicLayout />}>
          <Route index element={<Home />} />
          <Route path="about" element={<About />}/>
          ...
        </Route>
      </Routes>
    </div>
  );
}
// 下图绿色框
function BasicLayout() {
  return (
    <div>
      <ul>
          ...
          <Link to=".">Home({` <Link to=".">`})</Link>
          ...
        </li>
      </ul>
    <Outlet />
    </div>
  );
}
// 下图黄色框
function Home() {
  return (
    <h2>Home</h2>
  )
}

从代码和上面示意图我们可以得知,组件<Layout/>一般作为 parent route 的element,除了渲染一些公共的UI, 还有用于渲染子路由的<Outlet/>, 而代码中的<Outlet/>渲染的是<Basic/><Basic><BasicLayout />又有<Outlet/>,但其渲染的是<Route element={<BasicLayout />}>匹配到路由的 element。

从上面我们可以得到的结论是: 组件 <Layout/> 一般作为 parent route 的element,除了渲染一些公共的UI外, 其中的<Outlet/> 的作用类似插槽,用于匹配子路由的 element

useRoutes

上面我们都是把 Route 作为 Routes 的 children

function App() {
  return (
    <Routes>
      <Route path='/' element={<Layout />}>
        <Route path='auth/*' element={<Auth/> } />
        <Route path='basic/*' element={<Basic/> } />
      </Route>
    </Routes>
  )
}

但是我们还可以通过 useRoutes 生成对应的 element

import  { useRoutes } from 'react-router-dom'
function App() {
  const element = useRoutes([
    {
      path: '/',
      element: <Layout />,
      children: [
        {
          path: 'auth/*',
          element: <Auth/>
        },
        {
          path: 'basic/*',
          element: <Basic/>
        }
      ]
    }
  ])
  return (
     {element}
  )
}

这种配置项让我们可以更清晰地看出路由的嵌套结构

<Link>

<Link to>

v5中,如果 to 不以 / 开头的话会让人有点迷,因为这取决于当前的 URL。比如当前 URL 是/user, 那么<Link to="me"> 会渲染成 <a href="/me">; 而如果是 /users/,那么又会渲染成 <a href="/users/me">

那么,在 v6 中解决了上面那种让人迷惑的现象。

v6 中,无论当前 URL 是 /user 还是 /users/<Link to="me"> 都会渲染成 <a href='/user/me'>

也就是说 to 更像我们常用的 cd 命令行,我们看下更多例子


<Route path="app">
  <Route path="dashboard">
    <Route path="stats" />
  </Route>
</Route>

//  当前 URL 为 /app/dashboard 或 /app/dashboard/
<Link to="stats">               => <a href="/app/dashboard/stats">
<Link to="../stats">            => <a href="/app/stats">
<Link to="../../stats">         => <a href="/stats">
<Link to="../../../stats">      => <a href="/stats">

// 命令行中, 当前目录为 /app/dashboard
cd stats                        # pwd is /app/dashboard/stats
cd ../stats                     # pwd is /app/stats
cd ../../stats                  # pwd is /stats
cd ../../../stats               # pwd is /stats

有一点要特别注意,当 <Route path> 匹配多个 URL 片段时, <Link to=".."> 不总是渲染为 <a href="..">, 这种情况下的 to='..' 是基于父级 Route 的 path,例子如下

function App() {
  return (
    <Routes>
      <Route path="users">
        <Route
          path=":id/messages"
          element={
            // 最终是/users, 而不是/:id
            <Link to=".." />
          }
        />
      </Route>
    </Routes>
  );
}

如果还不理解,我们可以举个极端例子,比如 route 的 path 为basic/*,当前 URL 为/basic/auth/home,如下代码

function App() {
  return (
    <Routes>
      <Route path="/">
        <Route path="auth/*" element={<Auth />} />
        <Route
          path="basic/*"
          element={
            ...
            <Link to="../auth" />
            ...
          }
        />
      </Route>
    </Routes>
  );

那么上面的 to="../auth" 是要跳转到 /basic/auth 还是/auth (答案是/auth)?所以为了避免 * 通配符可以匹配多种路径的情况,统一为 to='..' 是基于父级 Route 的 path

<Link state>

即点击后可以给 to 传对应的 state

<Link replace>

replace:boolean,默认 false,即跳转路由要用 push 还是 replace

<Link target>

target 类型为

type HTMLAttributeAnchorTarget =
        | '_self'
        | '_blank'
        | '_parent'
        | '_top'
        | (string & {});

这个其实我们几乎没用到,这里只简单介绍下可以传入的值

useHistory 被干掉了,换成了 useNavigate

使用方法如下:

// v6
import { useNavigate } from "react-router-dom";

function App() {
  const navigate = useNavigate();
  function handleClick() {
    navigate("/home");
  }
  return (
    <div>
      <button onClick={handleClick}>go home</button>
    </div>
  );
}

naviaget(to)默认就是 history.push

// v6
navigate('/home');
//v5
history.push('/home')

naviaget(to, { replace: true })就是 history.replace

// v6
navigate('/home', { replace: true });
//v5
history.replace('/home')

naviaget(to: number)就是 history.go

// v6
import { useNavigate } from "react-router-dom";

function App() {
  const navigate = useNavigate();

  return (
    <>
      <button onClick={() => navigate(-2)}>
        Go 2 pages back
      </button>
      <button onClick={() => navigate(-1)}>Go back</button>
      <button onClick={() => navigate(1)}>
        Go forward
      </button>
      <button onClick={() => navigate(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}
//  v5
import { useHistory } from "react-router-dom";

function App() {
  const { go, goBack, goForward } = useHistory();

  return (
    <>
      <button onClick={() => go(-2)}>
        Go 2 pages back
      </button>
      <button onClick={goBack}>Go back</button>
      <button onClick={goForward}>Go forward</button>
      <button onClick={() => go(2)}>
        Go 2 pages forward
      </button>
    </>
  );
}

naviagete 与<Link to>一样, 输入参数类似 cd 命令行


<Route path="app">
  <Route path="dashboard">
    <Route path="stats" />
  </Route>
</Route>

//  当前 URL 为 /app/dashboard 或 /app/dashboard/
<Link to="stats">               => <a href="/app/dashboard/stats">
<Link to="../stats">            => <a href="/app/stats">
<Link to="../../stats">         => <a href="/stats">
<Link to="../../../stats">      => <a href="/stats">

//  当前 URL 为 /app/dashboard 或 /app/dashboard/
const navigate = useNavigate()
navigate('stats') => '/app/dashboard/stats'
navigate('../stats') => '/app/stats'
navigate('../../stats') => '/stats'
navigate('../../../stats') => '/stats'

// 命令行中, 当前目录为 /app/dashboard
cd stats                        # pwd is /app/dashboard/stats
cd ../stats                     # pwd is /app/stats
cd ../../stats                  # pwd is /stats
cd ../../../stats               # pwd is /stats

结语

好了,到了这里也差不多了,我们总结下一些变化:

  • <Switch> 全部改为 <Routes>
  • Routerendercomponent 改为 element,且可以嵌套路由
  • Route 还可以通过传入嵌套数组给useRoutes生成对应的element
  • tonavigatepath 不以 / 开头都是相对路径,与 cd 命令行类似
  • <Outlet /> 是个插槽,可以渲染匹配到的子路由

如果还想了解更多例子,可以打开react-router-source-analysis,有更多的例子。

到目前为止,这应该、大概、也许算国内最新的 React-Router 的源码分析了吧,有兴趣的可以看看,觉得可以的话请不要吝啬您的⭐ 。

最后

之后应该会写 React-Router 最新的源码分析文章,如果我不鸽的话🐶🐶,哈哈。

end-cover.png

感谢留下足迹,如果您觉得文章不错😄😄,还请动动手指😋😋,点赞+收藏+转发🌹🌹

往期文章

翻译翻译,什么叫ReactDOM.createRoot

翻译翻译,什么叫JSX