React Router v6新特性简介及迁移

掘金地址:https://juejin.cn/post/7053031843376922660
如需转载,请注明出处

简介

React Router库包含三个不同的npm包,以下每个包都有不同的用途。

  • react-router :核心库,包含 React Router 的大部分核心功能,包括路由匹配算法和大部分核心组件和钩子。
  • react-router-dom:React应用中用于路由的软件包,包括react-router的所有内容,并添加了一些特定于 DOM 的 API,包括BrowserRouterHashRouterLink
  • react-router-native: 用于开发React Native应用,包括react-router的所有内容,并添加了一些特定于 React Native 的 API,包括NativeRouterLink

版本

以下v5从5.1.2所有版本都列出了。v6的还有很多alpha和beta小版本没有列出。

新项目目前用了6.0.2,之前旧项目用的是5.1.2。

react-router-dom npm地址

版本 下载量 发布时间(对比日期2022.1.14)
6.2.1 818,595 1个月前
6.0.2 464,961 2个月前
6.0.0 5,540 2个月前
5.3.0 1,612,985 4个月前
5.2.1 68,038 5个月前
6.0.0-beta.0 62,966 2年前
5.2.0 1,734,184 2年前
6.0.0-alpha.0 7 2年前
5.1.2 462,691 2年前

相比v5,打包后的包大小,从20.8k 减少到 10.8k。包分析网站

image.png
image.png

安装

// npm 安装
npm install react-router-dom@6

// yarn 安装
yarn add react-router-dom@6

启用全局路由模式

全局路由有常用两种路由模式可选:HashRouter 和 BrowserRouter 分别是hash模式和history模式。

采用BrowserRouter:

import * as React from "react";
import * as ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";

ReactDOM.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

App.jsx 设置路由

import './App.css';
import { Routes, Route, Link } from "react-router-dom"
function App() {
  return (
    <div className="App">
      <header className="App-header">
        <Routes>
          <Route path="/" element={<Home />}></Route>
          <Route path="/about" element={<About />}></Route>
        </Routes>
      </header>
    </div>
  );
}
function Home() {
  return <div>
    <main>
      <h2>Welcome to the homepage</h2>
    </main>
    <nav>
      <Link to="/about">about</Link>
    </nav>
  </div>
}
function About() {
  return <div>
    <main>
      <h2>Welcome to the about page</h2>
    </main>
    <nav>
      <ol>
        <Link to="/">home</Link>
        <Link to="/about">about</Link>
      </ol>
 
    </nav>
  </div>
}
export default App;

写在前面

如果您刚刚开始使用 React Router,或者您想在新应用中试用 v6,请参阅入门指南

以下内容主要叙述的是与React Router v5版本对比的新特性及如何进行快速迁移。

升级前准备

1. 升级到React v16.8

React Router v6 大量使用React hooks,因此在尝试升级到 React Router v6 之前,您需要使用 React 16.8 或更高版本。好消息是 React Router v5 与 React >= 15 兼容,因此如果您使用的是 v5(或 v4),您应该能够在不接触任何路由器代码的情况下升级 React。

2. 升级到React Router v5.1(非必要)

如果您先升级到 v5.1,则切换到 React Router v6 会更容易。因为在 v5.1 中,发布了对元素处理的增强,<Route children>这将有助于平滑过渡到 v6。不要使用<Route component>和<Route render>props,而是在任何地方使用常规元素<Route children>并使用钩子来访问路由器的内部状态。

升级到 React Router v6

  • <Switch>重命名为<Routes>。
  • <Route>的新特性变更。
  • 嵌套路由变得更简单。
  • 用useNavigate代替useHistory。
  • 新钩子useRoutes代替react-router-config。

1. <Switch>重命名为<Routes>

React Router v6 引入了一个Routes的组件,类似于以前的Switch,但功能更强大。它包括相对路由和链接、自动路由排名、嵌套路由和布局等功能。

// v5
<Switch>
    <Route exact path="/"><Home /></Route>
    <Route path="/profile"><Profile /></Route>
</Switch>
 
 
// v6
<Routes>
    <Route path="/" element={<Home />} />
    <Route path="profile/*" element={<Profile />} />
</Routes>

2.<Route>的新特性变更

