type
status
date
slug
summary
tags
category
icon
password

Router

详细使用示例

注意:这里使用的时最新的 v6 版本,已完全弃用了原先的 react-router 库,较 v5 版本有很大的改动。

一、基本使用

  1. 首先安装依赖
npm i react-router-dom
  1. 引入实现路由所需的组件,以及页面组件
import { BrowserRouter, Routes, Route } from "react-router-dom"; import Foo from "./Foo"; import Bar from "./Bar"; function App() { return ( <BrowserRouter> <Routes> <Route path="/foo" element={<Foo />} /> <Route path="/bar" element={<Bar />} /> </Routes> </BrowserRouter> ); }
  • path:路径
  • element:要渲染的组件
注意:BrowserRouter组件最好放在最顶层所有组件之外,这样能确保内部组件使用 Link 做路由跳转时不出错

二、路由跳转

在跳转路由时,如果路径是/开头的则是绝对路由,否则为相对路由,即相对于当前 URL进行改变

2.1 Link 组件

Link组件只能在Router内部使用,因此使用到Link组件的组件一定要放在顶层的 Router 之内
import { Link } from "react-router-dom"; <Link to="foo">to foo</Link>;

2.2 NavLink 组件

  • NavLink组件Link组件的功能是一致的,区别在于可以判断其to属性是否是当前匹配到的路由
  • NavLink组件styleclassName可以接收一个函数,函数接收一个含有isActive字段的对象为参数,可根据该参数调整样式
import { NavLink } from "react-router-dom"; function Foo() { return ( <NavLink style={({ isActive }) => ({ color: isActive ? "red" : "#fff" })}> Click here </NavLink> ); }

2.3 编程式跳转

