React-router-dom 6 总结
通过Routes标签创建路由表
<Routes>
{/* index 表示当前路由的默认路由 */}
<Route index element={<Navigate to="/projects" replace />} />
<Route path="/projects" element={<ProjectListScreen />} />
<Route path="/projects/:projectId/*" element={<ProjectScreen />} />
</Routes>
路由指定组件的参数从 component 换成了 element,过去的 component 传递的是组件名,现在需要传递的是 JSX 标签,这点需要注意。
在 <Route> 标签中,可以通过申明 index 属性,来进行默认导航,上面的示例就是将根路由默认导航到 /projects,<Navigate> 标签申明 replace 属性,可以实现路由栈替换,避免路由表可以无限回退。
嵌套路由新写法 Outlet
在过去我们写嵌套路由的时候,一般会在子页面再次通过路由表标签来创建路由,实现嵌套路由,现在我们可以使用 <Outlet> 标签,在子页面作为占位标志,当导航到该页面的子路由时,将子路由对应的组件填充至占位。
路由传参的几种方式
-
Search传参,类似于url中的
?xxx=yyy&zzz=rrr<Link to={`/home/message/detail?id=${message.id}&title=${message.title}`}>{message.title}</Link> -
Params 传参,需要在路由声明接受参数 类似于restful,
/:id/:name/:gender<Link to={`/home/message/detail/${message.id}/${message.title}`}>{message.title}</Link> {/* 注意对应的路由申明 */} <Route path="/home/message/detail/:id/:title" element={<Detail/>} /> -
state传参
//v6 中的使用方式 to 不再是一个路由字符串,而是一个对象 {/* 方式3:通过state传递参数,在location.state中获取,link中的to传递为一个对象,必须包含pathname与state字段属性 */} <Link to={{ pathname: '/home/message/detail', state: message }}>{message.title}</Link>
V6的注意点
-
移除了
<Switch>标签,使用<Routes>标签替代 -
引入了
useRoutes([])钩子用于根据路由表创建根路由,其返回值是dom,用{}包裹,放置于<APP>节点下即可配置全部一级路由 -
路由表的创建模式
// import About from "../pages/About"; // import Home from "../pages/Home"; import News from "../pages/Home/News"; import Message from "../pages/Home/Message"; import { Navigate } from "react-router-dom"; import Detail from "../pages/Home/Message/Detail"; import { lazy } from "react"; const About = lazy(() => import("../pages/About")); const Home = lazy(() => import("../pages/Home")); export const routes = [ { path: "/", element: <Navigate to={"/a/about"} /> }, { path: "/a/about", element: <About /> }, { path: "/home", element: <Home />, children: [ //如果称`/`为根路由,那么`/home`就是一级子路由,`/home/news`就是二级子路由,我们希望打开home时帮我们导航到news //既可以在children中写一个空字符串的二级子路由,并将它通过Navigate的to属性指定为`/home/news`,就可以实现 // { path: "", element: <Navigate to={"news"} /> }, //我们还可以直接使用index:true,配合<Navigate>标签实现默认导航 { index: true, element: <Navigate to={"news"} /> }, { path: "news", element: <News /> }, { path: "message", element: <Message />, children: [ //使用search传参,或者state传参 { path: "detail", element: <Detail />, }, //使用params传参 // { // path: "detail/:id/:title", // element: <Detail />, // }, ], }, ], }, ]; -
子组件不需要再使用钩子创建路由,路由框架通过识别子组件的
<Outlet/>以及路由表的children关系,可以知道往什么地方渲染子组件,这一点非常方便 -
<NavLink>标签取消了activeClassName,className可以赋值一个函数,系统会为该函数传递一个{isActive:true}的对象作为参数; -
在@5中的几个钩子大多数废弃,例如:
useHistory、useRouteMatch()已经废弃或移除,useLocation保留,新增加了useParams,非常好用,state传参与params传参都有简化 -
我们无法再获取
useHistory钩子再获取history对象,但是可以使用useNavigate钩子获取navigate对象,在使用navigate对象导航时也与history不同:const nav = useNavigate() //此处可以不写全路径,只写子路由也可以成功导航 nav(`/home/message/detail`, { replace: true, state: message }) //傻瓜式前进后退 nav(-1) nav(1)nav函数的参数二接受的是一个固定类型的对象,不要随便传(TS在工程化的必要性?)
-
我们依旧可以使用React提供的lazy函数,只不过现在需要在路由表中声明
// import About from "../pages/About"; // import Home from "../pages/Home"; import News from "../pages/Home/News"; import Message from "../pages/Home/Message"; import { Navigate } from "react-router-dom"; import Detail from "../pages/Home/Message/Detail"; import { lazy } from "react"; const About = lazy(() => import("../pages/About")); const Home = lazy(() => import("../pages/Home")); export const routes = [ { path: "/", element: <Navigate to={"/a/about"} /> }, { path: "/a/about", element: <About /> }, { path: "/home", element: <Home />, children: [ //如果称`/`为根路由,那么`/home`就是一级子路由,`/home/news`就是二级子路由,我们希望打开home时帮我们导航到news //既可以在children中写一个空字符串的二级子路由,并将它通过Navigate的to属性指定为`/home/news`,就可以实现 // { path: "", element: <Navigate to={"news"} /> }, //我们还可以直接使用index:true,配合<Navigate>标签实现默认导航 { index: true, element: <Navigate to={"news"} /> }, { path: "news", element: <News /> }, { path: "message", element: <Message />, children: [ //使用search传参,或者state传参 { path: "detail", element: <Detail />, }, //使用params传参 // { // path: "detail/:id/:title", // element: <Detail />, // }, ], }, ], }, ];Tips:不要忘记在根路由组件中使用
<Suspense fallback={<h2>Loading</h2>}>{elements}</Suspense>包裹住根路由表。 -
<NavLink>新增加end关键字,当在上一级路由的link标签中使用end关键字,当他的下级路由被点击,他将失去激活状态。 -
路由鉴权,使用高阶组件包一下需要鉴权的页面,如果不能鉴权通过,则跳转,能鉴权通过则使用渲染:
import React from 'react' import { Fragment } from 'react'; import { Navigate } from 'react-router-dom'; import useLocalStorage from '../hooks/useLocalStorage' //一个简单的鉴权操作 export const AuthWrapComponent = ({ children }) => { const [token, _] = useLocalStorage('token'); return ( <Fragment> {/* 注意一定要replace ,导航到404空白页 或者 401无权*/} {token ? children : <Navigate to='/a/about' replace />} </Fragment> ) }
几个钩子的使用
useLocation: 获取当前路径,state传参时可以拿到 state 对象,还可以通过 patchname 拿到当前路由路径(不包含?)useSearchParams:获取当前查询路径对象,需要使用get获取对应key的值useParams:拿到 params传参的对象
简单的一个示例:
const Detail = (props) => { // 方式1:通过search传递参数,比较复杂,不推荐, //需要用解构赋值获得search对象,然后调用get函数获取对应的key的值 // const [search] = useSearchParams() // const detail = { id: search.get('id'), title: search.get('title') } // 方式2:通过params参数传递,非常简单直接使用useParams() // const detail = useParams() // 方式3:通过state传递参数 // const { state: detail } = useLocation(); // console.log("此处可以收到来自route的state参数:", detail); //方式1+plus:自定义的钩子根据传入的key数组拿到search中的数据并包装成对象 const detail = useUrlQueryParam(['id', 'title']) console.log("获取到对象:", detail); return ( <ul> <li>ID:{detail.id}</li> <li>TITLE:{detail.title}</li> <li>CONTENT:{detail.title}+{detail.id}</li> </ul> ) } export default Detail //js下极简的封装 export const useUrlQueryParam = (keys) => { const [search] = useSearchParams() //遍历keys,从search中获取对应的值,返回一个新对象 const query = keys.reduce((acc, key) => { acc[key] = search.get(key) return acc }, {}) return query }例如这样一个URL :http://localhost:3000/projects/1/kanban?id=1
在使用两个钩子的效果:
const { pathname } = useLocation() const [urlparam] = useSearchParams() console.log(pathname, urlparam); //### : /projects/1/kanban id=1可以看出他们是术业有专攻的
在 TypeScript 下封装 useSearchParams
由于 useSearchParams 钩子的不易用,不能直接返回我们需要的对象,我们可以自行封装如下钩子,帮助我们获取对象:
import { useMemo, useState } from "react" import { URLSearchParamsInit, useSearchParams } from "react-router-dom" export const isVoid = (value: unknown) => value === undefined || value === null || value === ""; export const isNullOrUndefined = (value: unknown) => value === undefined || value === null; //在js中函数传入对象是一个不好的方式,因为函数可能会污染对象 export const cleanObject = (obj?: { [key: string]: unknown }) => { //空对象直接返回 if (!obj) return {}; //浅拷贝 const result = { ...obj }; //清除对象中的空值 Object.keys(result).forEach((key) => { const value = result[key]; if (isVoid(value)) { delete result[key]; } }); return result; }; /** * 传入一个对象,和键集合,返回对应的对象中的键值对 * @param obj * @param keys */ export const subset = <O extends { [key in string]: unknown }, K extends keyof O>(obj: O, keys: K[]) => { const filteredEntries = Object.entries(obj).filter(([key]) => keys.includes(key as K) ); return Object.fromEntries(filteredEntries) as Pick<O, K>; }; //获取查询参数 => 转换成对象 export const useUrlQueryParam = <K extends string>(keys: K[]) => { const [searchParams] = useSearchParams() const setSearchParams = useSetUrlSearchParam() const [stateKeys] = useState(keys) return [ useMemo( () => subset(Object.fromEntries(searchParams), stateKeys) as { [key in K]: string }, [searchParams, stateKeys] ), (params: Partial<{ [key in K]: unknown }>) => setSearchParams(params) ] as const }; //专门用来修改url的查询参数的钩子 export const useSetUrlSearchParam = () => { const [searchParam, setSearchParam] = useSearchParams() return (params: Partial<{ [key in string]: unknown }>) => { const o = cleanObject({ ...Object.fromEntries(searchParam), ...params }) as URLSearchParamsInit return setSearchParam(o) } }; ---------- //使用: const [param, setParam] = useUrlQueryParam(['name', 'personId'])