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
组件,语义上更贴近其子元素Route
,Routes
中实现了全新的路由查找算法,许多新特性大多基于此算法实现。
规范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,例如useHistory
、useLocation
以及上文提到的useRouteMatch
等,但v5版本核心组件还是基于类组件开发,随着React Hooks的发布,社区中的React类库开始向Hooks迁移,因此v5中的hooks相当于过渡使用。v6开始全面重写,拥抱Hooks函数组件以及TS,许多API也是利用组合的思想来拆分、包装代码,例如Routes
则是useRoutes
的包装,Navigate
是useNavigate
的包装,Outlet
是useOutlet
的包装。
总结
React Router目前是React应用路由管理的最佳方案(没有之一), v6版本基于全新的路由算法带来强大的功能和hooks,开发体验大大增强👋