” 一觉醒来又有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版本
安装任选方案
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
都是实现路由跳转的,语法上几乎一样,区别就是每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和
NavLink
中to
指定的地址「或者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版本
安装任选方案 (默认最新版本)
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
这个路由HookuseSearchParams()
是个数组,数组中的第一项就是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 文件中我们要做三件事
- 根据路由表来动态设定路由的匹配规则
- 实现 v6 中可以基于
props
获取信息的方案 - 重写
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 作者:周一觉