一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

(1万字)[” 干中之干系列2 “] 手把手带你玩转 React 中路由那点事!!

” 一觉醒来又有BUG!” —— 大家好!我是周一觉

当代前端开发大部分都以SPA单页面应用开发为主,像各类管理系统、移动端WebApp「或App」等等,而我们前端的路由机制,就是打造SPA单页面应用的最好方案。
所以在学习React中的路由机制之前,我们先回顾下原生JS中是如何实现路由跳转的。

扩展:SPA和MPA的对比

一、JavaScript中的路由设计模式

1、哈希路由(Hash路由)

Hash路由是指使用 URL 中的 hash(即#后面的部分)来管理页面的路由。
在这种方式下,当URL的hash发生变化时,页面不会重新加载,
而是会通过 JavaScript 监听hashchange事件来改变页面的内容,从而实现页面的路由。

写一个简单的Demo

//html部分
    <nav class="nav-box">
        <a href="#/">首页</a>
        <a href="#/product">产品中心</a>
        <a href="#/personal">个人中心</a>
    </nav>
    //view-box中负责展示具体内容
    <div class="view-box"></div>
//script部分
   // 获取渲染内容的容器
        const viewBox = document.querySelector('.view-box');
        /*
        构建一个简单路由匹配表:每当我们重新加载页面、或者路由切换(切换哈希值),
        都先到这个路由表中进行匹配;根据当前页面的哈希值,匹配出要渲染的内容(组件)!!
        */
        const routes = [{
            path: '/',
            component: '首页的内容'
        }, {
            path: '/product',
            component: '产品中心的内容'
        }, {
            path: '/personal',
            component: '个人中心的内容'
        }];

        // 路由匹配的办法
        const routerMatch = function routerMatch() {
            let hash = location.hash.substring(1),
                text = "";
            routes.forEach(item => {
                if (item.path === hash) {
                    text = item.component;
                }
            });
            viewBox.innerHTML = text;
        };

        // 一进来要展示的是首页的信息,所以默认改变一下HASH值
        location.hash = '/';
        routerMatch();

        // 监测HASH值的变化,重新进行路由匹配
        window.onhashchange = routerMatch;

原理:每一次路由跳转,都是改变页面的hash值,并且监听hashchange事件,来渲染不同的内容。

2、浏览器路由(History路由)

浏览器路由则是使用 HTML5 中的 History API来实现的。在这种方式下,
当URL的路径发生变化时,浏览器会发送请求给服务器来获取新的页面内容,然后通过JavaScript更新页面内容,从而实现页面的路由。

继续写一个简单的Demo

//html部分
    <nav class="nav-box">
        <a href="/">首页</a>
        <a href="/product">产品中心</a>
        <a href="/personal">个人中心</a>
    </nav>
    //view-box中负责展示具体内容
    <div class="view-box"></div>
//script部分
   // 获取我们要操作的容器
     const viewBox = document.querySelector('.view-box'),
            navBox = document.querySelector('.nav-box');

        // 点击A标签实现页面地址切换,但是不能刷新页面
        navBox.onclick = function (ev) {
            let target = ev.target;
            if (target.tagName === 'A') {
                ev.preventDefault(); //阻止A标签页面跳转&刷新的默认行为
                history.pushState({}, "", target.href); //执行history的psuhState方法
                // 去执行路由匹配
                routerMatch();
            }
        };

        // 还是构建一个简单的路由表
        const routes = [{
            path: '/',
            component: '首页的内容'
        }, {
            path: '/product',
            component: '产品中心的内容'
        }, {
            path: '/personal',
            component: '个人中心的内容'
        }];
        
        // 路由匹配的办法
        const routerMatch = function routerMatch() {
            let path = location.pathname,
                text = "";
            routes.forEach(item => {
                if (item.path === path) {
                    text = item.component;
                }
            });
            viewBox.innerHTML = text;
        };

        // 默认展示首页
        history.pushState({}, "", "/");
        routerMatch();

        /*
        监听popstate地址变化事件;此事件执行go/forward/back等方法
        (或者点击浏览器前进后退按钮)可以触发,
        但是执行pushState/replaceState等方法无法触发!! 
        */ 
        window.onpopstate = routerMatch;

原理:利用了H5中的HistoryAPI来实现页面地址的切换「可以不刷新页面」。
问题:我们切换的地址,在页面不刷新的情况下是没有问题的,但是如果页面刷新,这个地址是不存在的会报 404 错误,此时我们需要服务器的配合,在地址不存在的情况下,也可以把主页面内容返回。

简单回顾了原生JS中的路由跳转方案,关于Hash路由和History路由中的API方法,大家回去记得要复习一下,这里就不多说了。那么现在我们就正式开启React中的路由解决方案!

二、React中的路由

在 React.js 社区中有一个 React Training 团队,他们致力于为 React 开发者提供最佳实践和工具,以提高 React 应用程序的可维护性和性能。React中的路由方案就是由他们打造的,也就是几乎每一位React开发者都在用的 "react-router-dom"

react-router-dom 目前主要是 v5 和 v6 两个版本,虽然最新的是 v6 但 v5 的使用率仍然很大,所以我们也要一并学习。在 v6 中大量使用React钩子,因此需要在 React16.8 或更高版本上才能尝试升级到 v6 版本。好消息是 React Router v5与React兼容>=15,所以如果你使用的是v5(或更早的v4),也可以在不接触任何路由代码的情况下升级React。

1、react-router-dom v5版本

v5地址v5.reactrouter.com/web/guides/…

安装任选方案
npm install react-router-dom@5.3.4
yarn add react-router-dom@5.3.4
其他 cnpm / pnpm …

(1)基础运用

首先在入口文件中从react-router-dom获取一些常用API

import { HashRouter, BrowserRouter, Route, Switch, Redirect, Link } from 'react-router-dom';
  • HashRouter 是一个路由器组件,它使用 URL 中的哈希值来管理路由。
  • BrowserRouter 是一个路由器组件,它使用 HTML5 的 history API 来管理路由。
  • Route 组件是一个用于渲染某个路由的组件。
  • Switch 组件是一个用于包装多个 Route 组件的组件。
  • Redirect 组件是一个用于重定向用户的组件。
  • Link 组件是一个用于创建超链接的组件。

在 src 目录下新建一个 views 文件夹来存放不同的跳转视图
在 views 中分别创建三个简单的渲染组件,并通过 ES6Module 导出

const A = function A() {
    return <div className="box">
        A组件的内容
    </div>;
};
export default A;

B、C同样方法,这里就不写了
之后在入口文件中导入

/* 导入组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';

因为<BrowserRouter>需后端服务器支持
所以我们基于<HashRouter>把所有要渲染的内容包起来,开启 Hash 路由
后续用到的<Route>、<Link>等,都需要在 HashRouter/BrowserRouter 中使用
开启后整个页面地址,默认会设置一个 #/ 哈希值

const App = function App() {
  return <HashRouter>
    {/* 导航部分 */}
    <nav>
      {/* 
        Link实现路由切换/跳转的组件,最后渲染完毕的结果依然是a标签
        它可以根据路由模式(HashRouter 或 BrowserRouter),自动设定点击a切换的方式(#/ 或 /)
      */}
      <Link to="/">A</Link>
      <Link to="/b">B</Link>
      <Link to="/c">C</Link>
    </nav>
  </HashRouter>;
};
export default App;