Route负责渲染React组件的UI。在v6中会用到两个属性:

  • path:与当前页面对应的URL匹配。
  • element:这个是新增的,用于决定路由匹配时,渲染哪个组件。在v5的时候,我们通常会用到component这个属性,或者是render。

简单来说,component/render被element替代

// v5
<Route path=":userId" component={Profile} />
<Route
  path=":userId"
  render={routeProps => (
    <Profile routeProps={routeProps} animate={true} />
  )}
/>

// v6
<Route path=":userId" element={<Profile />} />
<Route path=":userId" element={<Profile animate={true} />} />

3. 嵌套路由更简单

嵌套路由在实际应用中很常见,如以下两个页面,User这个组件是共用的,切换路由的时候,仅改变Profile和Posts子组件。

image.png

在React Router v5中,必须明确定义嵌套路由,React Router v6并非如此。它从React Router库中挑选了一个名为 Outlet 的最佳元素,为特定路由呈现任何匹配的子元素。用起来和Vue Router里的<router-view>差不多。

3.1 Outlet实现嵌套路由

  • v5中实现嵌套路由
// v5
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 { path, url } = useRouteMatch();

  return (
    <div>
      <nav>
        <Link to={`${url}/me`}>My Profile</Link>
      </nav>

      <Switch>
        <Route path={`${path}/me`}>
          <MyProfile />
        </Route>
        <Route path={`${path}/:id`}>
          <OthersProfile />
        </Route>
      </Switch>
    </div>
  );
}
  • v6中实现嵌套路由,可以删除字符串匹配逻辑。不需要任何useRouteMatch()。利用新API:Outlet( 利用下面第5点说到的useRoutes,可以进一步简化。
import { Outlet } from 'react-router-dom';
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>
      {/*将直接根据上面定义的不同路由参数,渲染<MyProfile />或<OthersProfile /> */}
      <Outlet />
    </div>
  )
}

3.2 多个<Routes />

以前,我们只能 在React App中使用一个 Routes。但是现在我们可以在React App中使用多个路由,这将帮助我们基于不同的路由管理多个应用程序逻辑。

import React from 'react';
import { Routes, Route } from 'react-router-dom';

function Dashboard() {
  return (
    <div>
      <p>Look, more routes!</p>
      <Routes>
        <Route path="/" element={<DashboardGraphs />} />
        <Route path="invoices" element={<InvoiceList />} />
      </Routes>
    </div>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="dashboard/*" element={<Dashboard />} />
    </Routes>
  );
}

4. 用useNavigate代替useHistory

从一目了然改到双目失明。。。

// v5
import { useHistory } from 'react-router-dom';

function MyButton() {
  let history = useHistory();
  function handleClick() {
    history.push('/home');
  };
  return <button onClick={handleClick}>Submit</button>;
};

现在,history.push()将替换为navigation()

// v6
import { useNavigate } from 'react-router-dom';

function MyButton() {
  let navigate = useNavigate();
  function handleClick() {
    navigate('/home');
  };
  return <button onClick={handleClick}>Submit</button>;
};

history的用法也将被替换成:

// v5
history.push('/home');
history.replace('/home');

// v6
navigate('/home');
navigate('/home', {replace: true});

5. 用新钩子useRoutes代替react-router-config

这个hooks用完,比上面直接写router组件的jsx文件要简洁很多

function App() {
  let element = useRoutes([
    { path: '/', element: <Home /> },
    { path: 'dashboard', element: <Dashboard /> },
    { path: 'invoices',
      element: <Invoices />,
      children: [
        { path: ':id', element: <Invoice /> },
        { path: 'sent', element: <SentInvoices /> }
      ]
    },
    // 404找不到
    { path: '*', element: <NotFound /> }
  ]);
  return element;
}

项目实践

以下例子都基于useRoutes

tips:例子中fullPath这个属性是自己定义的非官方属性,因为嵌套路由的时候,path只需要写相对的路径。为了方便直接看到包含父路径的完整路径,所以定义这个用于查看,实际React Router并不会用到这个属性。

1. 懒加载

import { useRoutes } from 'react-router-dom'

// 懒加载
const lazyLoad = (path: string) => {
    const Comp = React.lazy(() => import(`@page/${path}`))
    return (
        <React.Suspense fallback={<>加载中...</>}>
            <Comp />
        </React.Suspense>
    )
}

