项目搭建中碰到的一个路由嵌套的问题,想象中很简单,用的时候却老是路由匹配不上。在此记录一下用法。
嵌套路由的实现
先说一下需求,有一个上中下页面布局的页面,Header为固定的菜单, Content(中下)为菜单选中的页面,Detail(中)为展示的内容详情,Tabbar(下)为需要做Hash路由的嵌套页面;
路由示例:http://localhost:8095/reactive/123/detail#/eventLog
不废话,上代码
route.tsx
import * as React from 'react';
import { BrowserRouter, Route, Redirect, HashRouter, Switch } from 'react-router-dom';
import Header from '@packages/@core/common/containers/header/header';
import Content from './containers/content';
import tabbars from './routes';
const defaultPath = '/:menu/:id/detail';
const defaultUrl = '/reactive/123/detail#/eventLog';
const WoDetailEntry = () => {
return (
<React.Suspense fallback={null}>
<Header />
<BrowserRouter>
<Switch>
<Route key={defaultPath} path={`${defaultPath}`} >
<Content>
<HashRouter>
<Switch>
{
tabbars.map(x => {
return <Route
key={`${x.path}`}
path={`${x.path}`}
component={x.component}
/>;
})
}
</Switch>
</HashRouter>
</Content>
</Route>
<Route render={() => <Redirect to={defaultUrl} />} />
</Switch>
</BrowserRouter>
</React.Suspense>
);
};
component.ts
const components = [
{
path: '/communication',
component: Communication
},{
path: '/eventLog',
component: EventLog
}
];
export default components;
content.tsx
const Content = props => {
return <div>
<Detail />
{props.children}
</div>;
};
export default Content;
需要注意的地方:
- HashRouter只会与你的location.hash去匹配,所以在配置route的path时,应该为hash值的部分,且需要以/开头
- 在使用嵌套路由是,如果用
<Route component={ParentComponent}>...nestRouters</Route>
的方式, 不能将子路由的组件传递到父路由组件的children属性中。推荐使用<Route><ParentComponent>...nestRouters</ParentCompoent></Route>
的方式
react-router-dom v5.1.0中的新加入的Hook Api
如果你想使用真香的Hook Api, 请把react-router-dom至少升级到v5.1.0, 目前有四个钩子可以使用
- useHistory
- useLocation
- useParams
- useRouteMatch
下面挪用官方的例子介绍一下Api的使用
useHistory
import { useHistory } from "react-router-dom";
function HomeButton() {
let history = useHistory();
function handleClick() {
history.push("/home");
}
return (
<button type="button" onClick={handleClick}>
Go home
</button>
);
}
useLocation
import React from "react";
import ReactDOM from "react-dom";
import {
BrowserRouter as Router,
Switch,
useLocation
} from "react-router-dom";
function usePageViews() {
let location = useLocation();
React.useEffect(() => {
ga.send(["pageview", location.pathname]);
}, [location]);
}
function App() {
usePageViews();
return <Switch>...</Switch>;
}
ReactDOM.render(
<Router>
<App />
</Router>,
node
);
useParams
import React from "react";
import ReactDOM from "react-dom";
import {
BrowserRouter as Router,
Switch,
Route,
useParams
} from "react-router-dom";
function BlogPost() {
let { slug } = useParams();
return <div>Now showing post {slug}</div>;
}
ReactDOM.render(
<Router>
<Switch>
<Route exact path="/">
<HomePage />
</Route>
<Route path="/blog/:slug">
<BlogPost />
</Route>
</Switch>
</Router>,
node
);
useRouteMatch
import { Route } from "react-router-dom";
function BlogPost() {
return (
<Route
path="/blog/:slug"
render={({ match }) => {
// Do whatever you want with the match...
return <div />;
}}
/>
);
}
you can just import { useRouteMatch } from "react-router-dom";
function BlogPost() {
let match = useRouteMatch("/blog/:slug");
// Do whatever you want with the match...
return <div />;
}
useRouteMatch 和 useParams 的实际应用
下面把useRouteMatch和useParams加入到我的工程中
content.tsx
import { useParams } from 'react-router-dom';
const Content = props => {
const { menu, id } = useParams();
return <div>
<Detail menu={menu} id={id} />
{props.children}
</div>;
};
export default Content;
直接通过useParams可以到路由匹配到的参数menu, id。如http://localhost:8095/reactive/123/detail#/eventLog, 获取到的menu = reactive, id = 123
route.tsx
const WoDetailEntry = () => {
return (
<React.Suspense fallback={null}>
<Header />
<BrowserRouter>
<Switch>
<Route key={defaultPath} path={`${defaultPath}`} >
<Content />
</Route>
<Route render={() => <Redirect to={defaultUrl} />} />
</Switch>
</BrowserRouter>
</React.Suspense>
);
};
这里将嵌套路由放到Content内部中处理
Content.tsx
import { useParams } from 'react-router-dom';
const Content = props => {
const { menu, id } = useParams();
const { path, url } = useRouteMatch();
return <div>
<Detail menu={menu} id={id} />
<BrowserRouter>
<Switch>
{
routes.map(x => {
return <Route
key={`${path}${x.path}`}
path={`${path}${x.path}`}
component={x.component}
/>;
})
}
</Switch>
</BrowserRouter>
</div>;
};
export default Content;
将嵌套路由配置在Content中, 此时子路由Route的path需要配置为全路径才能匹配上, 因此需要拿到父组件Content的路由匹配,这里可以使用useRouteMatch直接拿到Content的match属性。
----------------------------------------------分割线---------------------------------------------
--------------------------------------------2020/10/30------------------------------------------
更新: 二级路由页面下需要再规划路由
先说场景,某个tab页面是一个列表页面,列表页面要跳转详情页面,因此需要在二级路由的tab页面在规划下级页面的路由。
探索HashRouter嵌套BrowserRouter
刚开始我想着是不是直接在hashRouter下面直接嵌套一个browserRouter就好了,来不及多想,老夫直接一梭子
const TabAComponent = () => {
return <BrowserRouter>
<Switch>
{
tabARoutes.map(x => (
<Route key={x.key} path={x.path} component={x.component} />
))
}
</Switch>
</BrowserRouter>
}
发现匹配不上hash路由下面的browser路由, 又换了几种嵌套的方式, 还是没办法匹配上子路由
是否需要再去嵌套BrowserRouter?
碰了壁之后思考一下是否需要这样嵌套,如果又有子级页面那不是无线嵌套,俄罗斯套娃了。
其实都是用hash路由就能解决hash路由下的所有子级页面的路由问题。只需要使用相同的路由前缀就ok了。
const history = useHistory();
const [index, setIndex] = useState(0);
useEffect(() => {
if (hash === '#/' || isEmptyValue(hash)) {
setHash(0);
setIndex(0);
return;
}
const index = hashMapIndexes.findIndex(x => hash.startsWith(`#${x}`));
setIndex(Math.max(index, 0));
}, []);
<HashRouter>
<React.Suspense fallback={null}>
<Switch>
<Tabs value={index} onChange={onTabChange}>
{ tabItemList.map(x => (
<Tabs.TabPane key={x.title} tab={x.title}>
<Route exact path={x.path} component={x.component} />
</Tabs.TabPane>
)) }
{ <Tabs.TabPane key="TabA" tab="TabA">
<Switch>
<Route exact path="/tabA" component={TabA} />
<Route exact path="/tabA/list" component={TabA_List} />
<Redirect to="tabA" />
</Switch>
</Tabs.TabPane> }
</Tabs>
</Switch>
</React.Suspense>
</HashRouter>
单独为tabA设置一组Route, 可以判断同一前缀的路由进同一个tab页面。
也可以有另一种用法
.....
<Tabs.TabPane key="TabA" tab="TabA">
<Route path="/tabA" component={TabA}>
<Switch>
<Route exact path="/tabA/list" component={TabA_List} />
<Route exact path="/tabA/Create" component={TabA_Create} />
<Redirect to="/tabA/list" />
</Switch>
</Route>
</Tabs.TabPane>
.....
Route下面支持传另一组Route, 但是parentRoute不能采用exact精准匹配
总结
看起来很简单的东西, 到用的时候不了解其工作方式还是会碰到很多坑,爬过这些坑耗时费力。后面会写一篇react-router路由匹配原理, 跟大家一起学习一下在各种配置下React-router是如何进行路由匹配, 以便彻底填平这个坑。