在导航部分下面写一个路由容器,每一次页面加载或者路由切换完毕,都会根据当前的哈希值,到这里和每一个Route进行匹配,把匹配到的组件,放在容器中渲染。

  <nav> ... </nav>
  {/* 路由容器部分 */}
  {/* path:路由地址;component:渲染组件地址 */}
  <div className="content">
      <Route path="/"  component={A} />
      <Route path="/b" component={B} />
      <Route path="/c" component={C} />
  </div>

此时分别点击A、B、C
URL:xxx.xx.x/#/ => A组件的内容
URL:xxx.xx.x/#/b => A组件的内容 + B组件的内容
URL:xxx.xx.x/#/c => A组件的内容 + C组件的内容

我们的目的是点击A、B、C,分别呈现出自己的内容
但此时是点击B时匹配A又同时匹配B,点击C时匹配A又同时匹配C

产生这个问题的原因是,涉及到了React中路径地址匹配的规则

路由地址:Route中path字段指定的地址
页面地址:浏览器URL后面的哈希值

页面地址 路由地址 非精准匹配 精准匹配
/ / true true
/ /b false false
/b / true false
/b/c /b true false
/b/c /b/c true true
/b2/c /b/c false false

非精准匹配:
@ 页面地址和路由地址一样,返回true
@ 页面地址中,包含一套完整的路由地址,返回true
@ 剩下返回的都是false
精准匹配:
@ 两个地址只有一模一样才是true,否则false

基于React中的路径地址匹配的规则,我们重写一下路由容器

  <nav> ... </nav>
  {/* 路由容器部分 */}
  <div className="content">
      {/*
         Switch:确保路由中,只要有一项匹配,则不再继续向下匹配
          exact:设置匹配模式为精准匹配
        */}
      <Switch>
        <Route exact path="/"  component={A} />
        <Route path="/b" component={B} />
        <Route path="/c" component={C} />
      </Switch>
  </div>

此时就可以满足点击A、B、C,分别呈现自己的内容

最后我们再来完善一下路由容器
如果都不匹配则跳转到404页面,或者重定向到首页(默认地址)

 <Switch> 
   <Route exact path="/" component={A} /> 
   <Route path="/b" component={B} />
   <Route path="/c" component={C} /> 
    // 放在最后一项,path设置*或者不写,意思是以上都不匹配,则执行这个规则
   <Route path="*" component={"404组件地址"} /> 
 </Switch>
 // 当然也可以不设置404组件,而是重定向到默认 / 地址
  <Switch> 
    <Route exact path="/" component={A} />
    <Route path="/b" component={B} />
    <Route path="/c" component={C} />
     // from从哪个地址来; to重定向的地址; exact是对from地址的修饰来开启精准匹配
    <Redirect from=" " to="/" exact/>
  </Switch>

Route组件中除了用 component 来渲染组件地址,还可以写到 render 方法里

    <Route path="/c" render={() => {
          // 当路由地址匹配后,先把render函数执行,返回的返回值就是我们需要渲染的内容
          // 在此函数中,可以处理一些事情,例如:登录态检验....
          if (isLogin) {
            return <C />;
          }
          return <Redirect to="/login" />
     }} />

(2)多级路由的简单构建

在之前我们做的是
/=>A内容 ;/b=>B内容;/c=>C内容
现在增加了需求
/a=>/A内容,要求A内容中还有三个子路由页面a1、a2、a3,并分别展示自己的内容

首先在 views 文件夹中,单独建立A内容的子路由文件夹 a ,之后分别导出三个页面

const A1 = function A1() {
    return <div className="box">
        A1组件的详细信息
    </div>;
};
export default A1;

A2、A3同样方法,这里就不写了
在入口文件中,我们先去调整一下一级路由:从” / “定向到” /a “

