说明
React Router v6 正式版本与 2021-11 月发布,使用起来最大的感受是:全面拥抱 Hooks, 提供简洁的 api, 但是对类组件不是很友好,但是可以通过封装来使用。
与 v5 版本相比 v6 使用 TS 重写,Api 较 v6 有了不少的改动,直接迁移不建议,工作量会很大,官方说会有一个向后兼容包 但是截止 2021-12-17 还没有看到,可以时刻关注官方的迁移文档 ,下边也会列举一些在迁移 v5 => v6 过程中的记录
v5 => v6 升级记录
1. 使用上没有重大改动的 <BrowserRouter>
和 <HashRouter>
import * as React from "react";
import * as ReactDOM from "react-dom";
import { HashRouter } from "react-router-dom";
ReactDOM.render(
<HashRouter>
{/* The rest of your app goes here */}
</HashRouter>,
root
);
2. 使用 <Routes>
代替 <Switch>
进行路由匹配
v5 版本中使用
<Switch>
来作为子级路由的匹配器,v6 中中的<Routes>
在它的基础上 有了更强的能力,如下:
- 下层
Route
Link
的路径可以是相对路径,这在某些情况下可以使得下层代码更精简 - 下层 Route 匹配顺序,会按照最佳匹配,而不是按照写作顺序匹配
- 支持下层 Route 的嵌套,也就是说所有路由可以通过嵌套写在一个文件中,而不是分散在各个组件中,这对于小型项目来说很友好(可以一次性看到所有路由属性结构)
<Routes>
{/* 支持 Route 嵌套 */}
<Route path="app">
{/* path=":id" 是相对路径实际匹配 /app/:id */}
<Route path=":id" element={<div>in ID </div>} />
{/* /app/me 的优先级高于 /app/:id 因此会优先匹配,而不是按照写作顺序匹配 */}
<Route
path="me"
element={
<div>
<Link to="../settings"> to /app/settings</Link>
</div>
}
/>
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
3. Route
取消 render\component 改为使用 element api 来渲染组件
- v5 的 Route 通过
component={Component}
或者render=(({history}) => <Component history={history}/>)
来渲染路由匹配到的内容,这些设计是基于早期类组件的; - v6 中基于 Hooks 有了
element={<Component/>}
这个替代品,它满足了通过 props 向组件传一些参数的需求,同时又不需要向 render 那样向组件传递路由的东西,因为 v6 中可以借助 Hooks (useParams\useLocation\useNavigate)来拿到这些参数,类组件的话可以通过封装一些工具来实现
<Routes>
<Route path="settings/:type" element={<Settings />} />
</Routes>
// Settings 组件实现
import { useParams } from 'react-router-dom';
const Settings = () => {
const { type } = useParams();
return <div>App Settings Type: {type}</div>;
};
4. 使用 <Route index>
代替 <Route exact>
来匹配主路由
v5 中
<Route exact>
是用来做”完全匹配“的,同时也可以用来确定主路由的渲染结果
v6 中 不再有 exact 这个 props, 每个 Route 的 path 都是完全匹配的,也就是说,/app/abc 不会匹配到
<Route path="/app" />
除非这个 Route 是一个嵌套的结构或者,path="/app/*"
v6 中的
<Route index>
用来表示在嵌套Route 结构中,匹配到主路由时的渲染
<Routes>
{/* /app1/abc 不会匹配到下边的 Route */}
<Route path="app1" element={<App />} />
{/* /app2/abc 可以匹配到下边的 Route, 因为它的 path 以 * 结尾 */}
<Route path="app2/*" element={<App />} />
{/* /app3/abc 可以匹配到下边的 Route,因为它是一个嵌套Route的上层 */}
<Route path="app3">
{/* index 用来匹配 /app3 时的渲染 */}
<Route index element={<App />} />
{/* :id 用来匹配 /app3/:id 时的渲染 */}
<Route path=":id" element={<div>in ID </div>} />
</Route>
</Routes>
5. <Route>
的嵌套使用
v5 中
<Route>
是不能直接嵌套的,只能通过在上层 Route 的 render 方法中再次借助 Switch 来实现
<Switch>
<Route
path="/app"
render={() => (
{/* 在 render 下再次使用 Switch 代码上看着比较繁琐 */}
<Switch>
<Route path="/app/:id" render={() => <Detail />} />
<Route path="/app/:id/settings" render={() => <Settings />} />
</Switch>
)}
/>
</Switch>
v6 中
<Route>
可以直接嵌套,例如如上路由可以直接这样写
<Routes>
<Route path="/app/:id" >
<Route index element={<Detail />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
还是上边的例子,如果 /app/:id/detail 与 /app/:id/settings 有相同的布局只是内容区域有一些不同,我不想重复的在
<Detail />
<Settings />
中实现这个布局,那么我们可以借助<Outlet/>
组件来实现,它的使用类似 Vue 中的插槽,决定了<Route>
组件 Children 的渲染位置
// 给上层路由追加 element={<App />} 用来渲染 /app/:id/detail /app/:id/settings 共同的部分
<Routes>
<Route path="/app/:id" element={<App />}>
<Route index element={<Detail />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
// App 组件的实现中
import { Link, Outlet } from 'react-router-dom';
const App = () => {
return (
<div>
App instance Page
<Link to="settings">to App Settings Page</Link>
<div style={{ border: '1px solid blue', width: 80, height: 80 }}>
{/* 决定 <Detail /> 或者 <Settings /> 渲染的位置 */}
<Outlet />
</div>
</div>
);
};
使用
<Outlet/>
虽然可以实现类似 Vue 插槽的渲染功能,但是,如果上层路由渲染结果,对下层路由渲染有影响了改怎么办,这个时候可以使用<Outlet context={} />
context 功能,将上层路由组件的上下文传给下层路由渲染的组件,下层路由渲染的组件在通过 useOutletContext 获得这个上下文
<Route path="/app/:id" element={<App />}>
<Route index element={<Detail />} />
<Route path="settings" element={<Settings />} />
</Route>
// App 组件中
import { Outlet } from 'react-router-dom';
const App = () => {
const [count, setCount] = React.useState(0);
return (
<div>
App instance Page
<button onClick={() => setCount(count + 1)}>increase count</button>
<div style={{ border: '1px solid blue', width: 80, height: 80 }}>
{/* 传入 context 上下文 */}
<Outlet context={{ count, setCount }} />
</div>
</div>
);
};
// Detail 组件中
const Detail = () => {
const { id } = useParams();
// useOutletContext 用来获得上级 Route 传入的上下文
const { count } = useOutletContext<{ count: number }>();
return (
<div>
In Detail Page Id: {id}
Count: {count}
</div>
);
};
// 注意 Route 的 context 只能一层一层传递
<Route path="/app/:id" element={<App />}>
<Route index element={<Detail />} />
<Route path="settings" element={<Settings />}>
{/* /app/:id/settings/info 只能拿 Settings 组件的上下文,即使 /app/:id/settings 不渲染组件,也不能获得 App 的 context */}
<Route path="info" element={<Info />} />
</Route>
</Route>
除了上述方案,向 v5 版本中在 render 出的组件中再次进行路由分配也是可行的
<Routes>
{/* 注意,一定要在尾部加上 * 来匹配后边的路径 */}
<Route path="app/:id/*" element={<App />} />
</Routes>
// App 组件中
import { Link, Route, Routes } from 'react-router-dom';
import Detail from './Detail';
import Settings from './Settings';
const App = () => {
return (
<div className={RouteLess.login}>
App instance Page
<Link to="settings">to App Settings Page</Link>
<div style={{ border: '1px solid blue', width: 80, height: 80 }}>
{/* 再次进行路由分发 */}
<Routes>
<Route index element={<Detail />} />
<Route path="settings" element={<Settings />} />
</Routes>
</div>
</div>
);
};
6. <Route path="abc">
上的路径
绝对路径的写法还是可以用的,但是会有一些功能的阉割,相对的 v6 增加了相对路径的写法
Route path 的写法与 v5 相比,只保留了
:id
样式参数与*
通配符;💥不再支持正则以及可选参数 原因是 v6 版本增加了 Route 的”最佳匹配“ 能力,如果是正则或者 可选参数则不容易进行计算;此外*
通配符也只支持写在末尾
- ✅ path = ‘/users/:id’
- ✅ path = ‘/files/*’
- ✅ path = ‘/files/:id/*’
- ✅ path = ‘/files-*’
- ❎ path = ‘/users/:id?’ // 不支持可选参数
- ❎ path = ‘/tweets/:id(\d+)’ // 不支持正则
- ❎ path = ‘/files/*/cat.jpg’// 通配符不能放中间
注意如果 Route 下嵌套了子级路由那么,不需要加 * 通配符,但是如果 Route 下没有直接嵌套子级路由,而是在 element 传入的组件中有子级路由,那么久需要加上 * 通配符
💥不支持可选参数 /:id? 是一个影响比较大的改动,很多旧项目对这个特性都是严重依赖,目前 github 有一个相关的讨论 , 讨论结果上来说,这是react-router官方有意为之的,短期内并没有打算把这个功能加回来,因此,可选参数的重度使用者,是不建议升级到 react-router v6 的。
我们举一个例子,我有如下多个路由,匹配到这些路由后都需要渲染同一个页面
- /app/:id
- /app/protocol/:id
- /app/preInt/:id
如果在 v5 版本中,可以借助可选参数配置在一个 Route 上
<Route
path="/app/:type?/:id"
render={({ match }) => {
const { type } = match.params;
if (type && !['protocol', 'preInt'].includes(type)) return <Redirect to="/notFound" />;
return <App/>;
}}
/>
但是在 v6 版本中, 需要列举出所有要匹配的Route 并且传入相同的 element
<Route path="app">
<Route path=":id" element={<App />} />
<Route path="protocol/:id" element={<App />} />
<Route path="preInt/:id" element={<App />} />
</Route>
上边是一个简单的例子,只有一个可选参数,如果有很多个可选参数,事情会变的更加复杂,例如如下这个 v5 版本中的路由要改写成 v6 版本会变得很麻烦
// v5
<Route
path="/:id?/:view?/:id2?/:view2?/:id3?/:view3?/:id4?/:view4?"
render={() => <ReturnedComponent someParams={someParams} />}
/>
针对这个问题 react-router v6 可以有如下三个妥协的方案,可以满足大部分的场景,如果这三个方案没法满足,那只能考虑不升级到 v6 或者 切换到 wouter 它的api 接近 react-router v5 并且更加轻量
- 借助 Route 的嵌套能力,将多个路由公共的部分放在顶层 Route 中实现,适用于原本可选参数不是很多的场景
// 如果不想其他 app 下的路由也显示 App, 则需要在 App 内进行路由判断
<Route path="app" element={<App />}>
<Route path=":id" />
<Route path="protocol/:id" element={<Protocol />}/>
<Route path="preInt/:id" element={<PreInt />}/>
</Route>
// App 组件的实现中
import { Link, Outlet } from 'react-router-dom';
const App = () => {
return (
<div>
App instance Page
<Outlet />
</div>
);
};
- 使用 查询参数(search) 代替可选参数,来处理可能存在可能不存在的参数,适用于原本有大量可选参数的场景,且可选参数使用 search 代替后不会有理念的问题(可选参数拼接的 path 相比 带有search 的 URL 更加干净、容易缓存、适合SEO、容易理解)
// 只提供一个路由
<Route path="app" element={<App />} />
// 请求时 /app?id=id-abc&type=protocol
// App 组件实现
import { useSearchParams } from 'react-router-dom';
const App = () => {
const [searchParams, setSearchParams] = useSearchParams();
return (
<div className={RouteLess.login}>
App instance Page
<div style={{ border: '1px solid blue', width: 80, height: 80 }}>
{JSON.stringify({id: searchParams.get('id'), type: searchParams.get('type')})}
</div>
</div>
);
};
- 只定义一个带有通配符的路由,其他的路径数据自己解析
<Route path="app/*" element={<App />} />
// App 组件中解析 location.pathname 获得相关数据
const App = () => {
const location = useLocation();
return <div>App</div>;
};
7. <Link to="abc">
的变动
- v6 中 Link to 的地址也可以是相对的,例如:to="/abc" to=“abc” to="…/abc"
- state=string 参数用来变更 location state
- target=_blank 可以打开新窗口跳转
- replace=boolean 可以设置当前调转是 push 或者 replace
<Link to="settings" target="_blank">to</Link>
<Link to="settings" replace>to</Link>
<Link to="settings" state={JSON.stringify({ sss: 'sss' })}>to</Link>
Link 的相对路径写法会有一些意想不到的结果
// 假如 Route 路由结构如下
<Route path="/app/:id" element={<App />}>
<Route path="settings" element={<Settings />}>
<Route path="info" element={<Info />} />
</Route>
<Route path="settings/config" element={<Config />} />
</Route>
// 在 Info 组件 中通过 Link 做跳转
const Info = () => {
return (
<div>
In Info Page
{/* /app/id-abc/settings/config 绝对路径跳转 */}
<Link to="/app/id-abc/settings/config">/app/id-abc/settings/config</Link>
{/* /app/id-abc/settings/info/config 相对路径跳转会在当前Link所在 Route 后边直接 + /config */}
<Link to="config">to config Page</Link>
{/* /app/id-abc/settings/info/config 同上相对路径跳转 */}
<Link to="./config">to ./config Page</Link>
{/* /app/id-abc/settings/config 同样是相对路径跳转,v6 Link 的跳转增加了类似 cd ../ 文件结构的跳转方式,如下 Link 就会跳转到当前Route 的上层 Route 地址(/app/id-abc/settings) 并 + /config */}
<Link to="../config">to ../config Page</Link>
</div>
);
};
// 在 Config 组件中通过Link 跳转,绝对路径与普通相对路径与 Info 组件中的行为一致,但是 ../ 这种相对路径的跳转就不一样了,因为 Config 组件所在 Route 的上层 Route 地址是 /app/:id, 所以 <Link to="../info"> 的跳转结果是 ’/app/:id‘ + ’/info‘ => /app/:id/info
const Config = () => {
return (
<div style={{ width: 200, height: 200 }}>
In Config Page
{/* /app/id-abc/info */}
<Link to="../info">to ../info Page</Link>
</div>
);
};
8. 没有了 <Redirect/>
组件,可以借助 <Route path="*" element={<Navigate />}/>
实现同样的功能
v5 中
<Redirect/>
通常用来在,没有Route 匹配的情况下重定向倒某个落地页,v6 中可以使用<Route path="*" />
来收集未匹配的路由
// v5
<Switch>
<Route exact path="/app" render={() => <App />} />
<Route path="/logs" render={() => {
return (
<Switch>
<Route exact path="/logs" render={() => <Logs />} />
<Route exact path="/logs/:id" render={() => <Logs />} />
<Redirect to="/notFound2" />
</Switch>
);
}} />
<Redirect to="/notFound" />
</Switch>
// v6
<Routes>
<Route path="/app/:id" element={<App />}>
<Route path="settings" element={<Settings />}>
<Route path="info" element={<Info />} />
{/* 嵌套的路由不需要重负处理未匹配情况 */}
</Route>
</Route>
{/* 未匹配的路由会进入这里后 */}
<Route path="*" element={<div>not found-1</div>} />
{/* 未匹配的路由会进入这里后, 重定向到 /notFound */}
<Route path="*" element={<Navigate to="/notFound" />} />
</Routes>
// v6 如果路由不是嵌套的,而是在路由指向的组件中再次分发,那么还是需要再次处理未分配情况的
<Routes>
<Route path="/app/:id" element={<App />}>
<Route path="settings" element={<Settings />} />
</Route>
{/* 处理这一层的未匹配的路由, 重定向到 /notFound */}
<Route path="*" element={<Navigate to="/notFound" />} />
</Routes>
// Settings 组件中
const Settings = () => {
return (
<div>
In Settings Page Id: {id} {location.state} ==
<Routes>
<Route path="info" element={<Info />} />
{/* 处理这一层的未匹配的路由, 重定向到 /notFound */}
<Route path="*" element={<Navigate to="/notFound" />} />
</Routes>
</div>
);
};
9. 取消了 history 相关 api 改为使用 navigate 和 <Navigate/>
navigate 是一个路由跳转的工具,它可以通过
const navigate = useNavigate()
获得(不提供 类组件的获取方式,需要自己封装, 或者直接使用<Navigate/>
),
// navigate 跳转相对路径的规则与 <Link to> 一样
const navigate = useNavigate();
navigate('settings'); // 类似 history.push
navigate('settings', { replace: true }); 类似 history.replace
navigate(-1); // 类似于 history.go
// Navigate 是 useNavigate() 上的一层组件化封装,可以用来在类组件中辅助路由跳转
<div>
App instance Page
{count > 0 ? <Navigate to="settings" replace /> : null}
</div>
10. 取消了 withRouter
类组件如果想要获得 location 或者 navigate 需要自己封装组件
11. 其他新增的 Api
- matchPath 将一个路径模式与一段 URL 匹配,并返回匹配结果
const location = useLocation();
const { pathname, params, pattern } = matchPath(
{ path: '/route/app/:id/:type' },
location.pathname,
);
- useNavigationType 获得用户是如何进入当前页面的 “POP” | “PUSH” | “REPLACE”;
- useParams 获得当前的路径参数信息
- useSearchParams 用于读取或者更改当前的 URL 的 search 部分
- useRoutes 用来代替 Route 组件,通过一个配置文件来生成路由系统
实用工具封装
综上可以看到 react-router v6 提供的Api 都是针对 Hooks 的,对类组件不是很友好,为了更方便的在类组件中使用,我们可以进行一些封装
封装类似 v5 中 withRouter 的包装方法
withRouter 用来给组件注入 navigate location params 数据
import React from 'react';
import { useNavigate, useParams, useLocation } from 'react-router-dom';
import type { NavigateFunction, Location, Params } from 'react-router-dom';
export interface IRouterProps = {
navigate: NavigateFunction;
location: Location;
params: Params;
};
export function withRouter<P extends IRouterProps>(
Component: React.ComponentType<Omit<P, keyof IRouterProps> & IRouterProps>,
): React.ComponentType<Omit<P, keyof IRouterProps>> {
// 如果要保留类组件的 static 方法可以考虑使用 ’hoist-non-react-statics‘ 处理一下
return (props) => (
<Component {...props} params={useParams()} location={useLocation()} navigate={useNavigate()} />
);
}
// 使用时
interface IProps extends IRouterProps {}
class Comp extends React.PureComponents<IProps> {
render() {
const {navigate, location, params} = this.props;
}
}
export default withRouter(Comp);