0
点赞
收藏
分享

微信扫一扫

升级到 React Router v6

全栈顾问 2022-01-04 阅读 80

说明

  • React Router 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> 进行路由匹配

  • 下层 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> 来匹配主路由

<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> 的嵌套使用

<Switch>
  <Route
    path="/app"
    render={() => (
      {/* 在 render 下再次使用 Switch 代码上看着比较繁琐 */}
      <Switch>
        <Route path="/app/:id" render={() => <Detail />} />
        <Route path="/app/:id/settings" render={() => <Settings />} />
      </Switch>
    )}
  />
</Switch>
<Routes>
  <Route path="/app/:id" >
    <Route index element={<Detail />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>

// 给上层路由追加 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>
  );
};
<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>
<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"> 上的路径

  • ✅ path = ‘/users/:id’
  • ✅ path = ‘/files/*’
  • ✅ path = ‘/files/:id/*’
  • ✅ path = ‘/files-*’
  • ❎ path = ‘/users/:id?’ // 不支持可选参数
  • ❎ path = ‘/tweets/:id(\d+)’ // 不支持正则
  • ❎ path = ‘/files/*/cat.jpg’// 通配符不能放中间
  • /app/:id
  • /app/protocol/:id
  • /app/preInt/:id
<Route
  path="/app/:type?/:id"
  render={({ match }) => {
    const { type } = match.params;
    if (type && !['protocol', 'preInt'].includes(type)) return <Redirect to="/notFound" />;
    return <App/>;
  }}
/>
<Route path="app">
  <Route path=":id" element={<App />} />
  <Route path="protocol/:id" element={<App />} />
  <Route path="preInt/:id" element={<App />} />
</Route>
// v5
<Route
  path="/:id?/:view?/:id2?/:view2?/:id3?/:view3?/:id4?/:view4?"
  render={() => <ReturnedComponent someParams={someParams}  />}
/>
  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>
// 假如 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
<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 跳转相对路径的规则与 <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

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 组件,通过一个配置文件来生成路由系统

实用工具封装

封装类似 v5 中 withRouter 的包装方法

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 条评论