const App = function App() {
  return <HashRouter>
    {/* 导航部分 */}
    <nav>
      <Link to="/a">A</Link>
      <Link to="/b">B</Link>
      <Link to="/c">C</Link>
    </nav>
    
    {/* 路由容器 */}
    <div className="content">
      <Switch>
        <Redirect exact from="/" to="/a" />
        <Route path="/a" component={A} />
        <Route path="/b" component={B} />
        <Route path="/c" component={C} />
        <Redirect to="/a" />
      </Switch>
    </div>
  </HashRouter>;
};

在A组件中

//原A组件
  const A = function A() {
    return <div className="box">
           A组件的内容
         </div>;
  };
  export default A;

先导入我们需要用到其他组件

import { Route, Switch, Redirect, Link } from 'react-router-dom';
import A1 from './a/A1';
import A2 from './a/A2';
import A3 from './a/A3';
//现A组件
  //这个组件就不需要用 HashRouter 或 BrowserRouter包起来了,因为最后都是导入到入口文件中
const A = function A() {
    return <>
        <div className="menu">
            <Link to="/a/a1">A1</Link>
            <Link to="/a/a2">A2</Link>
            <Link to="/a/a3">A3</Link>
        </div>      
       
        <div className="view">
           {/* 
             1.配置二级路由的path,要把一级路由地址带上/a/a1、/a/a2、/a/a3;
             2.从一级路由/a进来,但/a无直接内容匹配,所以重定向到/a/a1 ;
             3.因为/a/a1、/a/a2、/a/a3都和/a匹配,所以在加一个exact来开启精准匹配;
           */}
              <Redirect exact from="/a" to="/a/a1" />
              <Route path="/a/a1" component={A} />
              <Route path="/a/a2" component={B} />
              <Route path="/a/a3" component={C} />
          {/* 
            从一级路由进来后,若是二级路由都不匹配,则会返回一级路由中继续向下匹配 
            所以这里不需要做 404跳转 或 都不匹配重定向
          */}
        </div>
    </>;
};
export default A;

这样一个简单的二级路由就完成了

(3)构建React中的路由表机制

在实际开发中如果有许多渲染页面的话,那么路由我们一个一个来写就会非常麻烦,而且不便于管理,所以通常会打造一个路由表,通过路由表来动态循环处理每一个路由信息。

还是用我们上面写的Demo,在src目录下新建router文件夹
在router文件夹中新建三个js文件分别是 routes.js、aRoutes.js、index.js
routes.js => 处理一级路由信息(构建一级路由表)
aRoutes.js => 处理二级路由信息(构建二级路由表)
index.js => 根据路由表,动态设定路由的匹配规则

先规定一下我们路由表的信息匹配规则
路由表是一个数组,数组中每一项就是每一个需要配置的路由信息

  • redirect:true 此配置是重定向
  • from:来源的地址
  • to:重定向的地址
  • exact:是否精准匹配
  • path:匹配的路径
  • component:渲染的组件
  • name:路由名称(命名路由)
  • children:[ ]子路由
  • 其他(根据需求)

aRoutes 文件中

//导入渲染组件
import A1 from '../views/a/A1';
import A2 from '../views/a/A2';
import A3 from '../views/a/A3';

// A组件的二级路由表
const aRoutes = [{
    redirect: true,
    from: '/a',
    to: '/a/a1',
    exact: true
}, {
    path: '/a/a1',
    name: 'a-a1',
    component: A1,
}, {
    path: '/a/a2',
    name: 'a-a2',
    component: A2,
}, {
    path: '/a/a3',
    name: 'a-a3',
    component: A3,
}];
export default aRoutes;

routes 文件中

//导入A、B、C组件和aRoutes
import A from '../views/A';
import B from '../views/B';
import C from '../views/C';
import aRoutes from './aRoutes';

// 一级路由的理由表
const routes = [{
    redirect: true,
    from: '/',
    to: '/a',
    exact: true
}, {
    path: '/a',
    name: 'a',
    component: A,
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: B,
}, {
    path: '/c',
    name: 'c',
    component: C,
}, {
    redirect: true,
    to: '/a'
}];
export default routes;

index 文件中基于属性传递路由表进来,我们根据路由表,动态设定路由的匹配规则

//导入需要用到的路由组件
import { Switch, Route, Redirect } from 'react-router-dom';
const RouterView = function RouterView(props) {
    // 获取传递的路由表
    let { routes } = props;
    return <Switch>
        {/* 循环设置路由匹配规则 */}
        {routes.map((item, index) => {
            let { redirect, from, to, exact, path, component: Component } = item,
                config = {};
            if (redirect) {
                // 重定向的规则
                config = { to };
                if (from) config.from = from;
                if (exact) config.exact = true;
                return <Redirect key={index} {...config} />;
            }
            // 正常匹配规则
            config = { path };
            if (exact) config.exact = true;
            return <Route key={index} {...config} render={() => {
                // 统一基于RENDER函数处理,当某个路由匹配,后期在这里可以做一些其它事情
                return <Component/>
            }} />;
        })}
    </Switch>;
};
export default RouterView;

接下来我们就可以把入口的路由信息和 A 组件里的路由信息进行改写

在入口文件中

import { HashRouter, Link } from 'react-router-dom';
import RouterView from "./router";
import routes from "./router/routes";

const App = function App() {
    return <HashRouter>
            <nav>
              <Link to="/a">A</Link>
              <Link to="/b">B</Link>
              <Link to="/c">C</Link>
            </nav>
            
        {/* 路由容器 */}
        <div className="content">
            <RouterView routes={routes} />
        </div>
    </HashRouter>;
};

