本文将带你了解到:路由原理、react-router的使用、企业级封装
一、路由机制
HASH路由
- 改变页面的哈希值(#/xxx),主页面是不会刷新的
- 根据不同的哈希值,让容器中渲染不同的内容「组件」
简单实现
<body>
<nav class="nav-box">
<a href="#/">首页</a>
<a href="#/product">产品中心</a>
<a href="#/personal">个人中心</a>
</nav>
<div class="view-box"></div>
<script>
// 获取渲染内容的容器
const viewBox = document.querySelector('.view-box');
// 构建一个路由匹配表:每当我们重新加载页面、或者路由切换(切换哈希值),都先到这个路由表中进行匹配;根据当前页面的哈希值,匹配出要渲染的内容(组件)!!
const routes = [
{ path: '/', component: '首页的内容' },
{ path: '/product', omponent: '产品中心的内容' },
{ 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;
</script>
</body>
history路由
- 利用了H5中的HistoryAPI来实现页面地址的切换「可以不刷新页面」
- 根据不同的地址,到路由表中进行匹配,让容器中渲染不同的内容「组件」
问题:我们切换的地址,在页面不刷新的情况下是没有问题的但是如果页面刷新,这个地址是不存在的,会报404错误!!
解决方案:此时我们需要服务器的配合:在地址不存在的情况下,也可以把主页面内容返回!!
简单实现
<body>
<nav class="nav-box">
<a href="/">首页</a>
<a href="/product">产品中心</a>
<a href="/personal">个人中心</a>
</nav>
<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);
routerMatch(); // 去路由匹配
}
};
// 路由匹配的办法
const routes = [
{ path: '/', component: '首页的内容' },
{ path: '/product', omponent: '产品中心的内容' },
{ 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;
</script>
</body>
二、react-router-dom V5
1.简单使用
import React from "react";
import { HashRouter, Route, Switch, Redirect, Link } from 'react-router-dom';
/* 导入组件 */
import A from './views/A';
import B from './views/B';
import C from './views/C';
/* 导航区域的样式 */
import styled from "styled-components";
const NavBox = styled.nav`
a{
margin-right: 10px;
color: #000;
}
`;
const App = function App() {
/*
基于<HashRouter>把所有要渲染的内容包起来,开启HASH路由
+ 后续用到的<Route>、<Link>等,都需要在HashRouter/BrowserRouter中使用
+ 开启后,整个页面地址,默认会设置一个 #/ 哈希值
Link实现路由切换/跳转的组件
+ 最后渲染完毕的结果依然是A标签
+ 它可以根据路由模式,自动设定点击A切换的方式
*/
return <HashRouter>
{/* 导航部分 */}
<NavBox>
<Link to="/">A</Link>
<Link to="/b">B</Link>
<Link to="/c">C</Link>
</NavBox>
{/* 路由容器:每一次页面加载或者路由切换完毕,都会根据当前的哈希值,到这里和每一个Route进行匹配,把匹配到的组件,放在容器中渲染!! */}
<div className="content">
{/*
Switch:确保路由中,只要有一项匹配,则不再继续向下匹配
exact:设置匹配模式为精准匹配
*/}
<Switch>
<Route exact path="/" component={A} />
<Route path="/b" component={B} />
<Route path="/c" render={() => {
// 当路由地址匹配后,先把render函数执行,返回的返回值就是我们需要渲染的内容
// 在此函数中,可以处理一些事情,例如:登录态检验....
let isLogin = true;
if (isLogin) {
return <C />;
}
return <Redirect to="/login" />
}} />
{/*
// 放在最后一项,path设置※或者不写,意思是:以上都不匹配,则执行这个规则
<Route path="*" component={404组件} />
// 当然也可以不设置404组件,而是重定向到默认 / 地址:
<Redirect from="" to="" exact/>
+ from:从哪个地址来
+ to:重定向的地址
+ exact是对from地址的修饰,开启精准匹配
*/}
<Redirect to="/" />
</Switch>
</div>
</HashRouter>;
};
export default App;
2.关于withRouter
/*
只要在<HashRouter>/<BrowserRouter>中渲染的组件:
我们在组件内部,基于useHistory/useLocation/useRouteMatch这些Hook函数,就可以获取history/location/match这些对象信息!!
即便这个组件并不是基于<Route>匹配渲染的!!
只有基于<Route>匹配渲染的组件,才可以基于props属性,获取这三个对象信息!!
问题:如果当前组件是一个类组件,在<HashRouter>内,但是并没有经过<Route>匹配渲染,我们如何获取三个对象信息呢?
解决方案:基于函数高阶组件,自己包裹一层进行处理!!
在react-router-dom v5版本中,自带了一个高阶组件 withRouter ,就是用来解决这个问题的!!
*/
class HomeHead extends React.Component {
render() {
// console.log(this.props); //有三个对象信息了
return <NavBox>
{/*
NavLink VS Link
都是实现路由跳转的,语法上几乎一样,区别就是:
每一次页面加载或者路由切换完毕,都会拿最新的路由地址,和NavLink中to指定的地址「或者pathname地址」进行匹配
+ 匹配上的这一样,会默认设置active选中样式类「我们可以基于activeClassName重新设置选中的样式类名」
+ 我们也可以设置exact精准匹配
基于这样的机制,我们就可以给选中的导航设置相关的选中样式!!
*/}
<NavLink to="/a">A</NavLink>
<NavLink to="/b">B</NavLink>
<NavLink to="/c">C</NavLink>
</NavBox>;
}
}
export default withRouter(HomeHead);
3.传参方式
import React from "react";
import { useHistory } from 'react-router-dom';
import qs from 'qs';
const B = function B() {
let history = useHistory();
return <div className="box">
B组件的内容
<button onClick={() => {
/*
传参方案一:问号传参
+ 传递的信息出现在URL地址上:丑、不安全、长度限制
+ 信息是显式的,即便在目标路由内刷新,传递的信息也在
// history.push('/c?id=100&name=zhufeng');
history.push({
pathname: '/c',
// search存储的就是问号传参信息,要求是urlencoded字符串
search: qs.stringify({
id: 100,
name: 'zhufeng'
})
});
*/
/*
传参方案二:路径参数「把需要传递的值,作为路由路径中的一部分」
+ 传递的信息也在URL地址中:比问号传参看起来漂亮一些、但是也存在安全和长度的限制
+ 因为信息都在地址中,即便在目标组件刷新,传递的信息也在
history.push(`/c/100/zhufeng`);
*/
/*
方案三:隐式传参
+ 传递的信息不会出现在URL地址中:安全、美观,也没有限制
+ 在目标组件内刷新,传递的信息就丢失了
*/
history.push({
pathname: '/c',
state: {
id: 100,
name: 'zhufeng'
}
});
}}>按钮</button>
</div>;
};
export default B;
const C = function C() {
/* const location = useLocation();
// console.log(location.search); //"?id=100&name=zhufeng"
// 获取传递的问号参数信息
let { id, name } = qs.parse(location.search.substring(1));
console.log(id, name);
// 也可以基于URLSearchParams来处理
let usp = new URLSearchParams(location.search);
console.log(usp.get('id'), usp); */
/* // const match = useRouteMatch();
// console.log(match.params); //=>{id:100,name:'zhufeng'}
let params = useParams();
console.log(params); //=>{id:100,name:'zhufeng'} */
/* const location = useLocation();
console.log(location.state); */
return <div className="box">
C组件的内容
</div>;
};
4.企业级封装
路由表
/*
配置路由表:数组,数组中每一项就是每一个需要配置的路由规则
+ redirect:true 此配置是重定向
+ from:来源的地址
+ to:重定向的地址
+ exact:是否精准匹配
+ path:匹配的路径
+ component:渲染的组件
+ name:路由名称(命名路由)
+ meta:{} 路由元信息「包含当前路由的一些信息,当路由匹配后,我们可以拿这些信息做一些事情...」
+ children:[] 子路由
+ ...
*/
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,
meta: {},
children: aRoutes
}, {
path: '/b',
name: 'b',
component: lazy(() => import('../views/B')),
meta: {}
}, {
path: '/c/:id?/:name?',
name: 'c',
component: lazy(() => import('../views/C')),
meta: {}
}, {
redirect: true,
to: '/a'
}];
export default routes;
二级路由表
// 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(/* webpackChunkName:"AChild" */'../views/a/A1')),
meta: {}
}, {
path: '/a/a2',
name: 'a-a2',
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A2')),
meta: {}
}, {
path: '/a/a3',
name: 'a-a3',
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A3')),
meta: {}
}];
export default aRoutes;
全局路由入口router/index.js
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={(props) => {
// 统一基于RENDER函数处理,当某个路由匹配,后期在这里可以做一些其它事情
// Suspense.fallback:在异步加载的组件没有处理完成之前,先展示的Loading效果!!
return <Suspense fallback={<>正在处理中...</>}>
<Component {...props} />
</Suspense>;
}} />;
})}
</Switch>;
};
export default RouterView;
三、react-router-dom V6
1.基本使用
import React from "react";
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>;
+ 路由匹配成功,不再基于component/render控制渲染的组件,而是基于element,语法格式是<Component/>
+ 不再需要Switch,默认就是一个匹配成功,就不在匹配下面的了
+ 不再需要exact,默认每一项匹配都是精准匹配
原有的<Redirect>操作,被 <Navigate to="/" /> 代替!!
+ 遇到 <Navigate/> 组件,路由就会跳转,跳转到to指定的路由地址
+ 设置 replace 属性,则不会新增立即记录,而是替换现有记录
+ <Navigate to={{...}}/> to的值可以是一个对象:pathname需要跳转的地址、search问号传参信息
*/}
<Routes>
<Route path="/" element={<Navigate to="/a" />} />
<Route path="/a" element={<A />}>
{/* v6版本中,要求所有的路由(二级或者多级路由),不在分散到各个组件中编写,而是统一都写在一起进行处理!! */}
<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/:id?/:name?" element={<C />} />
{/* 如果以上都不匹配,我们可以渲染404组件,也可以重定向到A组件「传递不同的问号参数信息」 */}
<Route path="*" element={<Navigate to={{
pathname: '/a',
search: '?from=404'
}} />} />
</Routes>
</div>
</HashRouter>;
};
export default App;
2.Outlet
v5 是 RouterView
const A = function A() {
return <DemoBox>
<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>
</DemoBox>;
};
export default A;
3.路由传参
import React from "react";
import { useNavigate } from 'react-router-dom';
import qs from 'qs';
/*
在react-router-dom v6中 ,实现路由跳转的方式:
+ <Link/NavLink to="/a" > 点击跳转路由
+ <Navigate to="/a" /> 遇到这个组件就会跳转
+ 编程式导航:取消了history对象,基于navigate函数实现路由跳转
import { useNavigate } from 'react-router-dom';
const navigate = useNavigate();
navigate('/c');
navigate('/c', { replace: true });
navigate({
pathname: '/c'
});
navigate({
pathname: '/c',
search: '?id=100&name=zhufeng'
});
...
*/
/*
在react-router-dom v6中 ,即便当前组件是基于<Route>匹配渲染的,也不会基于属性,把history/location/match传递给组件!!想获取相关的信息,我们只能基于Hook函数处理!!
+ 首先要确保,需要使用“路由Hook”的组件,是在Router「HashRouter或BrowserRouter」内部包着的,否则使用这些Hook会报错!!
+ 只要在<Router>内部包裹的组件,不论是否是基于<Route>匹配渲染的
+ 默认都不可能再基于props获取相关的对象信息了
+ 只能基于“路由Hook”去获取!!
为了在类组件中也可以获取路由的相关信息:
1. 稍后我们构建路由表的时候,我们会想办法:继续让基于<Route>匹配渲染的组件,可以基于属性获取需要的信息
2. 不是基于<Route>匹配渲染的组件,我们需要自己重写withRouter「v6中干掉了这个API」,让其和基于<Route>匹配渲染的组件,具备相同的属性!!
*/
const B = function B() {
const navigate = useNavigate();
const handle = () => {
/*
// 问号传参
navigate({
pathname: '/c',
search: qs.stringify({
id: 100,
name: 'zhufeng'
})
});
*/
/* // 路径参数
navigate(`/c/100/zhufeng`); */
// 隐式传参
navigate('/c', {
//历史记录池替换现有地址
replace: true,
//隐式传参信息
state: {
id: 100,
name: 'zhufeng'
}
});
};
return <div className="box">
B组件的内容
<button onClick={handle}>按钮</button>
</div>;
};
export default B;
/*
在react-router-dom v6中,常用的路由Hook
+ useNavigate -> 代替5中的 useHistory :实现编程式导航
+ useLocation 「5中也有」:获取location对象信息 pathname/search/state….
+ useSearchParams「新增的」:获取问号传参信息,取到的结果是一个URLSearchParams对象
+ useParams「5中也有」:获取路径参数匹配的信息
———————
+ useMatch(pathname) -> 代替5中的 useRouteMatch「5中的这个Hook有用,可以基于params获取路径参数匹配的信息;但是在6中,这个Hook需要我们自己传递地址,而且params中也没有获取匹配的信息,用的就比较少了!!」
*/
const C = function C() {
/*
const location = useLocation();
// location.search:"?id=100&name=zhufeng"
const usp = new URLSearchParams(location.search);
console.log(usp.get('id'), usp.get('name'));
let [usp] = useSearchParams();
console.log(usp.get('id'), usp.get('name'));
*/
/* const params = useParams();
console.log(params); //=>{id:100,name:'zhufeng'} */
const location = useLocation();
console.log(location.state);
return <div className="box">
C组件的内容
</div>;
};
4.企业级封装
路由表配置
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(/* webpackChunkName:"AChild" */'../views/a/A1')),
meta: {}
}, {
path: '/a/a2',
name: 'a-a2',
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A2')),
meta: {}
}, {
path: '/a/a3',
name: 'a-a3',
component: lazy(() => import(/* webpackChunkName:"AChild" */'../views/a/A3')),
meta: {}
}];
/* 一级路由 */
const routes = [{
path: '/',
component: () => <Navigate to="/a" />
}, {
path: '/a',
name: 'a',
component: A,
meta: {},
children: aRoutes
}, {
path: '/b',
name: 'b',
component: lazy(() => import('../views/B')),
meta: {}
}, {
path: '/c/:id?/:name?',
name: 'c',
component: lazy(() => import('../views/C')),
meta: {}
}, {
path: '*',
component: () => {
return <Navigate to={{
pathname: '/a',
search: '?from=404'
}} />;
}
}];
export default routes;
全局路由入口router/index.js
import { Suspense } from 'react';
import routes from "./routes";
import { Routes, Route, useNavigate, useLocation, useParams, useSearchParams } from 'react-router-dom';
/* 统一渲染的组件:在这里可以做一些事情「例如:权限/登录态校验,传递路由信息的属性...」 */
const Element = function Element(props) {
let { component: Component } = props;
// 把路由信息先获取到,最后基于属性传递给组件:只要是基于<Route>匹配渲染的组件,都可以基于属性获取路由信息
const navigate = useNavigate(),
location = useLocation(),
params = useParams(),
[usp] = useSearchParams();
// 最后要把Component进行渲染
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做一些特殊处理后,再去渲染我们真实要渲染的组件!!
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} />;
};
};