使用useNavigate钩子函数生成navigate函数,可以通过 JS 代码完成路由跳转
useNavigate`取代了原先版本中的`useHistory
import { useNavigate } from 'react-router-dom'; function Foo(){ const navigate = useNavigate(); return ( // 上一个路径:/a; 当前路径: /a/a1 <div onClick={() => navigate('/b')}>跳转到/b</div> <div onClick={() => navigate('a11')}>跳转到/a/a1/a11</div> <div onClick={() => navigate('../a2')}>跳转到/a/a2</div> <div onClick={() => navigate(-1)}>跳转到/a</div> ) }
  • 可以直接传入要跳转的目标路由(可以使用相对路径,语法和 JS 相同)
  • 传入1表示后退

四、动态路由参数(URL参数)

4.1 路径参数

  • Route组件中的path属性中定义路径参数
  • 在组件内通过useParams hook 访问路径参数
<BrowserRouter> <Routes> <Route path="/foo/:id" element={<Foo />} /> </Routes> </BrowserRouter>; import { useParams } from "react-router-dom"; export default function Foo() { const params = useParams(); return ( <div> <h1>{params.id}</h1> </div> ); }

路径匹配规则

当URL同时匹配到含有路径参数的路径和无参数路径时,有限匹配没有参数的”具体的“(specific)路径。
<Route path="teams/:teamId" element={<Team />} /> <Route path="teams/new" element={<NewTeamForm />} />
如上的两个路径,将会匹配 teams/new
路径的正则匹配已被移除。

兼容类组件

在以前版本中,组件的props会包含一个match对象,在其中可以取到路径参数。
但在最新的 6.x 版本中,无法从 props 获取参数。
并且,针对类组件的 withRouter 高阶组件已被移除。因此对于类组件来说,使用参数有两种兼容方法:
  1. 将类组件改写为函数组件
  1. 自己写一个 HOC 来包裹类组件,用 useParams 获取参数后通过 props 传入原本的类组件

4.2 search 参数

  • 查询参数不需要在路由中定义
  • 使用 useSearchParams hook 来访问和修改查询参数。其用法和 useState 类似,会返回当前对象和更改它的方法
  • 使用 setSearchParams 时,必须传入所有的查询参数,否则会覆盖已有参数
import { useSearchParams } from "react-router-dom"; // 当前路径为 /foo?id=12 function Foo() { const [searchParams, setSearchParams] = useSearchParams(); console.log(searchParams.get("id")); // 12 setSearchParams({ name: "foo", }); // /foo?name=foo return <div>foo</div>; }

五、嵌套路由

5.1 路由定义

通过嵌套的书写Route组件实现对嵌套路由的定义。
path 开头为 / 的为绝对路径,反之为相对路径。
<Routes> <Route path="/" element={<Home />}></Route> <Route path="/father" element={<Father />}> <Route path="child" element={<Child />}></Route> <Route path=":name" element={<Another />}></Route> </Route> </Routes>

5.2 在父组件中展示

在父组件中使用Outlet来显示匹配到的子组件
import { Outlet } from "react-router-dom"; function Father() { return ( <div> // ... 自己组件的内容 // 留给子组件Child的出口 <Outlet /> </div> ); }

5.3 在组件中定义

可以在任何组件中使用 Routes 组件,且组件内的Routes中,路径默认带上当前组件的路径作为前缀。
注意:此时定义父组件的路由时,要在后面加上 /* ,否则父组件将无法渲染。
<Routes> <Route path="/" element={<Home />} /> <Route path="dashboard/*" element={<Dashboard />} /> </Routes> function Dashboard() { return ( <div> <p>Look, more routes!</p> <Routes> <Route path="/" element={<DashboardGraphs />} /> <Route path="invoices" element={<InvoiceList />} /> </Routes> </div> ); }

六、默认路由

定义: 在嵌套路由中,如果 URL 仅匹配了父级 URL,则Outlet中会显示带有index属性的子路由。可以使用在路由的任何层级
<Routes> <Route path="/foo" element={Foo}> <Route index element={Default}></Route> <Route path="bar" element={Bar}></Route> </Route> </Routes>
  • 当 url 为/foo时:Foo 中的 Outlet 会显示 Default 组件
  • 当 url 为/foo/bar时:Foo 中的 Outlet 会显示为 Bar 组件

七、全匹配路由

定义: path属性取值为*时,可以匹配任何(非空)路径,该匹配拥有最低的优先级。可以用于设置 404 页面。
<Routes> <Route path="/foo" element={Foo}> <Route path="bar" element={Bar}></Route> <Route path="*" element={NotFound}></Route> </Route> </Routes>

八、多组路由

通常,一个应用中只有一个Routes组件。
但根据实际需要也可以定义多个路由出口(如:侧边栏和主页面都要随 URL 而变化)
<Router> <SideBar> <Routes> <Route></Route> </Routes> </SideBar> <Main> <Routes> <Route></Route> </Routes> </Main> </Router>

九、路由重定向

当在某个路径/a下,要重定向到路径/b时,可以通过Navigate组件进行重定向到其他路径
等价于以前版本中的Redirect组件
import { Navigate } from "react-router-dom"; function A() { return <Navigate to="/b" />; }

十、布局路由

当多个路由有共同的父级组件时,可以将父组件提取为一个没有 pathindex 属性的Route组件(Layout Route)
<Route element={<PageLayout />}> <Route path="/privacy" element={<Privacy />} /> <Route path="/tos" element={<Tos />} /> </Route>
这种写法等价于:
<Route path="/privacy" element={ <PageLayout> <Privacy /> </PageLayout> } /> <Route path="/tos" element={ <PageLayout> <Tos /> </PageLayout> } />

十一、订阅和操作 history stack的原理

浏览器会记录导航堆栈,以实现浏览器中的前进后退功能。在传统的前端项目中,URL的改变意味着向服务器重新请求数据。
在现在的客户端路由( client side routing )中,可以做到编程控制URL改变后的反应。如在点击a标签的回调函数中使用 event.preventDefault() 阻止默认事件,此时URL的改变不会带来任何UI上的更新。
<a href="/contact" onClick={(event) => { // stop the browser from changing the URL and requesting the new document event.preventDefault(); // push an entry into the browser history stack and change the URL window.history.pushState({}, undefined, "/contact"); }} />

11.1 History对象

浏览器没有直接提供监听URL改变(push、pop、replace)的接口,因此 react-router 对原生的 history 对线进行了包装,提供了监听URL改变的API。
let history = createBrowserHistory(); history.listen(({ location, action }) => { // this is called whenever new locations come in // the action is POP, PUSH, or REPLACE });
使用 react-router 时不需操作History对象(Routes 组件会进行操作)

11.2 Location对象

react-routerwindow.location 进行包装后,提供了一个形式简洁的Location对象,形如:
{ pathname: "/bbq/pig-pickins", // 主机名之后的URL地址 search: "?campaign=instagram", // 查询参数 hash: "#menu", // 哈希值,用于确定页面滚动的具体位置 state: null, // 对于 window.history.state 的包装 key: "aefz24ie" // }

state

不显示在页面上,不会引起刷新,只由开发人员操作。
可用于记录用户的跳转详情(从哪跳到当前页面)或在跳转时携带信息。
可以用在 Link 组件或 navigate 方法中
<Link to="/pins/123" state={{ fromDashboard: true }} /> let navigate = useNavigate(); navigate("/users/123", { state: partialUser });
在目标的组件中,可以用 useLocation 方法获取该对象
let location = useLocation(); console.log(location.state);
state中的信息会进行序列化,因此如日期对象等信息会变为string

key

每个Location对象拥有一个唯一的key,可以据此来实现基于Location的滚动管理,或是数据缓存。
如:将 location.key 和 URL 作为键,每次请求数据前,先查找缓存是否存在来判断是否实际发送请求,来实现客户端数据缓存。

十二、 各类Router组件

12.1 HashRouter和BrowserRouter的区别

  • HashRouter 只会修改URL中的哈希值部分;而 BrowserRouter 修改的是URL本身
  • HashRouter纯前端路由,可以通过输入URL直接访问;使用时 BrowserRouter 直接输入URL会显示404,除非配置Nginx将请求指向对应的HTML文件。初次进入 / 路径时或点击 Link 组件跳转时不会发送请求

12.2 unstable_HistoryRouter

使用 unstable_HistoryRouter 需要传入一个 history 库的实例,这将允许在非react作用于下操作history对象。
由于项目使用的history和react-router中使用的history版本可能不一样,该API目前标为unstable状态

12.3 MemoryRouter

HashRouterBrowserRouter 都是依据外部对象(history)进行导航,而 MemoryRouter 则是自己存储和管理状态堆栈,多用于测试场景。

12.4 NativeRouter

推荐的用于 React Native的Router组件

12.5 StaticRouter

在nodejs端使用,渲染react应用。
import * as React from "react"; import * as ReactDOMServer from "react-dom/server"; import { StaticRouter } from "react-router-dom/server"; import http from "http"; function requestHandler(req, res) { let html = ReactDOMServer.renderToString( <StaticRouter location={req.url}> {/* The rest of your app goes here */} </StaticRouter> ); res.write(html); res.end(); } http.createServer(requestHandler).listen(3000);

十三、使用JS对象定义路由(路由表)

使用 useRoutes hook,可以使用一个JS对象而不是Routes组件与Route组件来定义路由。其功能类似于react-router-config(opens new window)
useRoutes 的返回是 React Element,或是 null。
此功能用来管理路由表,相比v5,可能需要借助一些第三方库来实现路由config管理,现在v6版本自带

(一) 创建路由表

const routes = useRoutes([ { path: '/profile', element: <Profile/> }, { path: '/message', element: <Message/> children: [ // 嵌套路由,需在组件中使用 Outlet组件 进行占位 { path: 'message1', element: <Message1/> }, { path: 'message2', element: <Message2/> children:[ { // 匹配动态路由 path: 'message1/:id', element: <MessageDetail/> } ] } ] }, { path: '/', element: <Navigate to='/profile'/> }, { path: '/user', children: [ { index: true, element: <h1>user~</h1> // 这种不属于嵌套路由,这里面children会放到父亲的位置,所以不需要配合Outlet组件使用 } ] } ])
对于传入的配置对象, 其类型定义如下:
interface RouteObject { caseSensitive?: boolean; children?: RouteObject[]; element?: React.ReactNode; index?: boolean; path?: string; }
提示
也可以单独创建 routes 文件夹管理路由表,然后在组件内使用 useRoutes

(二) 组件结构内使用函数返回值作为占位符

return ( <div> <ul className="nav nav-pills"> {/*路由导航区*/} <li role="presentation"><NavLink to="/profile" >Profile</NavLink></li> <li role="presentation"><NavLink to="/message">Messages</NavLink></li> </ul> {/*路由展示区*/} <h1> {routes} </h1> </div>

(三) 嵌套路由

在一级路由下面创建二级子路由
  1. 引入路由路由占位符OutLet到组件中
  1. 引入NavLink到组件中改变路径,子路由路径可以不需要加/
import React from 'react' import { NavLink, Outlet } from 'react-router-dom' export default function Message() { return ( <div> <ul> <li> <NavLink to='message1'>message1</NavLink> </li> <li> <NavLink to='message2'>message2</NavLink> </li> </ul> {/*路由占位符*/} <Outlet/> </div> ) }
注意
不要忘记使用Outlet作为占位符

(四) 路由懒加载

import { lazy } from 'react' const Message1 = lazy(() => import('../pages/LoginPage')) // 路由懒加载 // 在路由表中直接使用即可 export default [ { path: '/login', element: <LoginPage/> } ]

十四、其他hooks函数

直接在组件中调用即可

useRouterContext
作用:判断组件是否被路由包裹

useNavigationType
作用:判断路由跳转方式
返回值:push pop replace
pop是在浏览器中直接打开了这个路由的组件

useOutlet
作用:查看组件下级路由信息
组件若未被挂载则返回null,若以被挂载,则返回该子组件信息

useResolvePath
作用:解析路径
useResolvedPath("https://www.bilibili.com/video/BV1wy4y1D7JT?p=140&spm_id_from=pageDriver")

动态路由

router/index.ts 默认路由

import { lazy } from "react"; import { Navigate } from "react-router-dom"; // React 组件懒加载 // 快速导入工具函数 const lazyLoad = (moduleName: string) => { const Module = lazy(() => import(`views/${moduleName}`)); return <Module />; }; // 路由鉴权组件 const Appraisal = ({ children }: any) => { const token = localStorage.getItem("token"); return token ? children : <Navigate to="/login" />; }; interface Router { name?: string; path: string; children?: Array<Router>; element: any; } const routes: Array<Router> = [ { path: "/login", element: lazyLoad("login"), }, { path: "/", element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>, children: [ { path: "", element: <Navigate to="home" />, }, { path: "*", element: lazyLoad("sand-box/nopermission"), }, ], }, { path: "*", element: lazyLoad("not-found"), }, ]; export default routes;

redux login/action.ts

注意带 //import! 的标识 每次导航列表更新时,再触发路由更新action handelFilterRouter 就是根据导航菜单列表 和权限列表 得出路由表的
import { INITSIDEMENUS, UPDATUSERS, LOGINOUT, UPDATROUTES } from "./contant"; import { getSideMenus } from "services/home"; import { loginUser } from "services/login"; import { patchRights } from "services/right-list"; import { handleSideMenu } from "@/utils/devUtils"; import { handelFilterRouter } from "@/utils/routersFilter"; import { message } from "antd"; // 获取导航菜单列表 export const getSideMenusAction = (): any => { return (dispatch: any, state: any) => { getSideMenus().then((res: any) => { const rights = state().login.users.role.rights; const newMenus = handleSideMenu(res, rights); dispatch({ type: INITSIDEMENUS, menus: newMenus }); dispatch(updateRoutesAction()); //import! }); }; }; // 退出登录 export const loginOutAction = (): any => ({ type: LOGINOUT }); // 更新导航菜单 export const updateMenusAction = (item: any): any => { return (dispatch: any) => { patchRights(item).then((res: any) => { dispatch(getSideMenusAction()); }); }; }; // 路由更新 //import! export const updateRoutesAction = (): any => { return (dispatch: any, state: any) => { const rights = state().login.users.role.rights; const menus = state().login.menus; const routes = handelFilterRouter(rights, menus); //import! dispatch({ type: UPDATROUTES, routes }); }; }; // 登录 export const loginUserAction = (item: any, navigate: any): any => { return (dispatch: any) => { loginUser(item).then((res: any) => { if (res.length === 0) { message.error("用户名或密码错误"); } else { localStorage.setItem("token", res[0].username); dispatch({ type: UPDATUSERS, users: res[0] }); dispatch(getSideMenusAction()); navigate("/home"); } }); }; };

utils 工具函数处理

说一说我这里为什么要映射element 成对应组件这部操作,原因是我使用了redux-persist(redux持久化。
若是直接转换后存入本地再取出来渲染是会有问题的,所以需要先将element保存成映射路径,然后渲染前再进行一次路径映射出对应组件。
每个后台的数据返回格式都不一样,需要自己去转换,我这里的转换仅供参考。 ps:defaulyRoutes和默认router/index.ts导出是一样的,可以做个小优化,复用起来。
import { lazy } from "react"; import { Navigate } from "react-router-dom"; // 快速导入工具函数 const lazyLoad = (moduleName: string) => { const Module = lazy(() => import(`views/${moduleName}`)); return <Module />; }; const Appraisal = ({ children }: any) => { const token = localStorage.getItem("token"); return token ? children : <Navigate to="/login" />; }; const defaulyRoutes: any = [ { path: "/login", element: lazyLoad("login"), }, { path: "/", element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>, children: [ { path: "", element: <Navigate to="home" />, }, { path: "*", element: lazyLoad("sand-box/nopermission"), }, ], }, { path: "*", element: lazyLoad("not-found"), }, ]; // 权限列表 和 导航菜单 得出路由表 element暂用字符串表示 后面渲染前再映射 export const handelFilterRouter = ( rights: any, menus: any, routes: any = [] ) => { for (const menu of menus) { if (menu.pagepermisson) { let index = rights.findIndex((item: any) => item === menu.key) + 1; if (!menu.children) { if (index) { const obj = { path: menu.key, element: `sand-box${menu.key}`, }; routes.push(obj); } } else { handelFilterRouter(rights, menu.children, routes); } } } return routes; }; // 返回最终路由表 export const handelEnd = (routes: any) => { defaulyRoutes[1].children = [...routes, ...defaulyRoutes[1].children]; return defaulyRoutes; }; // 映射element 成对应组件 export const handelFilterElement = (routes: any) => { return routes.map((route: any) => { route.element = lazyLoad(route.element); return route; }); };

App.tsx

import routes from "./router"; import { useRoutes } from "react-router-dom"; import { shallowEqual, useSelector } from "react-redux"; import { useState, useEffect } from "react"; import { handelFilterElement, handelEnd } from "@/utils/routersFilter"; import { deepCopy } from "@/utils/devUtils"; function App() { console.log("first"); const [rout, setrout] = useState(routes); const { routs } = useSelector( (state: any) => ({ routs: state.login.routes }), shallowEqual ); const element = useRoutes(rout); // 监听路由表改变重新渲染 useEffect(() => { // deepCopy 深拷贝state数据 不能影响到store里的数据! // handelFilterElement 映射对应组件 // handelEnd 将路由表嵌入默认路由表得到完整路由表 const end = handelEnd(handelFilterElement(deepCopy(routs))); setrout(end); }, [routs]); return <div className="height-all">{element}</div>; } export default App;

路由传参

1. params传参

优点:刷新页面,参数不丢失
缺点:1.只能传字符串,传值过多url会变得很长 2. 参数必须在路由上配置

路由配置

{ path: '/detail/:id/:name', component: Detail },

路由跳转与获取路由参数

import { useHistory,useParams } from 'react-router-dom'; const history = useHistory(); // 跳转路由 地址栏:/detail/2/zora history.push('/detail/2/zora') // 获取路由参数 const params = useParams() console.log(params) // {id: "2",name:"zora"}

2. search传参

优点:刷新页面,参数不丢失
缺点:只能传字符串,传值过多url会变得很长,获取参数需要自定义hooks

路由配置

{ path: '/detail', component: Detail },

路由跳转与获取路由参数

import { useHistory } from 'react-router-dom'; const history = useHistory(); // 路由跳转 地址栏:/detail?id=2 history.push('/detail?id=2') // 或者 history.push({pathname:'/detail',search:'?id=2'}) /** * 自定义hooks用于获取路由参数 * IE11及以下浏览器 不支持浏览器内置的URLSearchParams API **/ function useQuery() { return new URLSearchParams(useLocation().search); } const query = useQuery() const id = query.get('id') //2 /** 自定义hooks */ import { useLocation } from 'react-router-dom'; import qs from 'query-string'; export function useQuery<T = any>(): T { const { search } = useLocation(); return (qs.parse(search) as unknown) as T; } const query = useQuery<IRouteQuery>(); const {id} = query

3. state传参🙌

优点:可以传对象
缺点: <HashRouter>刷新页面,参数丢失

路由配置

{ path: '/detail', component: Detail },

路由跳转与获取路由参数

import { useHistory,useLocation } from 'react-router-dom'; const history = useHistory(); const item = {id:1,name:"zora"} // 路由跳转 history.push(`/user/role/detail`, { id: item }); // 参数获取 const {state} = useLocation() console.log(state) // {id:1,name:"zora"}
笔记
<HashRouter> 不支持 location.keylocation.state<HashRouter>通过state传递参数,刷新页面后参数丢失,官方建议使用<BrowserRouter><BrowserRouter>页面刷新参数也不会丢失。

Router

详细使用示例

注意:这里使用的时最新的 v6 版本,已完全弃用了原先的 react-router 库,较 v5 版本有很大的改动。

一、基本使用

  1. 首先安装依赖
npm i react-router-dom
  1. 引入实现路由所需的组件,以及页面组件
import { BrowserRouter, Routes, Route } from "react-router-dom"; import Foo from "./Foo"; import Bar from "./Bar"; function App() { return ( <BrowserRouter> <Routes> <Route path="/foo" element={<Foo />} /> <Route path="/bar" element={<Bar />} /> </Routes> </BrowserRouter> ); }
  • path:路径
  • element:要渲染的组件
注意:BrowserRouter组件最好放在最顶层所有组件之外,这样能确保内部组件使用 Link 做路由跳转时不出错

二、路由跳转

在跳转路由时,如果路径是/开头的则是绝对路由,否则为相对路由,即相对于当前 URL进行改变

2.1 Link 组件

Link组件只能在Router内部使用,因此使用到Link组件的组件一定要放在顶层的 Router 之内
import { Link } from "react-router-dom"; <Link to="foo">to foo</Link>;

2.2 NavLink 组件

  • NavLink组件Link组件的功能是一致的,区别在于可以判断其to属性是否是当前匹配到的路由
  • NavLink组件styleclassName可以接收一个函数,函数接收一个含有isActive字段的对象为参数,可根据该参数调整样式
import { NavLink } from "react-router-dom"; function Foo() { return ( <NavLink style={({ isActive }) => ({ color: isActive ? "red" : "#fff" })}> Click here </NavLink> ); }

2.3 编程式跳转

使用useNavigate钩子函数生成navigate函数,可以通过 JS 代码完成路由跳转
useNavigate`取代了原先版本中的`useHistory
import { useNavigate } from 'react-router-dom'; function Foo(){ const navigate = useNavigate(); return ( // 上一个路径:/a; 当前路径: /a/a1 <div onClick={() => navigate('/b')}>跳转到/b</div> <div onClick={() => navigate('a11')}>跳转到/a/a1/a11</div> <div onClick={() => navigate('../a2')}>跳转到/a/a2</div> <div onClick={() => navigate(-1)}>跳转到/a</div> ) }
  • 可以直接传入要跳转的目标路由(可以使用相对路径,语法和 JS 相同)
  • 传入1表示后退

四、动态路由参数

4.1 路径参数

  • Route组件中的path属性中定义路径参数
  • 在组件内通过useParams hook 访问路径参数
<BrowserRouter> <Routes> <Route path="/foo/:id" element={<Foo />} /> </Routes> </BrowserRouter>; import { useParams } from "react-router-dom"; export default function Foo() { const params = useParams(); return ( <div> <h1>{params.id}</h1> </div> ); }

路径匹配规则

当URL同时匹配到含有路径参数的路径和无参数路径时,有限匹配没有参数的”具体的“(specific)路径。
<Route path="teams/:teamId" element={<Team />} /> <Route path="teams/new" element={<NewTeamForm />} />
如上的两个路径,将会匹配 teams/new
路径的正则匹配已被移除。

兼容类组件

在以前版本中,组件的props会包含一个match对象,在其中可以取到路径参数。
但在最新的 6.x 版本中,无法从 props 获取参数。
并且,针对类组件的 withRouter 高阶组件已被移除。因此对于类组件来说,使用参数有两种兼容方法:
  1. 将类组件改写为函数组件
  1. 自己写一个 HOC 来包裹类组件,用 useParams 获取参数后通过 props 传入原本的类组件

4.2 search 参数

  • 查询参数不需要在路由中定义
  • 使用 useSearchParams hook 来访问和修改查询参数。其用法和 useState 类似,会返回当前对象和更改它的方法
  • 使用 setSearchParams 时,必须传入所有的查询参数,否则会覆盖已有参数
import { useSearchParams } from "react-router-dom"; // 当前路径为 /foo?id=12 function Foo() { const [searchParams, setSearchParams] = useSearchParams(); console.log(searchParams.get("id")); // 12 setSearchParams({ name: "foo", }); // /foo?name=foo return <div>foo</div>; }

五、嵌套路由

5.1 路由定义

通过嵌套的书写Route组件实现对嵌套路由的定义。
path 开头为 / 的为绝对路径,反之为相对路径。
<Routes> <Route path="/" element={<Home />}></Route> <Route path="/father" element={<Father />}> <Route path="child" element={<Child />}></Route> <Route path=":name" element={<Another />}></Route> </Route> </Routes>

5.2 在父组件中展示

在父组件中使用Outlet来显示匹配到的子组件
import { Outlet } from "react-router-dom"; function Father() { return ( <div> // ... 自己组件的内容 // 留给子组件Child的出口 <Outlet /> </div> ); }

5.3 在组件中定义

可以在任何组件中使用 Routes 组件,且组件内的Routes中,路径默认带上当前组件的路径作为前缀。
注意:此时定义父组件的路由时,要在后面加上 /* ,否则父组件将无法渲染。
<Routes> <Route path="/" element={<Home />} /> <Route path="dashboard/*" element={<Dashboard />} /> </Routes> function Dashboard() { return ( <div> <p>Look, more routes!</p> <Routes> <Route path="/" element={<DashboardGraphs />} /> <Route path="invoices" element={<InvoiceList />} /> </Routes> </div> ); }

六、默认路由

定义: 在嵌套路由中,如果 URL 仅匹配了父级 URL,则Outlet中会显示带有index属性的子路由。可以使用在路由的任何层级
<Routes> <Route path="/foo" element={Foo}> <Route index element={Default}></Route> <Route path="bar" element={Bar}></Route> </Route> </Routes>
  • 当 url 为/foo时:Foo 中的 Outlet 会显示 Default 组件
  • 当 url 为/foo/bar时:Foo 中的 Outlet 会显示为 Bar 组件

七、全匹配路由

定义: path属性取值为*时,可以匹配任何(非空)路径,该匹配拥有最低的优先级。可以用于设置 404 页面。
<Routes> <Route path="/foo" element={Foo}> <Route path="bar" element={Bar}></Route> <Route path="*" element={NotFound}></Route> </Route> </Routes>

八、多组路由

通常,一个应用中只有一个Routes组件。
但根据实际需要也可以定义多个路由出口(如:侧边栏和主页面都要随 URL 而变化)
<Router> <SideBar> <Routes> <Route></Route> </Routes> </SideBar> <Main> <Routes> <Route></Route> </Routes> </Main> </Router>

九、路由重定向

当在某个路径/a下,要重定向到路径/b时,可以通过Navigate组件进行重定向到其他路径
等价于以前版本中的Redirect组件
import { Navigate } from "react-router-dom"; function A() { return <Navigate to="/b" />; }

十、布局路由

当多个路由有共同的父级组件时,可以将父组件提取为一个没有 pathindex 属性的Route组件(Layout Route)
<Route element={<PageLayout />}> <Route path="/privacy" element={<Privacy />} /> <Route path="/tos" element={<Tos />} /> </Route>
这种写法等价于:
<Route path="/privacy" element={ <PageLayout> <Privacy /> </PageLayout> } /> <Route path="/tos" element={ <PageLayout> <Tos /> </PageLayout> } />

十一、订阅和操作 history stack的原理

浏览器会记录导航堆栈,以实现浏览器中的前进后退功能。在传统的前端项目中,URL的改变意味着向服务器重新请求数据。
在现在的客户端路由( client side routing )中,可以做到编程控制URL改变后的反应。如在点击a标签的回调函数中使用 event.preventDefault() 阻止默认事件,此时URL的改变不会带来任何UI上的更新。
<a href="/contact" onClick={(event) => { // stop the browser from changing the URL and requesting the new document event.preventDefault(); // push an entry into the browser history stack and change the URL window.history.pushState({}, undefined, "/contact"); }} />

11.1 History对象

浏览器没有直接提供监听URL改变(push、pop、replace)的接口,因此 react-router 对原生的 history 对线进行了包装,提供了监听URL改变的API。
let history = createBrowserHistory(); history.listen(({ location, action }) => { // this is called whenever new locations come in // the action is POP, PUSH, or REPLACE });
使用 react-router 时不需操作History对象(Routes 组件会进行操作)

11.2 Location对象

react-routerwindow.location 进行包装后,提供了一个形式简洁的Location对象,形如:
{ pathname: "/bbq/pig-pickins", // 主机名之后的URL地址 search: "?campaign=instagram", // 查询参数 hash: "#menu", // 哈希值,用于确定页面滚动的具体位置 state: null, // 对于 window.history.state 的包装 key: "aefz24ie" // }

state

不显示在页面上,不会引起刷新,只由开发人员操作。
可用于记录用户的跳转详情(从哪跳到当前页面)或在跳转时携带信息。
可以用在 Link 组件或 navigate 方法中
<Link to="/pins/123" state={{ fromDashboard: true }} /> let navigate = useNavigate(); navigate("/users/123", { state: partialUser });
在目标的组件中,可以用 useLocation 方法获取该对象
let location = useLocation(); console.log(location.state);
state中的信息会进行序列化,因此如日期对象等信息会变为string

key

每个Location对象拥有一个唯一的key,可以据此来实现基于Location的滚动管理,或是数据缓存。
如:将 location.key 和 URL 作为键,每次请求数据前,先查找缓存是否存在来判断是否实际发送请求,来实现客户端数据缓存。

十二、 各类Router组件

12.1 HashRouter和BrowserRouter的区别

  • HashRouter 只会修改URL中的哈希值部分;而 BrowserRouter 修改的是URL本身
  • HashRouter纯前端路由,可以通过输入URL直接访问;使用时 BrowserRouter 直接输入URL会显示404,除非配置Nginx将请求指向对应的HTML文件。初次进入 / 路径时或点击 Link 组件跳转时不会发送请求

12.2 unstable_HistoryRouter

使用 unstable_HistoryRouter 需要传入一个 history 库的实例,这将允许在非react作用于下操作history对象。
由于项目使用的history和react-router中使用的history版本可能不一样,该API目前标为unstable状态

12.3 MemoryRouter

HashRouterBrowserRouter 都是依据外部对象(history)进行导航,而 MemoryRouter 则是自己存储和管理状态堆栈,多用于测试场景。

12.4 NativeRouter

推荐的用于 React Native的Router组件

12.5 StaticRouter

在nodejs端使用,渲染react应用。
import * as React from "react"; import * as ReactDOMServer from "react-dom/server"; import { StaticRouter } from "react-router-dom/server"; import http from "http"; function requestHandler(req, res) { let html = ReactDOMServer.renderToString( <StaticRouter location={req.url}> {/* The rest of your app goes here */} </StaticRouter> ); res.write(html); res.end(); } http.createServer(requestHandler).listen(3000);

十三、使用JS对象定义路由(路由表)

使用 useRoutes hook,可以使用一个JS对象而不是Routes组件与Route组件来定义路由。其功能类似于react-router-config(opens new window)
useRoutes 的返回是 React Element,或是 null。
此功能用来管理路由表,相比v5,可能需要借助一些第三方库来实现路由config管理,现在v6版本自带

(一) 创建路由表

const routes = useRoutes([ { path: '/profile', element: <Profile/> }, { path: '/message', element: <Message/> children: [ // 嵌套路由,需在组件中使用 Outlet组件 进行占位 { path: 'message1', element: <Message1/> }, { path: 'message2', element: <Message2/> children:[ { // 匹配动态路由 path: 'message1/:id', element: <MessageDetail/> } ] } ] }, { path: '/', element: <Navigate to='/profile'/> }, { path: '/user', children: [ { index: true, element: <h1>user~</h1> // 这种不属于嵌套路由,这里面children会放到父亲的位置,所以不需要配合Outlet组件使用 } ] } ])
对于传入的配置对象, 其类型定义如下:
interface RouteObject { caseSensitive?: boolean; children?: RouteObject[]; element?: React.ReactNode; index?: boolean; path?: string; }
提示
也可以单独创建 routes 文件夹管理路由表,然后在组件内使用 useRoutes

(二) 组件结构内使用函数返回值作为占位符

return ( <div> <ul className="nav nav-pills"> {/*路由导航区*/} <li role="presentation"><NavLink to="/profile" >Profile</NavLink></li> <li role="presentation"><NavLink to="/message">Messages</NavLink></li> </ul> {/*路由展示区*/} <h1> {routes} </h1> </div>

(三) 嵌套路由

在一级路由下面创建二级子路由
  1. 引入路由路由占位符OutLet到组件中
  1. 引入NavLink到组件中改变路径,子路由路径可以不需要加/
import React from 'react' import { NavLink, Outlet } from 'react-router-dom' export default function Message() { return ( <div> <ul> <li> <NavLink to='message1'>message1</NavLink> </li> <li> <NavLink to='message2'>message2</NavLink> </li> </ul> {/*路由占位符*/} <Outlet/> </div> ) }
注意
不要忘记使用Outlet作为占位符

(四) 路由懒加载

import { lazy } from 'react' const Message1 = lazy(() => import('../pages/LoginPage')) // 路由懒加载 // 在路由表中直接使用即可 export default [ { path: '/login', element: <LoginPage/> } ]

十四、其他hooks函数

直接在组件中调用即可

useRouterContext
作用:判断组件是否被路由包裹

useNavigationType
作用:判断路由跳转方式
返回值:push pop replace
pop是在浏览器中直接打开了这个路由的组件

useOutlet
作用:查看组件下级路由信息
组件若未被挂载则返回null,若以被挂载,则返回该子组件信息

useResolvePath
作用:解析路径
useResolvedPath("https://www.bilibili.com/video/BV1wy4y1D7JT?p=140&spm_id_from=pageDriver")

动态路由

router/index.ts 默认路由

import { lazy } from "react"; import { Navigate } from "react-router-dom"; // React 组件懒加载 // 快速导入工具函数 const lazyLoad = (moduleName: string) => { const Module = lazy(() => import(`views/${moduleName}`)); return <Module />; }; // 路由鉴权组件 const Appraisal = ({ children }: any) => { const token = localStorage.getItem("token"); return token ? children : <Navigate to="/login" />; }; interface Router { name?: string; path: string; children?: Array<Router>; element: any; } const routes: Array<Router> = [ { path: "/login", element: lazyLoad("login"), }, { path: "/", element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>, children: [ { path: "", element: <Navigate to="home" />, }, { path: "*", element: lazyLoad("sand-box/nopermission"), }, ], }, { path: "*", element: lazyLoad("not-found"), }, ]; export default routes;

redux login/action.ts

注意带 //import! 的标识 每次导航列表更新时,再触发路由更新action handelFilterRouter 就是根据导航菜单列表 和权限列表 得出路由表的
import { INITSIDEMENUS, UPDATUSERS, LOGINOUT, UPDATROUTES } from "./contant"; import { getSideMenus } from "services/home"; import { loginUser } from "services/login"; import { patchRights } from "services/right-list"; import { handleSideMenu } from "@/utils/devUtils"; import { handelFilterRouter } from "@/utils/routersFilter"; import { message } from "antd"; // 获取导航菜单列表 export const getSideMenusAction = (): any => { return (dispatch: any, state: any) => { getSideMenus().then((res: any) => { const rights = state().login.users.role.rights; const newMenus = handleSideMenu(res, rights); dispatch({ type: INITSIDEMENUS, menus: newMenus }); dispatch(updateRoutesAction()); //import! }); }; }; // 退出登录 export const loginOutAction = (): any => ({ type: LOGINOUT }); // 更新导航菜单 export const updateMenusAction = (item: any): any => { return (dispatch: any) => { patchRights(item).then((res: any) => { dispatch(getSideMenusAction()); }); }; }; // 路由更新 //import! export const updateRoutesAction = (): any => { return (dispatch: any, state: any) => { const rights = state().login.users.role.rights; const menus = state().login.menus; const routes = handelFilterRouter(rights, menus); //import! dispatch({ type: UPDATROUTES, routes }); }; }; // 登录 export const loginUserAction = (item: any, navigate: any): any => { return (dispatch: any) => { loginUser(item).then((res: any) => { if (res.length === 0) { message.error("用户名或密码错误"); } else { localStorage.setItem("token", res[0].username); dispatch({ type: UPDATUSERS, users: res[0] }); dispatch(getSideMenusAction()); navigate("/home"); } }); }; };

utils 工具函数处理

说一说我这里为什么要映射element 成对应组件这部操作,原因是我使用了redux-persist(redux持久化。
若是直接转换后存入本地再取出来渲染是会有问题的,所以需要先将element保存成映射路径,然后渲染前再进行一次路径映射出对应组件。
每个后台的数据返回格式都不一样,需要自己去转换,我这里的转换仅供参考。 ps:defaulyRoutes和默认router/index.ts导出是一样的,可以做个小优化,复用起来。
import { lazy } from "react"; import { Navigate } from "react-router-dom"; // 快速导入工具函数 const lazyLoad = (moduleName: string) => { const Module = lazy(() => import(`views/${moduleName}`)); return <Module />; }; const Appraisal = ({ children }: any) => { const token = localStorage.getItem("token"); return token ? children : <Navigate to="/login" />; }; const defaulyRoutes: any = [ { path: "/login", element: lazyLoad("login"), }, { path: "/", element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>, children: [ { path: "", element: <Navigate to="home" />, }, { path: "*", element: lazyLoad("sand-box/nopermission"), }, ], }, { path: "*", element: lazyLoad("not-found"), }, ]; // 权限列表 和 导航菜单 得出路由表 element暂用字符串表示 后面渲染前再映射 export const handelFilterRouter = ( rights: any, menus: any, routes: any = [] ) => { for (const menu of menus) { if (menu.pagepermisson) { let index = rights.findIndex((item: any) => item === menu.key) + 1; if (!menu.children) { if (index) { const obj = { path: menu.key, element: `sand-box${menu.key}`, }; routes.push(obj); } } else { handelFilterRouter(rights, menu.children, routes); } } } return routes; }; // 返回最终路由表 export const handelEnd = (routes: any) => { defaulyRoutes[1].children = [...routes, ...defaulyRoutes[1].children]; return defaulyRoutes; }; // 映射element 成对应组件 export const handelFilterElement = (routes: any) => { return routes.map((route: any) => { route.element = lazyLoad(route.element); return route; }); };

App.tsx

import routes from "./router"; import { useRoutes } from "react-router-dom"; import { shallowEqual, useSelector } from "react-redux"; import { useState, useEffect } from "react"; import { handelFilterElement, handelEnd } from "@/utils/routersFilter"; import { deepCopy } from "@/utils/devUtils"; function App() { console.log("first"); const [rout, setrout] = useState(routes); const { routs } = useSelector( (state: any) => ({ routs: state.login.routes }), shallowEqual ); const element = useRoutes(rout); // 监听路由表改变重新渲染 useEffect(() => { // deepCopy 深拷贝state数据 不能影响到store里的数据! // handelFilterElement 映射对应组件 // handelEnd 将路由表嵌入默认路由表得到完整路由表 const end = handelEnd(handelFilterElement(deepCopy(routs))); setrout(end); }, [routs]); return <div className="height-all">{element}</div>; } export default App;

路由传参

1. params传参

优点:刷新页面,参数不丢失
缺点:1.只能传字符串,传值过多url会变得很长 2. 参数必须在路由上配置

路由配置

{ path: '/detail/:id/:name', component: Detail },

路由跳转与获取路由参数

import { useHistory,useParams } from 'react-router-dom'; const history = useHistory(); // 跳转路由 地址栏:/detail/2/zora history.push('/detail/2/zora') // 获取路由参数 const params = useParams() console.log(params) // {id: "2",name:"zora"}

2. search传参

优点:刷新页面,参数不丢失
缺点:只能传字符串,传值过多url会变得很长,获取参数需要自定义hooks

路由配置

{ path: '/detail', component: Detail },

路由跳转与获取路由参数

import { useHistory } from 'react-router-dom'; const history = useHistory(); // 路由跳转 地址栏:/detail?id=2 history.push('/detail?id=2') // 或者 history.push({pathname:'/detail',search:'?id=2'}) /** * 自定义hooks用于获取路由参数 * IE11及以下浏览器 不支持浏览器内置的URLSearchParams API **/ function useQuery() { return new URLSearchParams(useLocation().search); } const query = useQuery() const id = query.get('id') //2 /** 自定义hooks */ import { useLocation } from 'react-router-dom'; import qs from 'query-string'; export function useQuery<T = any>(): T { const { search } = useLocation(); return (qs.parse(search) as unknown) as T; } const query = useQuery<IRouteQuery>(); const {id} = query

3. state传参🙌

优点:可以传对象
缺点: <HashRouter>刷新页面,参数丢失

路由配置

{ path: '/detail', component: Detail },

路由跳转与获取路由参数

import { useHistory,useLocation } from 'react-router-dom'; const history = useHistory(); const item = {id:1,name:"zora"} // 路由跳转 history.push(`/user/role/detail`, { id: item }); // 参数获取 const {state} = useLocation() console.log(state) // {id:1,name:"zora"}
笔记
<HashRouter> 不支持 location.keylocation.state<HashRouter>通过state传递参数,刷新页面后参数丢失,官方建议使用<BrowserRouter><BrowserRouter>页面刷新参数也不会丢失。
React笔记-ReduxReact笔记-案例演示
慕雨
慕雨
一个普通的干饭人🍚
公告
🎉欢迎来到慕雨的博客小站🎉
这里是我的个人学习、生活记录
--- 免责声明 ---
⚠️ 本站内容仅代表个人观点
⚠️ 本站内容仅供学习参考使用
 
目录
0%
目录
0%