export default App;

在A组件中

import { Link } from 'react-router-dom';
import RouterView from "../router";
import routes from "../router/aRoutes";

const A = function A() {
    return <>
        <div className="menu">
            <Link to="/a/a1">A1</Link>
            <Link to="/a/a2">A2</Link>
            <Link to="/a/a3">A3</Link>
        </div>

        <div className="view">
            <RouterView routes={routes} />
        </div>
    </>;
};
export default A;

到此由 React 路由表打造的多级路由就成功了

(4)路由懒加载

在真实项目中,如果我们事先把所有组件全部导入进来,再基于Route做路由匹配,最后项目打包时所有组件全部打包到一个JS中,在第一次加载页面时,从服务器获取这个JS文件就会花费很久的时间,导致此阶段,页面一直处于白屏的状态。

解决方案就是路由懒加载
借助 React.lacy 函数和ES6中 import 实现

分割打包:每个组件单独打包为一个 JS
按需加载:最开始渲染页面不会加载这些单独的 JS,只有路由匹配成功时再去加载

在 aRoutes文件中

// A组件的二级路由表
import { lazy } from 'react';
const aRoutes = [{
    redirect: true,
    from: '/a',
    to: '/a/a1',
    exact: true
}, {
    path: '/a/a1',
    name: 'a-a1',
    component: lazy(() => import('../views/a/A1')),
}, {
    path: '/a/a2',
    name: 'a-a2',
    component: lazy(() => import('../views/a/A2')),
}, {
    path: '/a/a3',
    name: 'a-a3',
    component: lazy(() => import('../views/a/A3')),
}];
export default aRoutes;

在 routes 文件中

import { lazy } from 'react';
import A from '../views/A';
import aRoutes from './aRoutes';

// 一级路由的理由表
const routes = [{
    redirect: true,
    from: '/',
    to: '/a',
    exact: true
}, {
    path: '/a',
    name: 'a',
    component: A,
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: lazy(() => import('../views/B')),
}, {
    path: '/c',
    name: 'c',
    component: lazy(() => import('../views/C')),
}, {
    redirect: true,
    to: '/a'
}];
export default routes;

路由懒加载处理后,同步加载变为异步按需加载
所以我们还要用React.Suspense方法对<Route>组件进行异步处理

在 index 文件中

import React, { Suspense } from "react";
import { Switch, Route, Redirect } from 'react-router-dom';

/* 调用组件的时候,基于属性传递路由表进来,我们根据路由表,动态设定路由的匹配规则 */
const RouterView = function RouterView(props) {
    // 获取传递的路由表
    let { routes } = props;
    return <Switch>
        {/* 循环设置路由匹配规则 */}
        {routes.map((item, index) => {
            let { redirect, from, to, exact, path, component: Component } = item,
                config = {};
            if (redirect) {
                // 重定向的规则
                config = { to };
                if (from) config.from = from;
                if (exact) config.exact = true;
                return <Redirect key={index} {...config} />;
            }
            // 正常匹配规则
            config = { path };
            if (exact) config.exact = true;
            return <Route key={index} {...config} render={() => {
                // 统一基于RENDER函数处理,当某个路由匹配,后期在这里可以做一些其它事情
                // Suspense.fallback:在异步加载的组件没有处理完成之前,先展示的Loading效果
                return <Suspense fallback={<>正在处理中...</>}>
                    <Component/>
                </Suspense>;
            }} />;
        })}
    </Switch>;
};
export default RouterView;

(5)获取路由对象信息

在 react-router-dom v5 中,基于Route路由匹配渲染的组件,路由会默认给每个组件传递三个属性 history 、location 、match,后期我们可以基于props/this.props获取传递的属性值。

若是通过<Route>的 component 来渲染,则默认会给组件传递这些属性
若是通过<Route>的 render 来渲染,则在render中可以获取传递的属性
但是组件中没有这些属性,此时我们需要自己传递给组件

//在 index 文件中
   return <Route key={index} {...config} render={(props) => {
   // Suspense.fallback:在异步加载的组件没有处理完成之前,先展示的Loading效果
         return <Suspense fallback={<>正在处理中...</>}>
                  <Component {...props} />
               </Suspense>;
         }} />;
//在 B 组件中通过 props 就可以就收到传递过来的三个属性
  const B = function B(props) {
    console.log(props)//=>{history, location, match}
      return <div className="box">
          B组件的内容
      </div>;
  };
  export default B;

在函数组件中除了通过 props 获取这三个属性
还可以通过react-router-dom提供的 Hook 来获取

//在 B 组件中通过 Hook 获取这三个属性
import { useHistory, useLocation, useRouteMatch } from 'react-router-dom';
  const B = function B() {
    let history = useHistory(),
        location = useLocation(),
        match = useRouteMatch();       
      return <div className="box">
          B组件的内容
      </div>;
  };
  export default B;

总结1.0
基于<Route>匹配渲染的组件,我们想获取这三个属性对象
@ 基于props属性获取,适用于函数组件和类组件
@ 基于Hook函数获取,只适用于函数组件

但此时在入口文件中,我们无论基于 props 还是 Hook 都获取不到这三个信息
现在我们把入口文件中的<nav>...</nav>导航部分抽离出来,做单独封装
在src目录下新建 components 文件夹,新建 HomeHead.jsx 文件为导航封装部分

// 入口文件中
import { HashRouter } from 'react-router-dom';
import RouterView from "./router";
import routes from "./router/routes";
import HomeHead from "./components/HomeHead";