const routers = [
    {
        name: '项目',
        path: '/',
        element: <Backbone />,
        children: [
            {
                name: '首页',
                path: 'index/*',
                // 这个属性实际路由渲染不会用到。
                fullPath: '/index',
                element: lazyLoad('IndexPage')
            }
        ]
]

const Index = () => useRoutes(routers)

export default Index

2. Index路由:默认子路由

用于嵌套路由,仅匹配父路径时,设置渲染的组件。 解决当嵌套路由有多个子路由但本身无法确认默认渲染哪个子路由的时候,可以增加index属性来指定默认路由。

index路由和其他路由不同的地方是它没有path属性,他和父路由共享同一个路径。

例如: 以下路由设置了两个子路由,分别是/foo/invoices 和 /foo/activity 。但是当页面直接访问 /foo的时候,页面就不知道怎么渲染了。这时候可以通过index设置默认展示到invoices。

const routers = [
    {
        name: '项目',
        path: '/foo',
        element: <Layout />,
        children: [
            // 仅匹配到父级路由时, 设置index为true,不需要指定path
            { index: true, element: lazyLoad('Invoices') },
            {
                name: '首页',
                path: 'invoices',
                element: lazyLoad('Invoices')
            },
            {
                    name: 'about',
                path: 'activity',
                element: lazyLoad('Activity')
            }
        ]
   }
]

3. 404

废弃了V5中的Redirect

  // v5 废弃了
   const routers = [
        { path: 'home', redirectTo: '/' }
  ]
  
  // 404可以这么写
  const routers = [
     {
          name: '404',
          path: '*',
          element: <NoMatch />
      }
   ]

4. 动态路由

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如新闻详情页,对应不同新闻,都对应到News组件,路径可能为 news/1,news/2...。其中数字1为新闻的id。这个是动态变化。

  • 动态路径参数 以冒号开头
const routers = [
        {
        name: '公告详情页',
        // 动态路径参数 以冒号开头
        path: 'noticeDetail/:id',
        fullPath: '/noticeDetail',
        element: lazyLoad('NoticeDetail')
    },
    {
        name: '帮助中心详情页',
        path: 'helpCenterDetail/:fid/:sid',
        fullPath: '/helpCenterDetail',
        element: lazyLoad('HelpCenterDetail')
    }
]
  • useParams 用于组件获取动态路径的参数

    例如上面这个例子,有两个动态参数fid和sid,在HelpCenterDetail组件:

import { useParams } from 'react-router-dom'
const { fid, sid } = useParams()

5. 路由通配符

支持以下几种通配符号。注意:这里的*只能用在/后面,不能用在实际路径中间

/groups
/groups/admin
/users/:id
/users/:id/messages
/files/*
/files/:id/*

以下这些v6里面不支持

/users/:id?
/tweets/:id(\d+)
/files/*/cat.jpg
/files-*

6. url搜索参数

useSearchParams的使用。

例如 :http://URL/user?id=111,获取id对应的值

import { useSearchParams } from 'react-router-dom'

const [searchParams, setSearchParams] = useSearchParams()

// 获取参数
searchParams.get('id')

// 判断参数是否存在
searchParams.has('id')

// 同时页面内也可以用set方法来改变路由
setSearchParams({"id":2})

7. withRouter

v6不再提供withRouter,略坑。

官方解释:这个问题通常源于您使用的是不支持钩子的 React 类组件。在 React Router v6 中,我们完全接受了钩子并使用它们来共享所有路由器的内部状态。但这并不意味着您不能使用路由器。假设您实际上可以使用钩子(您使用的是 React 16.8+),您只需要一个包装器。

import {
  useLocation,
  useNavigate,
  useParams
} from "react-router-dom";

function withRouter(Component) {
  function ComponentWithRouterProp(props) {
    let location = useLocation();
    let navigate = useNavigate();
    let params = useParams();
    return (
      <Component
        {...props}
        router={{ location, navigate, params }}
      />
    );
  }

  return ComponentWithRouterProp;
}

项目中完整例子

import { useRoutes } from 'react-router-dom'
import Backbone from '../layouts/Backbone'
import InfoPage from '../pages/InfoPage/index'
import DataCenterPage from '../pages/DataCenterPage/index'
import NoMatch from './NoMatch'

// 懒加载
const lazyLoad = (path: string) => {
    const Comp = React.lazy(() => import(`@page/${path}`))
    return (
        <React.Suspense fallback={<>加载中...</>}>
            <Comp />
        </React.Suspense>
    )
}

const routers = [
    {
        name: '项目',
        path: '/',
        element: <Backbone />,
        children: [
            // 仅匹配到父级路由时
            { index: true, fullPath: '/', element: lazyLoad('IndexPage') },
            {
                name: '首页',
                path: 'index/*',
                fullPath: '/index',
                element: lazyLoad('IndexPage')
            },
            {
                name: '消息中心',
                path: 'info/',
                // element: lazyLoad('InfoPage'),
                element: <InfoPage />,
                children: [
                    { index: true, fullPath: '/info', element: lazyLoad('NoticeList') },
                    {
                        name: '公告',
                        path: 'noticeList',
                        fullPath: '/info/noticeList',
                        element: lazyLoad('NoticeList')
                    },
                    {
                        name: '帮助中心',
                        path: 'helpCenter',
                        fullPath: '/info/helpCenter',
                        element: lazyLoad('HelpCenter')
                    }
                ]
            },
            {
                name: '我要推广',
                path: 'activityList/*',
                fullPath: '/activityList',
                element: lazyLoad('ActivityList')
            },
            {
                name: '数据中心',
                path: 'data/',
                // element: lazyLoad('InfoPage'),
                element: <DataCenterPage />,
                children: [
                    { index: true, fullPath: '/data', element: lazyLoad('Order') },
                    {
                        name: '订单查询',
                        path: 'order',
                        fullPath: '/data/order',
                        element: lazyLoad('Order')
                    },
                    {
                        name: '收益查询',
                        path: 'reward',
                        fullPath: '/data/reward',
                        element: lazyLoad('Reward')
                    },
                    {
                        name: '结算查询',
                        path: 'settlement',
                        fullPath: '/data/settlement',
                        element: lazyLoad('Settlement')
                    }
                ]
            }
        ]
    },
    {
        name: '公告详情页',
        // 动态路由
        path: 'noticeDetail/:id',
        fullPath: '/noticeDetail',
        element: lazyLoad('NoticeDetail')
    },
    {
        name: '帮助中心详情页',
        path: 'helpCenterDetail/:fid/:sid',
        fullPath: '/helpCenterDetail',
        element: lazyLoad('HelpCenterDetail')
    },
    {
        name: '个人注册页',
        path: 'register/person',
        fullPath: '/register/person',
        element: lazyLoad('PersonBaseInfo')
    },
    {
        name: '404',
        path: '*',
        element: <NoMatch />
    }
]

const Index = () => useRoutes(routers)

export default Index

常用路由组件和hooks

完整API: https://reactrouter.com/docs/en/v6/api

组件名 作用 说明
<Routers> 一组路由 代替原有<Switch>,所有子路由都用基础的Router children来表示
<Router> 基础路由 Router是可以嵌套的,解决原有V5中严格模式
<Link> 导航组件 在实际页面中跳转使用
<Outlet/> 自适应渲染组件 根据实际路由url自动选择组件,主要用于嵌套路由,类似Vue Router中的<router-view>
hooks名 作用 说明
useParams 返回当前参数 根据路径读取参数
useNavigate 返回当前路由 代替原有V5中的 useHistory
useOutlet 返回根据路由生成的element
useLocation 返回当前的location 对象
useRoutes 同Routers组件一样,只不过是在js中使用 代替react-router-config
useSearchParams 用来匹配URL中?后面的搜索参数

参考文献

React Router网址: React Router

React Router文档: https://reactrouter.com/docs/en/v6

完整API: https://reactrouter.com/docs/en/v6/api

react-router-dom: npm地址

参考博客:https://blog.csdn.net/weixin_40906515/article/details/104957712

https://zhuanlan.zhihu.com/p/191419879

//www.greatytc.com/p/03234215a90e

https://blog.csdn.net/zjjcchina/article/details/121921585

包大小分析网站: https://bundlephobia.com/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,653评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,321评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,833评论 0 324
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,472评论 1 266
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,306评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,274评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,658评论 3 385
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,335评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,638评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,697评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,454评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,311评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,699评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,986评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,254评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,647评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,847评论 2 335

推荐阅读更多精彩内容