本文记录下在使用 react-router-dom v6时遇到的一些问题。
react-router-dom 的 v6 版本,做了一些修改
- 引入了 Hooks API,使得在函数组件中更方便地使用路由功能。比如 useNavigate、useParams、useSearchParams、useLocation 等
- 引入了 Routes 组件来替代 v5 中的 Switch 组件,可以更灵活地定义路由匹配规则。
- Route 组件 element 属性替代 v5 中的 component 属性,用于指定匹配路由时要渲染的元素。
- 引入了 navigate 方法取代了 v5 中的 history,,用于在函数组件中进行编程式导航。
- 不再直接支持 query 参数,而是鼓励使用URL搜索参数(search params)来处理查询参数。
常用 Hooks API
useNavigate
做页面跳转等操作用的,替代 v5 的 history。不再支持 query 方式。
const navigate = useNavigate();
// 跳转到某个页面
navigate('/Demo');
// 跳转到某个页面并携带参数
navigate('/Demo', {
state: {
name: 'wmm66',
age: 18,
}
});
// 替换某个页面
navigate('/Demo', {
replace: true,
state: {
name: 'wmm66',
}
});
// 页面回退
navigate(-1);
useLocation
页面中获取 location 参数用的。
注意:这种方式获取的 location 和 window.location 并不一样。
useParams
获取 params 参数用的。
在路由路径中定义动态参数,示例如下
<Route path="/user/:id" element={<User />} />
在 User 组件中使用 useParams 就可以获取到 id 参数的值。
useSearchParams
这是 v6 新加的,获取和设置 searchParams 参数用的。
const [searchParams, setSearchParams] = useSearchParams();
const query = searchParams.get('query');
setSearchParams({ query: 'react' });
实际上是获取的 hash 中 ?后面的参数。比如页面路径是 http://localhost:5173/#/Demo/About?name=wmm66&age=18
我们就可以用 useSearchParams 来获取
const [searchParams] = useSearchParams();
console.log(searchParams.get('name'));
路由自动遍历配置
我们以在 Vite 中为例。Vite 中有 import.meta.glob 方法可以遍历获取某个目录下的文件。我们可以借助此方法实现。
import { lazy, Suspense, Fragment } from 'react';
import { HashRouter, Route, Routes } from 'react-router-dom';
import PageLoading from '@/components/PageLoading';
import PageNotFound from '@/components/PageNotFound';
// 遍历 pages 目录下的所有 tsx 文件
const pages = import.meta.glob('../pages/**/*.ts?(x)');
const routes = Object.keys(pages).reduce((ret: any[], path: string) => {
// 去掉 尾部的 .tsx 和 前面的 ../pages
const arr = path.replace(/.tsx?$/, '').split('/').slice(2);
// 没有,直接return
if (arr.length === 0) return ret;
// 过滤目录
const arrDir = arr.slice(0, - 1);
const excludeDirs = ['hooks', 'components'];
for (let i = 0; i < excludeDirs.length; i++) {
if (arrDir.includes(excludeDirs[i])) return ret;
}
// 过滤文件 && hook
const fileName = arr[arr.length - 1];
const excludeFiles = ['model'];
if (excludeFiles.includes(fileName)) return ret;
if (fileName.slice(0, 3) === 'use') return ret;
// 对应的组件
const Component = lazy(pages[path] as any);
// 组合成 path
const routePath = arr.reduce((ret, item) => {
return `${ret}/${item}`;
}, '');
// index 做下 兼容
if (arr[arr.length - 1] === 'index') {
ret.push({
path: routePath.slice(0, -6) || '/',
Component,
});
}
ret.push({
path: routePath,
Component,
});
return ret;
}, []);
export default function Router() {
return (
<HashRouter>
<Suspense fallback={<PageLoading />}>
<Routes>
{routes.map((route) => {
const { path, Component } = route ?? {};
return (
<Fragment key={path}>
{!!path && !!Component && (
<Route
key={path}
path={path}
element={<Component />}
/>
)}
</Fragment>
);
})}
<Route path='*' element={<PageNotFound />} />
</Routes>
</Suspense>
</HashRouter>
);
}
路由跳转
路由跳转我们一般使用 useNavigate 跳转,方式如下
import { useNavigate } from 'react-router-dom';
// ...
export default function DemoPage() {
const navigate = useNavigate();
// ...
const toAboutPage = () => {
navigate('/Demo/About', {
state: {
name: 'wmm66',
age: 18,
},
});
}
// ...
}
在目标页面使用 useLocation 获取传递的参数
import { useLocation } from 'react-router-dom';
const About = () => {
const location = useLocation();
console.warn('location: ', location, location.state);
return <div>demo About Page</div>;
};
export default About;
这种方式有个缺点是:在目标页面的URL中并没有看到传递的参数。
比如我们项目的某个页面访问时需要携带参数。就没法使用 url 直接打开。
我们这里采用另外一种方式(searchParams方式)做页面跳转。
import { useNavigate } from 'react-router-dom';
// ...
export default function DemoPage() {
const navigate = useNavigate();
// ...
const toAboutPage = () => {
navigate('/Demo/About?name=wmm66&age=18');
}
// ...
}
我们写一个方法包装一下
// utils/navigate.ts
import { NavigateFunction } from 'react-router-dom';
type CombineUrlQueryParams = Record<string, string | number>;
export function combineUrlQuery(url: string, params?: CombineUrlQueryParams) {
if (!params) return url;
const queryStr = Object.keys(params).map((key: string) => {
return `${key}=${params[key]}`;
}).join('&');
if (!queryStr) return url;
return `${url}?${queryStr}`;
}
// 页面跳转
export function navigateTo(navigate: NavigateFunction) {
return function(url: string, params?: CombineUrlQueryParams) {
navigate(combineUrlQuery(url, params));
}
}
// demo页面中使用
import { useNavigate } from 'react-router-dom';
import { navigateTo } from '@/utils/navigate';
// ...
export default function DemoPage() {
const navigate = useNavigate();
// ...
const toAboutPage = () => {
// navigate('/Demo/About?name=wmm66&age=18');
navigateTo(navigate)('/Demo/About', {
name: 'wmm66',
age: 18,
});
}
// ...
}
在目标页面中,我们使用 useSearchParams 的方式获取,这里也同样包装下。
可以采用上面类似的科里化的方式包装,这里直接写个Hook 吧
// hooks/useSearchQuery.ts
import { useSearchParams } from 'react-router-dom';
export default function useSearchQuery() {
const [searchParams] = useSearchParams();
const res: Record<string, any> = {};
searchParams.forEach((value: string, key: string) => {
if (typeof res[key] !== 'undefined') {
res[key] += ',' + value;
} else {
res[key] = value;
}
});
return res;
}
// 目标页面中使用
import useSearchQuery from '@/hooks/useSearchQuery';
const About = () => {
const query = useSearchQuery();
console.warn('query', query);
return <div>demo About Page</div>;
};
export default About;
当然我们也可以不用 useSearchParams,自己写一个方法获取。
// utils/index.ts
export const objectUrlQuery = (_str?: string) => {
const hash = _str || window.location.hash;
const search = hash.split('?')[1];
if (!search) return;
const query: any = {};
search.replace(/\\b([^&=]*)=([^&]*)/g, (m, key, value): any => {
if (typeof query[key] !== 'undefined') {
query[key] += ',' + decodeURIComponent(value);
} else {
query[key] = decodeURIComponent(value);
}
});
return query;
};
// 目标页面中使用
import { objectUrlQuery } from '@/utils';
const About = () => {
const query = objectUrlQuery();
console.warn('query', query);
return <div>demo About Page</div>;
};
export default About;