const App = function App() {
    return <HashRouter>
        {/* 封装处理 */}
        <HomeHead />

        {/* 路由容器 */}
        <div className="content">
            <RouterView routes={routes} />
        </div>
    </HashRouter>;
};

export default App;
// HomeHead 文件中
import { Link, useHistory } from 'react-router-dom';
const HomeHead = function HomeHead(props) {
    console.log(props);//=> 无
    console.log(useHistory());// => 有
    return <nav>
        <Link to="/a">A</Link>
        <Link to="/b">B</Link>
        <Link to="/c">C</Link>
    </nav>;
};
export default HomeHead; 

此时在 HomeHead 组件中,通过 props 依然无法获取三个属性信息,但通过Hook却可以获取到。

总结2.0
只要在<HashRouter>/<BrowserRouter>中渲染的组件,
我们在组件内部基于useHistory/useLocation/useRouteMatch这些 Hook 函数,
就可以获取history/location/match这些对象信息,
即便这个组件并不是基于<Route>匹配渲染的,
只有基于<Route>匹配渲染的组件,才可以通过props属性获取这三个对象信息。

但是现在又出现一个问题
Hook 只能在函数组件中使用,如果不是基于<Route>匹配渲染的类组件该怎么处理呢?

解决方案
基于函数高阶组件,我们自己来包裹一层进行处理

高阶组件简单来说就是基于闭包的机制,来保存一些信息
我们先把 HomeHead 组件由函数组件变为类组件

class HomeHead extends React.Component {
    render() {
    console.log(this.props)//=>无
        return <nav>
            <Link to="/a">A</Link>
            <Link to="/b">B</Link>
            <Link to="/c">C</Link>
        </nav>;
    }
}

写一个简单的高阶组件来给类组件传递三个属性

 const Handle = function Handle(Component) {
    // Component:真正需要渲染的组件 HomeHead
    // 返回一个高阶组件「导出去供别的地方调用的就是HOC组件」
    return function HOC(props) {
        // props:调用HOC传递的属性,其实这些属性原本是想传递给HomeHead的
        // HOC是个函数组件,我们可以在这里基于Hook函数获取需要的三个对象信息,
        // 然后手动作为属性,传递给HomeHead
        let history = useHistory(),
            location = useLocation(),
            match = useRouteMatch();
        return <Component {...props} history={history} location={location} match={match} />;
    };
};

最后在高阶组件下包裹导出
完整代码如下:

// HomeHead 文件中 
import { Link, useHistory, useLocation, useRouteMatch } from 'react-router-dom';
class HomeHead extends React.Component {
    render() {
    console.log(this.props)//=>有三个属性信息
        return <nav>
            <Link to="/a">A</Link>
            <Link to="/b">B</Link>
            <Link to="/c">C</Link>
        </nav>;
    };
};
const Handle = function Handle(Component) {
   //HOC这个方法名首字母要大写,不然会报错
    return function HOC(props) {
        let history = useHistory(),
            location = useLocation(),
            match = useRouteMatch();
        return <Component {...props} history={history} location={location} match={match} />;
    };
};
export default Handle(HomeHead);

在 react-router-dom v5版本中自带了一个高阶组件 withRouter ,就是用来解决这个问题的

官方解决方案:

// HomeHead 文件中 
import { Link, withRouter} from 'react-router-dom';
class HomeHead extends React.Component {
    render() {
    console.log(this.props)//=>有三个属性信息
        return <nav>
            <Link to="/a">A</Link>
            <Link to="/b">B</Link>
            <Link to="/c">C</Link>
        </nav>;
    };
};
export default withRouter(HomeHead);

总结3.0
所有组件最好都包裹在<HashRouter>/<BrowserRouter>中,只有这样的组件,我们才能在每个组件中,获取history/location/match等对象信息
函数组件,并且是基于<Route>匹配渲染的
+ 基于 Hook 函数获取
+ 基于 props 属性获取,若是render渲染的需要自己处理一下
函数组件,但并不是基于<Route>匹配渲染的
+ 基于Hook函数获取
+ 基于withRouter代理一下这个组件,这样就可以基于props获取了
类组件
只能基于props获取,但是如果其没有被<Route>匹配渲染
则需要基于withRouter代理一下这个组件,就可从this.props中获取

(6)路由跳转和传参方案

路由跳转

方案一:Link/NavLink跳转

<Link to="/xxx">导航</Link>
//跳转地址pathname;问号传参search;隐式传参state
<Link to={{pathname:'/xxx', search:'', state:{}}}>导航</Link>
//replace => to的地址为替换而不是新增
<Link to="/xxx" replace>导航</Link>

方案二:通过useHistory()实现跳转

//histoty => let history = useHistory()
history.push('/c');
//跳转地址pathname;问号传参search;隐式传参state
history.push({pathname: '/c', search: '', state: {}});
//replace => 跳转的地址为替换而不是新增
history.replace('/c');

路由传参

方案一:问号传参
传递信息暴露到URL地址中,信息是显式的即便在目标路由内刷新,传递的信息也在,缺点是不安全并有长度限制。

