React-Router v6新特性

2,813 阅读4分钟

React-Router v6稳定版本已在近期发布,相较前两个主版本(v5和v4, v5并没有做破坏性更改)带来了破坏性的api,v6路由功能搭配一系列的hooks将变得更加强大,我们先一睹为快。

Routes替换Switch

v5版本中使用Switch来匹配命中路由的组件

// v5
function App() {
  return (
    <BrowserRouter>
       <Switch>
         <Route exact path="/">
           <Home />
         </Route>
         <Route path="/about">
           <About />
         </Route>
         <Route path="/users/:id" children={<User />} />
        </Switch>
     </BrowserRouter>
   );
 }

在v5中,Switch担任路由匹配的核心角色,它会遍历查找自己的子元素,基于v5的路由匹配算法会渲染第一个命中路由的组件。

// v6
function App() {
   return (
     <BrowserRouter>
       <Routes> // 使用Routes替换 Switch
          <Route path="/Home" element={<Home />} />
          <Route path="/user" element={<Users />} />
       </Routes>
     </BrowserRouter>
   )
}

v6版本使用Routes替换Switch组件,语义上更贴近其子元素RouteRoutes 中实现了全新的路由查找算法,许多新特性大多基于此算法实现。

规范Route的渲染属性

v5中提供component、render、children三种方式渲染组件

// v5
function App() {
  return (
    <BrowserRouter>
      <Switch>
        // component渲染
        <Route exact path=":userId" component={User} />
        <Route
          path=":userId"
          // render函数渲染
          render={routeProps => (
            <User routeProps={routeProps} animate={true} />
          )}
         />
        <Route
          path=":userId"
          // children渲染
          children={({ match }) => (
            match ? (
              <User match={match} animate={true} />
            ) : (
              <NotFound />
            )
          )}
        />
        <Route
          path=":userId"
          children={<User animate={true} />}
        />
      </Switch>
    </BrowserRouter>
  );
}

v5的Route可以接收component作为渲染组件,看起来很简单,但我们无法给该组件(User)传递自定义属性,因此我们可以使用render来弥补component的缺陷,除此之外v5版本还提供children属性,当children是一个函数时,接收路由匹配的上下文数据,实际上功能和render相同。v6版本将Route的三个属性统一规范成element

// v6
function App() {
   return (
     <BrowserRouter>
       <Routes>
          <Route path="/user" element={<User />} />
          <Route path="/user/1" element={<User animate={true} />} />
       </Routes>
     </BrowserRouter>
   )
}

// 结合v6提供的hooks,可以获取路由相关数据
function User({ animate }) {
  let params = useParams();
  let location = useLocation();
}

element的类型为React.ReactNode,可以传入Suspense实现路由懒加载。v6在支持原有路由功能的基础上,通过规范、减少组件api来减轻开发者使用的负担,最终让开发体验得到提升。

手动排序 vs 智能匹配

由于v5中的路由算法是渲染第一个命中的路由组件,开发者在使用时需要进行手动排序来展示组件优先级。v6中的路由匹配算法更加智能、强大,会通过计算比较返回优先级高的路由组件。通过一个例子看他们之间的区别。

<Route path="/user/:id" component={User} />
<Route path="/user/new" component={NewUser} />

我们定义了两个路由,实际上访问大多数/user路径没有很大的歧义,但当页面访问/user/new时,此时同时命中了这两个路由,那么最终会渲染哪个组件? 在v5中,最终渲染是先定义的路由组件(User),即先定义,先渲染(符合之前说的v5路由算法返回第一个匹配的组件),所以开发者需要手动对路由进行排序控制组件渲染的优先级。但这并不符合我们的页面渲染预期。 在v6中,一切变的更加自动和智能,开发者不需要手动维护路由组件的顺序,而是交给v6路由匹配算法自动选择渲染,那么v6具体是怎样实现的呢?简单的讲,在v6内部,会对每个路径进行分割,对路径中的各个部分累计打分排名,分数越高,则优先渲染,其打分方法如下:

// 匹配路由动态部分, 如/:id
const paramRe = /^:\\w+$/;
// 动态路由部分分值
const dynamicSegmentValue = 3;
// index子路由分值
const indexRouteValue = 2;
// 空路由部分分值
const emptySegmentValue = 1;
// 静态路由部分分值
const staticSegmentValue = 10;
// 当路径中存在*时分值
const splatPenalty = -2;
const isSplat = (s: string) => s === "*";

function computeScore(path: string, index: boolean | undefined): number {
  // 分割路径成数组
  let segments = path.split("/");
  let initialScore = segments.length;
  if (segments.some(isSplat)) {
    initialScore += splatPenalty;
  }

 if (index) {
   initialScore += indexRouteValue;
 }

 return segments
   .filter(s => !isSplat(s))
   .reduce(
     (score, segment) =>
       score +
       (paramRe.test(segment)
         ? dynamicSegmentValue
         : segment === ""
         ? emptySegmentValue
         : staticSegmentValue),
     initialScore
   );
}

观察这个方法,可以看出越是动态的路由它的分数往往越低,例如,当路径中存在*时,它的初始分数就相对较低,同时动态路由部分分值(dynamicSegmentValue: 3)比静态路由部分分值(staticSegmentValue: 10)低了许多。回到上面的例子,在访问/user/new路径时,v6会计算/user/:id/user/new路由分数,由于/user/new是静态路由分数会高于/user/:id,因此v6中会渲染NewUser组件。

相对路径的Link和Route

v5嵌套路由里需要拼接绝对路径来渲染组件的子路由,例如

// v5
function App() {
 return (
   <BrowserRouter>
     <Switch>
       <Route exact path="/">
         <Home />
       </Route>
       <Route path="/users">
         <Users />
       </Route>
     </Switch>
   </BrowserRouter>
 );
}

function Users() {
  // 当子组件渲染嵌套路由时,需要通过useRouteMatch获得match对象,开发者基于match对象拼接绝对路径供Link和Route组件使用。
  let match = useRouteMatch();

  return (
    <div>
      <nav>
        <Link to={`${match.url}/me`}>My Profile</Link>
      </nav>

      <Switch>
        <Route path={`${match.path}/me`}>
          <OwnUserProfile />
        </Route>
        <Route path={`${match.path}/:id`}>
          <UserProfile />
        </Route>
      </Switch>
    </div>
  );
}

在v6中,开发者使用Link或Route时只需定义相对父路由的相对路径即可,v6内部会为我们自动拼接全路径。

// v6
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="users/*" element={<Users />} />
      </Routes>
    </BrowserRouter>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>

      <Routes>
        <Route path=":id" element={<UserProfile />} />
        <Route path="me" element={<OwnUserProfile />} />
      </Routes>
    </div>
  );
}

Outlet组件

v6带来了Outlet组件,用于渲染当前路由下的子路由组件,我们对第四点中的v6版本代码做一下转换:

// v6
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>
  );
}

function Users() {
  return (
    <div>
      <nav>
        <Link to="me">My Profile</Link>
      </nav>
      // 当访问/users/me 或者 /users/id时,子路由会被渲染
      <Outlet />
    </div>
  );
}

通过Outlet可以将所有的路由(嵌套的子路由)配置合并在一起,可进行路由的统一管理,增加了代码可维护性。

一系列的Hooks

实际上v5也提供了个别hooks,例如useHistoryuseLocation以及上文提到的useRouteMatch等,但v5版本核心组件还是基于类组件开发,随着React Hooks的发布,社区中的React类库开始向Hooks迁移,因此v5中的hooks相当于过渡使用。v6开始全面重写,拥抱Hooks函数组件以及TS,许多API也是利用组合的思想来拆分、包装代码,例如Routes则是useRoutes的包装,NavigateuseNavigate的包装,OutletuseOutlet的包装。

总结

React Router目前是React应用路由管理的最佳方案(没有之一), v6版本基于全新的路由算法带来强大的功能和hooks,开发体验大大增强👋