阅读 483

React-Router v6新特性

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

  1. 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 中实现了全新的路由查找算法,许多新特性大多基于此算法实现。

  2. 规范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来减轻开发者使用的负担,最终让开发体验得到提升。

  3. 手动排序 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组件。

  4. 相对路径的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>
      );
    }
    复制代码
  5. 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可以将所有的路由(嵌套的子路由)配置合并在一起,可进行路由的统一管理,增加了代码可维护性。

  6. 一系列的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,开发体验大大增强👋

文章分类
前端
文章标签