// B组件中传递
import { useHistory } from 'react-router-dom';
const B = function B() {
    let history = useHistory();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
           // 写法一
            history.push('/c?age=24&name=YIJUE');
           // 写法二
            history.push({
                pathname: '/c',
                // search存储的就是问号传参信息,要求是urlencoded字符串格式
                search: 'age=24&name=YIJUE'
            });
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;

通过URLSearchParams这个API,来获取具体值

// C组件中接收
import { useLocation } from 'react-router-dom';
const C = function C() {
    const location = useLocation();
    console.log(location.search); //"?age=24&name=YIJUE"
    const usp = new URLSearchParams(location.search);
    console.log(usp.get('age'), usp.get('name')); // => 24 YIJUE
    
    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

方案二:路径传参
传递的信息也在URL地址中,即便在目标组件刷新,传递的信息也在,比问号传参看起来漂亮一些、但是也存在安全和长度的限制。

//更改一级路由表 routes 中的 c 的path
//之前
path: '/c'
//现在:路径传参
path: '/c/:age/:name'//age和name必传
path: '/c/:age?/:name?'//?这里使用了正则匹配规则,存在就传否则不传
// B组件中传递
import { useHistory } from 'react-router-dom';
const B = function B() {
    let history = useHistory();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
           /* 注意要用ES6模板字符串的格式,因为是拼接到地址中的 */ 
            history.push(`/c/21/YIJUE`);
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;
// C组件中接收
import { useRouteMatch, useParams } from 'react-router-dom';
const C = function C() {
   //接收方法一:
    const match = useRouteMatch();
    console.log(match.params); //=>{age:24, name:'YIJUE'}
   //接收方法二:
    const params = useParams();
    console.log(params); //=>{age:24, name:'YIJUE'}
    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

方案三:隐式传参
传递的信息不会出现在URL地址中,并且安全、美观,也没有长度限制,但在目标组件内刷新,传递的信息就丢失了。

// B组件中传递
import { useHistory } from 'react-router-dom';
const B = function B() {
    let history = useHistory();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
             history.push({
                pathname: '/c',
                state: {
                    age: 24,
                    name: 'YIJUE'
                }
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;
// C组件中接收
import { useLocation } from 'react-router-dom';
const C = function C() {
    const location = useLocation();
    console.log(location.state); //=>{age:24, name:'YIJUE'}
    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

(7)NavLink 和 Link

都是实现路由跳转的,语法上几乎一样,区别就是每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和NavLinkto 指定的地址「或者pathname地址」进行匹配,匹配上的这一样,会默认设置active选中样式类「我们可以基于activeClassName重新设置选中的样式类名」,我们也可以设置exact精准匹配,基于这样的机制,我们就可以给选中的导航设置相关的选中样式。

获取方式和用法和 Link 一样,至于 active 样式类大家回去在 css 代码中自己写下就好了

//以这个代码为例
import { NavLink, withRouter} from 'react-router-dom';
class HomeHead extends React.Component {
    render() {
    console.log(this.props)//=>有三个属性信息
        return <nav>
            <NavLink to="/a">A</NavLink>
            <NavLink to="/b">B</NavLink>
            <NavLink to="/c">C</NavLink>
        </nav>;
    };
};
export default withRouter(HomeHead);

2、react-router-dom v6版本

v6地址reactrouter.com/en/main

安装任选方案 (默认最新版本)
npm install react-router-dom
yarn add react-router-dom
其他 cnpm / pnpm …

在 react-router-dom v6 版本中

v6 中移除了Switch、Redirect、withRouter

(1)v6中的基础运用

先回顾一下v5版本

//在 react-router-dom v5 中
// 入口文件
import { HashRouter, Switch, Route, Redirect } from 'react-router-dom';
import HomeHead from "./components/HomeHead";
import A from './views/A';
import B from './views/B';
import C from './views/C';

const App = function App() {
  return <HashRouter>
    {/* 导航封装处理 */}
    <HomeHead />

    {/* 路由容器 */}
    <div className="content">
      <Switch>
        <Route exact path="/" component={A} />
        <Route path="/b" component={B} />
        <Route path="/c" component={C} />
        <Redirect from=" " to="/" exact/>
      </Switch>
    </div>
  </HashRouter>;
};

export default App;

v6版本

//在 react-router-dom v6 中
// 入口文件
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import HomeHead from './components/HomeHead';
/* 导入需要的组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';

const App = function App() {
    return <HashRouter>
        {/* 导航封装处理 */}
        <HomeHead />
        
        {/* 路由容器 */}
        <div className="content">
            <Routes>
                <Route path="/" element={<Navigate to="/a" />} />
                <Route path="/a" element={<A />} />
                //设置 replace 属性,则不会新增记录,而是替换现有记录
                <Route path="/b" element={<B />} replace={true} />
                <Route path="/c" element={<C />} />
                <Route path="*" element={<Navigate to="/a" />} />
            </Routes>
        </div>
    </HashRouter>;
};

export default App;

v6 中所有的路由匹配规则放在<Routes>内,每一条规则的匹配还是基于<Route>
路由匹配成功,不再基于component/render控制渲染的组件
而是基于element,语法格式是<组件名/>
不再需要Switch,默认就是一个匹配成功,就不再匹配下面的了
不再需要exact,默认每一项匹配都是精准匹配
原有的<Redirect>操作,被 <Navigate to="/" /> 代替,遇到 <Navigate/> 组件,路由就会跳转到to指定的路由地址

关于 <Navigate/> 组件的一些细节

/* 如果以上都不匹配,我们可以渲染404组件,也可以重定向到A组件,并传递不同的问号参数信息 */
 //<Navigate to={{...}}/> to的值可以是一个对象:pathname需要跳转的地址、search问号传参信息
   <Route path="*" element={<Navigate to={{pathname: '/a', search: '?from=404'}} />} />

(2)多级路由搭建

在 v5 中多级路由都是分开搭建的,但在 v6 中要求所有的路由,不再分散到各个组件中编写,而是统一都写在一起进行处理

//入口文件
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import HomeHead from './components/HomeHead';
/* 导入需要的组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';
import A1 from './views/a/A1';
import A2 from './views/a/A2';
import A3 from './views/a/A3';

const App = function App() {
    return <HashRouter>
        {/* 导航封装处理 */}
        <HomeHead />
        
        {/* 路由容器 */}
        <div className="content">
            <Routes>
                <Route path="/" element={<Navigate to="/a" />} />
                {/* 拥有二级路由的Route,变为双闭合标签,二级路由写在里面 */}
                <Route path="/a" element={<A />}>
                    {/* 二级路由 */}
                    <Route path="/a" element={<Navigate to="/a/a1" />} />
                    <Route path="/a/a1" element={<A1 />} />
                    <Route path="/a/a2" element={<A2 />} />
                    <Route path="/a/a3" element={<A3 />} />
                    {/* 若是还有三级路由或更多,则同样变为双闭合标签,多级路由依次写在内部 */}
                </Route>
                <Route path="/b" element={<B />} />
                <Route path="/c" element={<C />} />
                <Route path="*" element={<Navigate to={{
                    pathname: '/a',
                    search: '?from=404'
                }} />} />
            </Routes>
        </div>
    </HashRouter>;
};

export default App;

子组件内用<Outlet/>组件来处理

//A组件中
import { Link, Outlet } from 'react-router-dom';

const A = function A() {
    return <>
        <div className="menu">
            <Link to="/a/a1">A1</Link>
            <Link to="/a/a2">A2</Link>
            <Link to="/a/a3">A3</Link>
        </div>
        <div className="view">
            {/* Outlet:路由容器,用来渲染二级(多级)路由匹配的内容 */}
            <Outlet />
        </div>
    </>;
};
export default A;

(3)路由跳转和传参方案

跳转方案

方案一:Link/NavLink跳转

<Link to="/a" > A </link>

方案二:<Navigate/> 遇到这个组件就会跳转

<Navigate to="/a" />

方案三:通过useNavigate()实现跳转

 import { useNavigate } from 'react-router-dom';
 const navigate = useNavigate();
 navigate('/c');

传参方案

方案一:问号传参

// B组件中传递
import { useNavigate } from 'react-router-dom';
const B = function B() {
    let navigate = useNavigate();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
          navigate({
            pathname: '/c',
            search: 'age=24&name=YIJUE'
        }); 
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;

新增useSearchParams这个路由Hook
useSearchParams()是个数组,数组中的第一项就是URLSearchParams
就不需要自己去通过URLSearchParams来获取具体值了

//c组件中接收
import { useLocation, useSearchParams } from 'react-router-dom';
const C = function C() {
    //方法一:useLocation
    const location = useLocation();
    console.log(location.search); //"?age=24&name=YIJUE"
    const usp = new URLSearchParams(location.search);
    console.log(usp.get('age'), usp.get('name')); // => 24 YIJUE
    
    //方法二:useSearchParams
    const [usp] = useSearchParams();
    console.log(usp.get('age'), usp.get('name')); // => 24 YIJUE

    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

方案二:路径传参

//还是先改变一级路由路径信息
<Route path="/c/:age?/:name?" element={<C />} />
// B组件中传递
import { useNavigate } from 'react-router-dom';
const B = function B() {
    let navigate = useNavigate();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
          navigate(`/c/24/YIJUE`);
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;
//c组件中接收
import { useParams } from 'react-router-dom';
const C = function C() {
    const params = useParams();
    console.log(params); //=>{age:24, name:'YIJUE'}
    
    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

方案三:隐式传参
与 v5 不同的是在目标组件内刷新,传递的信息依然存在

// B组件中传递
import { useNavigate } from 'react-router-dom';
const B = function B() {
    let navigate = useNavigate();
    return <div className="box">
        B组件的内容
        <button onClick={() => {
             navigate('/c', {
            //历史记录池替换现有地址
            replace: true,
            //隐式传参信息
            state: {
                age: 24,
                name: 'YIJUE'
            }
        });
        }}>按钮(跳转同时传参)</button>
    </div>;
};
export default B;
import { useLocation } from 'react-router-dom';
const C = function C() {
    const location = useLocation();
    console.log(location.state); //=>{age:24, name:'YIJUE'}
    
    return <div className="box">
        C组件的内容
    </div>;
};
export default C;

在react-router-dom v6中,常用的路由Hook

  • useNavigate 代替5中的 useHistory 实现编程式导航
  • useLocation「5中也有」获取location对象信息 pathname/search/state…
  • useParams「5中也有」获取路径参数匹配的信息
  • useSearchParams「新增的」获取问号传参信息,取到的结果是一个URLSearchParams对象

(4)获取信息和搭建路由表

在react-router-dom v6中 ,当前组件不论是否基于<Route>匹配渲染,都不会通过props把属性传递给组件,想获取相关的信息,我们只能基于路由Hook函数处理,这样的话非<Route>匹配渲染的类组件,就会无法获取相关属性.

解决方案

  • 仿照 v5 让基于<Route>匹配渲染的组件,可以通过属性获取需要的信息
  • 重写 withRouter 让其和基于匹配渲染的组件,具备相同的属性

我们先来搭建路由表
因为 v6 中多级路由都写在了一起,那我们路由表也都放在一个文件内
(当然也可以像 v5 里一样分开写)

还是在src目录下新建route文件夹
在router文件夹中新建两个js文件分别是 routes.js、index.js

//routes文件中
import { Navigate } from 'react-router-dom';
import { lazy } from 'react';
import A from '../views/A';

/* A版块的二级路由 */
const aRoutes = [{
    path: '/a',
    component: () => <Navigate to="/a/a1" />
}, {
    path: '/a/a1',
    name: 'a-a1',
    component: lazy(() => import('../views/a/A1')),
}, {
    path: '/a/a2',
    name: 'a-a2',
    component: lazy(() => import('../views/a/A2')),
}, {
    path: '/a/a3',
    name: 'a-a3',
    component: lazy(() => import('../views/a/A3')),
}];

/* 一级路由 */
const routes = [{
    path: '/',
    component: () => <Navigate to="/a" />
}, {
    path: '/a',
    name: 'a',
    component: A,
    children: aRoutes
}, {
    path: '/b',
    name: 'b',
    component: lazy(() => import('../views/B')),
}, {
    path: '/c/:age?/:name?',
    name: 'c',
    component: lazy(() => import('../views/C')),
}, {
    path: '*',
    component: () => {
        return <Navigate to={{
            pathname: '/a',
            search: '?from=404'
        }} />;
    }
}];
export default routes;

在  index.js 文件中我们要做三件事

  1. 根据路由表来动态设定路由的匹配规则
  2. 实现 v6 中可以基于props获取信息的方案
  3. 重写 withRouter 这个高阶函数
 //index文件中
import { Suspense } from 'react';
import routes from "./routes";
import { Routes, Route, useNavigate, useLocation, useParams, useSearchParams } from 'react-router-dom';

/* 统一渲染的组件:在这里我们来传递路由信息的属性 */
const Element = function Element(props) {
    // 因为React中,渲染的组件命名方式要大写,所以这里做一步重命名
    // props就是通过 createRoute方法中的item传递过来的,也就是路由表中每一项需要用到的信息
    let { component: Component } = props;

    /* 
     把路由信息先获取到,最后基于属性传递给组件:只要是基于<Route>匹配渲染的组件,
     都可以基于属性获取路由信息
    */ 
    const navigate = useNavigate(),
        location = useLocation(),
        params = useParams(),
        [usp] = useSearchParams();

    // 最后把Component进行渲染,把通过Hook获取到属性挂载上去
    return <Component navigate={navigate} location={location} params={params} usp={usp} />;
};

/* 递归创建Route */
const createRoute = function createRoute(routes) {
    return <>
        {routes.map((item, index) => {
            let { path, children } = item;
            /* 
              每一次路由匹配成功,不直接渲染我们设定的组件,而是渲染Element;
              在Element做一些特殊处理后,再去渲染我们真实要渲染的组件
            */ 
            // 把路由表中每一项需要用到的信息item,传递给Element,也就是上面写的Element方法
            return <Route key={index} path={path} element={<Element {...item} />}>
                {/* 基于递归方式,绑定子集路由 */}
                {Array.isArray(children) ? createRoute(children) : null}
            </Route>;
        })}
    </>;
};

/* 路由容器 */
export default function RouterView() {
    //
    return <Suspense fallback={<>正在处理中...</>}>
          <Routes>
          //这里就是我们通过一列操作,循环创建的动态路由信息
            {createRoute(routes)}
        </Routes>
    </Suspense>;
};

/* 创建withRouter */
export const withRouter = function withRouter(Component) {
    // Component:真实要渲染的组件
    return function HOC(props) {
        // 提前获取路由信息,作为属性传递给Component
        const navigate = useNavigate(),
            location = useLocation(),
            params = useParams(),
            [usp] = useSearchParams();
     return <Component {...props} navigate={navigate} location={location} params={params} usp={usp} />;
    };
};

在入口文件 App.jsx 中

import { HashRouter } from 'react-router-dom';
import HomeHead from './components/HomeHead';
import RouterView from "./router";

const App = function App() {
    return <HashRouter>
        <HomeHead />
        <div className="content">
            <RouterView />
        </div>
    </HashRouter>;
};

export default App;

现在以B组件为例,现在就可以通过props来获取属性了

//B组件中
const B = function B(props) {
    const { navigate } = props;
    return <div className="box">
        B组件的内容
        <button onClick={() => {
           navigate(`/c/24/YIJUE`);
        }}>按钮</button>
    </div>;
};
export default B;

v6 中获取属性信息总结:

函数组件,并且是基于<Route>匹配渲染的
+ 基于 Hook 函数获取
+ 通过我们的处理基于 props 属性获取
函数组件,但并不是基于<Route>匹配渲染的
+ 基于 Hook 函数获取
+ 基于我们写的 withRouter 代理一下这个组件,通过 props 获取
类组件,并且是基于<Route>匹配渲染的
+ 通过我们的处理基于 props 属性获取
类组件,但并不是基于<Route>匹配渲染的
+ 基于我们写的 withRouter 代理一下这个组件,通过 props 获取

三、写在最后

从 GitHub 上的统计数据来看,React Router v6 自发布以来已经获得了很高的关注度。截至 2023 年 3 月,React Router v6 的 GitHub 仓库获得了超过 32,000 个星标和 5000 多个 Fork。
至于为什么在 v6 中移除了 withRouter 这个高阶组件,你觉(作者)认为这就是FaceBook 团队释放的信号,class 组件终将成为历史,函数式编程就是未来的主流方向。

本文花费你觉(作者)4天时间,希望能对大家有所帮助
欢迎大家点赞、评论、收藏和关注!
你们的一键三连就是你觉(作者)更新的动力源泉!!

作者VX:IBeancurd 有问题可以随时与我联系

下期内容预告:“ 干中之干系列3 —— Redux!!”

原文链接:https://juejin.cn/post/7214318587431190586 作者:周一觉

(0)
上一篇 2023年3月25日 下午7:49
下一篇 2023年3月25日 下午8:00

相关推荐

发表评论

登录后才能评论