升级到 React Router v6

说明

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 并且更加轻量

  1. 借助 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>
  );
};
  1. 使用 查询参数(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>
  );
};
  1. 只定义一个带有通配符的路由,其他的路径数据自己解析
<Route path="app/*" element={<App />} />

// App 组件中解析 location.pathname 获得相关数据
const App = () => {
  const location = useLocation();
  return <div>App</div>;
};
7. <Link to="abc"> 的变动
  1. v6 中 Link to 的地址也可以是相对的,例如:to="/abc" to=“abc” to="…/abc"
  2. state=string 参数用来变更 location state
  3. target=_blank 可以打开新窗口跳转
  4. 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
  1. matchPath 将一个路径模式与一段 URL 匹配,并返回匹配结果
const location = useLocation();
const { pathname, params, pattern } = matchPath(
  { path: '/route/app/:id/:type' },
  location.pathname,
);
  1. useNavigationType 获得用户是如何进入当前页面的 “POP” | “PUSH” | “REPLACE”;
  2. useParams 获得当前的路径参数信息
  3. useSearchParams 用于读取或者更改当前的 URL 的 search 部分
  4. 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);
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值