当前期刊数: 145
1 引言 React Router v6 alpha 版本发布了,本周通过 A Sneak Peek at React Router v6 这篇文章分析一下带来的改变。
2 概述 更名为 一个不痛不痒的改动,使 API 命名更加规范。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import { BrowserRouter , Switch , Route } from "react-router-dom" ;function App ( ) { return ( <BrowserRouter > <Switch > <Route exact path ="/" > <Home /> </Route > <Route path ="/profile" > <Profile /> </Route > </Switch > </BrowserRouter > ); }
在 React Router v6 版本里,直接使用 Routes
替代 Switch
:
1 2 3 4 5 6 7 8 9 10 11 12 13 import { BrowserRouter , Routes , Route } from "react-router-dom" ;function App ( ) { return ( <BrowserRouter > <Routes > <Route path ="/" element ={ <Home /> } /> <Route path ="profile/*" element ={ <Profile /> } /> </Routes > </BrowserRouter > ); }
升级 在 v5 版本里,想要给组件传参数是不太直观的,需要利用 RenderProps 的方式透传 routeProps
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import Profile from './Profile' ;<Route path =":userId" component ={Profile} /> <Route path =":userId" render ={routeProps => ( <Profile {...routeProps } animate ={true} /> )} /> <Route path =":userId" element ={ <Profile /> } /><Route path =":userId" element ={ <Profile animate ={true} /> } />
而在 v6 版本中,render
与 component
方案合并成了 element
方案,可以轻松传递 props 且不需要透传 roteProps
参数。
更方便的嵌套路由 在 v5 版本中,嵌套路由需要通过 useRouteMatch
拿到 match
,并通过 match.path
的拼接实现子路由:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 import { BrowserRouter , Switch , Route , Link , useRouteMatch } from "react-router-dom" ;function App ( ) { return ( <BrowserRouter > <Switch > <Route exact path ="/" component ={Home} /> <Route path ="/profile" component ={Profile} /> </Switch > </BrowserRouter > ); }function Profile ( ) { let match = useRouteMatch (); return ( <div > <nav > <Link to ={ `${match.url }/me `}> My Profile</Link > </nav > <Switch > <Route path ={ `${match.path }/me `}> <MyProfile /> </Route > <Route path ={ `${match.path }/:id `}> <OthersProfile /> </Route > </Switch > </div > ); }
在 v6 版本中省去了 useRouteMatch
这一步,支持直接用 path
表示相对路径:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import { BrowserRouter , Routes , Route , Link , Outlet } from "react-router-dom" ;function App ( ) { return ( <BrowserRouter > <Routes > <Route path ="/" element ={ <Home /> } /> <Route path ="profile/*" element ={ <Profile /> } /> </Routes > </BrowserRouter > ); }function Profile ( ) { return ( <div > <nav > <Link to ="me" > My Profile</Link > </nav > <Routes > <Route path ="me" element ={ <MyProfile /> } /> <Route path =":id" element ={ <OthersProfile /> } /> </Routes > </div > ); }function App ( ) { return ( <BrowserRouter > <Routes > <Route path ="/" element ={ <Home /> } /> <Route path ="profile" element ={ <Profile /> }> <Route path =":id" element ={ <MyProfile /> } /> <Route path ="me" element ={ <OthersProfile /> } /> </Route > </Routes > </BrowserRouter > ); }function Profile ( ) { return ( <div > <nav > <Link to ="me" > My Profile</Link > </nav > <Outlet /> </div > ); }
注意 Outlet
是渲染子路由的 Element。
useNavigate 替代 useHistory 在 v5 版本中,主动跳转路由可以通过 useHistory
进行 history.push
等操作:
1 2 3 4 5 6 7 8 9 10 import { useHistory } from "react-router-dom" ;function MyButton ( ) { let history = useHistory (); function handleClick ( ) { history.push ("/home" ); } return <button onClick ={handleClick} > Submit</button > ; }
而在 v6 版本中,可以通过 useNavigate
直接实现这个常用操作:
1 2 3 4 5 6 7 8 9 10 import { useNavigate } from "react-router-dom" ;function MyButton ( ) { let navigate = useNavigate (); function handleClick ( ) { navigate ("/home" ); } return <button onClick ={handleClick} > Submit</button > ; }
react-router 内部对 history 进行了封装,如果需要 history.replace
,可以通过 { replace: true }
参数指定:
1 2 3 4 5 6 7 history.push ("/home" ); history.replace ("/home" );navigate ("/home" );navigate ("/home" , { replace : true });
更小的体积 8kb 由于代码几乎重构,v6 版本的代码压缩后体积从 20kb 缩小到 8kb。
3 精读 react-router v6 源码中有一段比较核心的理念,笔者拿出来与大家分享,对一些框架开发是大有裨益的。我们看 useRoutes
这段代码节选:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 export function useRoutes (routes, basename = "" , caseSensitive = false ) { let { params : parentParams, pathname : parentPathname, route : parentRoute } = React .useContext (RouteContext ); if (warnAboutMissingTrailingSplatAt) { } basename = basename ? joinPaths ([parentPathname, basename]) : parentPathname; let navigate = useNavigate (); let location = useLocation (); let matches = React .useMemo ( () => matchRoutes (routes, location, basename, caseSensitive), [routes, location, basename, caseSensitive] ); let element = matches.reduceRight ((outlet, { params, pathname, route } ) => { return ( <RouteContext.Provider children ={route.element} value ={{ outlet , params: readOnly ({ ...parentParams , ...params }), pathname: joinPaths ([basename , pathname ]), route }} /> ); }, null ); return element; }
可以看到,利用 React.Context
,v6 版本在每个路由元素渲染时都包裹了一层 RouteContext
。
拿更方便的路由嵌套来说:
在 v6 版本中省去了 useRouteMatch
这一步,支持直接用 path
表示相对路径。
这就是利用这个方案做到的,因为给每一层路由文件包裹了 Context,所以在每一层都可以拿到上一层的 path
,因此在拼接路由时可以完全由框架内部实现,而不需要用户在调用时预先拼接好。
再以 useNavigate
举例,有人觉得 navigate
这个封装仅停留在形式层,但其实在功能上也有封装,比如如果传入但是一个相对路径,会根据当前路由进行切换,下面是 useNavigate
代码节选:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function useNavigate ( ) { let { history, pending } = React .useContext (LocationContext ); let { pathname } = React .useContext (RouteContext ); let navigate = React .useCallback ( (to, { replace, state } = {} ) => { if (typeof to === "number" ) { history.go (to); } else { let relativeTo = resolveLocation (to, pathname); let method = !!replace || pending ? "replace" : "push" ; history[method](relativeTo, state); } }, [history, pending, pathname] ); return navigate; }
可以看到,利用 RouteContext
拿到当前的 pathname
,并根据 resolveLocation
对 to
与 pathname
进行路径拼接,而 pathname
就是通过 RouteContext.Provider
提供的。
巧用多层 Context Provider 很多时候我们利用 Context 停留在一个 Provider
,多个 useContext
的层面上,这是 Context 最基础的用法,但相信读完 React Router v6 这篇文章,我们可以挖掘出 Context 更多的用法:多层 Context Provider。
虽然说 Context Provider 存在多层会采取最近覆盖的原则,但这不仅仅是一条规避错误的功能,我们可以利用这个功能实现 React Router v6 这样的改良。
为了更仔细说明这个特性,这里再举一个具体的例子:比如实现搭建渲染引擎时,每个组件都有一个 id,但这个 id 并不透出在组件的 props 上:
1 2 3 const Input = ( ) => { };
此时如果我们允许 Input 组件内部再创建一个子元素,又希望这个子元素的 id 是由 Input 推导出来的,我们可能需要用户这么做:
1 2 3 const Input = ({ id } ) => { return <ComponentLoader id ={id + "1 "} /> ; };
这样做有两个问题:
将 id 暴露给 Input 组件,违背了之前设计的简洁性。
组件需要对 id 进行拼装,很麻烦。
这里遇到的问题和 React Router 遇到的一样,我们可以将代码简化成下面这样,但功能不变吗?
1 2 3 const Input = ( ) => { return <ComponentLoader id ="1" /> ; };
答案是可以做到,我们可以利用 Context 实现这种方案。关键点就在于,渲染 Input 但组件容器需要包裹一个 Provider:
1 2 3 const ComponentLoader = ({ id, element } ) => { <Context.Provider value ={{ id }}> {element}</Context.Provider > ; };
那么对于内部的组件来说,在不同层级下调用 useContext
拿到的 id 是不同的,这正是我们想要的效果:
1 2 3 4 5 6 7 const ComponentLoader = ({id,element} ) => { const { id : parentId } = useContext (Context ) <Context .Provider value={{ id : parentId + id }}> {element} </Context .Provider > }
这样我们在 Input
内部调用的 <ComponentLoader id="1" />
实际上拼接的实际 id 是 01
,而这完全抛到了外部引擎层处理,用户无需手动拼接。
4 总结 React Router v6 完全利用 Hooks 重构后,不仅代码量精简了很多,还变得更好用了,等发正式版的时候可以快速升级一波。
另外从 React Router v6 做的这些优化中,我们从源码中挖掘到了关于 Context 更巧妙的用法,希望这个方法可以帮助你运用到其他更复杂的项目设计中。
讨论地址是:精读《React Router v6》 · Issue ##241 · dt-fe/weekly
如果你想参与讨论,请 点击这里 ,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。
关注 前端精读微信公众号
版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证 )