React Router路由嵌套

11,496 阅读2分钟

项目搭建中碰到的一个路由嵌套的问题,想象中很简单,用的时候却老是路由匹配不上。在此记录一下用法。

嵌套路由的实现

先说一下需求,有一个上中下页面布局的页面,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是如何进行路由匹配, 以便彻底填平这个坑。

参考: reacttraining.com/react-route…