具有多种布局的 React Router v4

Posted

技术标签:

【中文标题】具有多种布局的 React Router v4【英文标题】:React Router v4 with multiple layouts 【发布时间】:2017-08-09 06:39:11 【问题描述】:

我想在我的公共布局中渲染我的一些路线,在我的私人布局中渲染一些其他路线,有没有一种干净的方法可以做到这一点?

显然不起作用的示例,但我希望大致说明我在寻找什么:

<Router>

  <PublicLayout>
    <Switch>
      <Route exact path="/" component=HomePage />
      <Route exact path="/about" component=AboutPage />
    </Switch>
  </PublicLayout>

  <PrivateLayout>
    <Switch>
      <Route exact path="/profile" component=ProfilePage />
      <Route exact path="/dashboard" component=DashboardPage />
    </Switch>
  </PrivateLayout>

</Router>

我想要切换某些路由的布局,我该如何使用新的 react 路由器来做到这一点?

嵌套路由不再起作用并给我这个错误:

You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored

编辑:让布局包裹整个路由组也意味着只要您留在同一个私有/公共路由组中,这些布局只会渲染一次。例如,如果您的布局必须从服务器获取某些内容,这将是一件大事,因为如果您使用布局包装每个页面,那么每次页面更改都会发生这种情况。

【问题讨论】:

在此对话:github.com/ReactTraining/react-router/issues/3928 教程在这里:simonsmith.io/reusing-layouts-in-react-router-4 你能找到一种干净的方法吗? 使用简单的帮助库npmjs.com/package/react-router4-with-layouts 【参考方案1】:

为此我所做的是创建一个简单的组件,它为 Route 组件添加了一个额外的属性,即布局:

function RouteWithLayout(layout, component, ...rest)
  return (
    <Route ...rest render=(props) =>
      React.createElement( layout, props, React.createElement(component, props))
    />
  );

那么在您的情况下,您的路线将如下所示

<Switch>
    <RouteWithLayout layout=PublicLayout path="/" component=HomePage/>
    <RouteWithLayout layout=PublicLayout path="/about" component=AboutPage/>
    <RouteWithLayout layout=PrivateLayout path="/profile" component=ProfilePage/>
    <RouteWithLayout layout=PrivateLayout path="/dashboard" component=DashboardPage/>
</Switch>

【讨论】:

有趣...为什么它不起作用:&lt;Switch&gt;&lt;PublicLayout&gt; &lt;Route path="/" component=HomePage/&gt; &lt;Route path="/about" component=AboutPage/&gt;&lt;/PublicLayout&gt; &lt;PrivateLayout&gt;&lt;Route path="/profile" component=ProfilePage/&gt; &lt;Route path="/dashboard" component=DashboardPage/&gt;&lt;/PrivateLayout&gt; &lt;/Switch&gt; @carkod all 所做的是确保只有一条路由匹配,它实际上并没有按照您的示例执行您希望它执行的操作。在实际演示的场景中,它实际上甚至不需要。较新版本的 react 路由器的工作方式与以前的版本完全不同。 嗨,使用这种模式,如果您希望所有路线都准确无误,您必须将 exact 属性传递给 RouteWithLayout&lt;Switch&gt;&lt;RouteWithLayout exact ... &gt;。直接将其传递给 Route 孩子是行不通的。我的猜测是 &lt;Switch&gt; 只考虑它自己的直接孩子的 exact 道具,即使它不是 Route 组件而是包装器。 这里最大的不便是您的布局会为您访问的每条路线重新渲染。如果您的 Switch 嵌套在布局中,性能会更高。【参考方案2】:

2020 年更新

好吧,现在我正在遵循这种方法,它比我之前发布的更简单:

const Pages = () => 
  return (
    <ReactRouter>
      <Switch>
        <Route path="/comingsoon" component=ComingSoon exact />
        <Route>
          <MainLayout>
            <Switch>
              <Route path="/home" exact>
                <Home />
              </Route>
              <Route path="/login" exact>
                <Login />
              </Route>
              <Route path="/useraccount" exact>
                <UserAccount />
              </Route>
              <Route path="/createaccount" exact>
                <CreateAccount />
              </Route>
              <Route path="/contact" exact>
                <Contact />
              </Route>
              <Route path="/about" exact>
                <About />
              </Route>
              <Redirect path="/" exact to="/comingsoon" />
              <Route path="*" exact component=NotFound />
            </Switch>
          </MainLayout>
        </Route>
      </Switch>
    </ReactRouter>
  );
;

这样,MainLayout 将处理除即将到来的页面之外的所有内容。

老答案

如果您使用 Typescript 并想关注this react layout aproach,那么您可以像这样声明您的布局:

import './Default.less';

import React from 'react';
import  Route  from "react-router-dom";

import  Sider  from './Components';
import  Notification  from 'Client/Components';

interface IDefaultProps 
  component: any
  path?: string;
  exact?: boolean;


const Default: React.SFC<IDefaultProps> = (props) => 
  const  component: Component, ...rest  = props;
  return <Route ...rest render=matchProps => (
    <div className="defaultLayout">
      <Sider />
      <div className="defaultLayoutContent">
        <Component ...matchProps />
      </div>
      <Notification />
    </div>
  ) />


export default Default;

并像这样声明路由:

import React from 'react';
import  Route  from 'react-router-dom';

import  DefaultLayout  from 'Client/Layout';
import  Dashboard, Matters, Schedules, Students  from './Containers';

export const routes = <div>
  <DefaultLayout exact path="/" component=Dashboard />
  <DefaultLayout path="/matters" component=Matters />
  <DefaultLayout path="/schedules" component=Schedules />
  <DefaultLayout path="/students" component=Students />
</div>;

【讨论】:

请接受这个作为正确答案。它很干净,遵循当前的 React 模式并使用 Typescript。 不是每次更改路由都要重新挂载DefaultLayout组件吗?它可能会对整个应用程序产生很大的性能影响,并使得在路线变化时执行动画变得更加困难。 最终使用了 React Router 文档中描述的技术,非常适合这个用例 reacttraining.com/react-router/web/example/sidebar 它工作了,谢谢!【参考方案3】:

2019+

找了之后,干净高效的方式(避免滥用重渲染):

    <Route exact path=["/about", "/"]>
      <PublicLayout>
        <Route exact path="/" component=HomePage />
        <Route path="/about" component=AboutPage />
      </PublicLayout>
    </Route>
    <Route path=["/profile", "/dashboard"]>
      <PrivateLayout>
        <Route path="/profile" component=ProfilePage />
        <Route path="/dashboard" component=DashboardPage />
      </PrivateLayout>
    </Route>

同样,它可以被重构, 查看我的完整答案: https://***.com/a/57358661/3437790

【讨论】:

【参考方案4】:

我认为布局不属于路由文件。

保持路线干净,即:

<Route exact path="/" component="HomePage" />

然后,在 HomePage 组件中,将呈现的内容包装在您选择的布局中:

...
render() 
  <PublicLayout>
    <h1>Home page!</h1>
  </PublicLayout>

这样,路线保持超级干净,而且您可以轻松地显示路线,该路线应支持两种布局(例如 404 页面)。

【讨论】:

想象一个应用程序有几十个或上百个路线,这意味着您必须为每条路线重复一次包装布局。只是想保持干燥。 嗯,在这两种情况下它都是一个组件的名称。使用包装器,您可以获得一些额外的好处:您可以让他们将道具传递给孩子;您将它们全部保存在一个文件中;下一个打开该文件的开发人员会立即知道发生了什么。 这应该是公认的答案。是的,如果您有大量路由,您将有一些重复的代码(我们每次只讨论 3 行 tbh)但它有一些优点:单独的布局和路由(这是 2 个不同的关注点),更具声明性( React 方式),更少的额外包装器 s。顺便说一句,大多数应用程序不会处理超过一百条路线,即使那样,布局也不会是你的第一个问题:) @ClementParis016 我完全同意你的看法,我也有同样的想法,重复的代码不会太多,但这可以让你分离关注点! 我不认为这是“更多的 React 方式”,因为 ReactRouter 培训人员写了一个清晰的例子,如何使用渲染函数来做到这一点,正如 Zaptree reacttraining.com/react-router/core/api/Route/render-func 上面提到的那样。【参考方案5】:

如果您在其他地方看到过这个问题,我将在任何地方写下这个问题,非常抱歉。 我这样做只是因为我已经挣扎了一段时间,我认为值得尽可能地传播这个解决方案。 足够的话,这就是我所做的,我认为这很不言自明。就像一个魅力,它很容易打字。

const withLayout = (LayoutComp, ComponentComp) => <LayoutComp><ComponentComp /></LayoutComp>;
const withDefaultLayout = (ComponentComp) => () => withLayout(Layout, ComponentComp);
const withEmptyLayout = (ComponentComp) => () => withLayout(EmptyLayout, ComponentComp);

export const routes = <div>
    <Switch>
        <Route path="/" exact render=withDefaultLayout(Home) />
        <Route path='/subscriptions' render=withDefaultLayout(SubscriptionsWrapped) />

        <Route path='/callback' render=withEmptyLayout(AuthCallback) />
        <Route path='/welcome/initial' render=withEmptyLayout(Start) />
    </Switch>
</div>;

【讨论】:

感谢分享,对我帮助很大。【参考方案6】:

我尝试了 Florians 的回答,但这对我来说还不够,因为 react 将为每条路线创建一个单独的布局实例,因此任何导航转换(如标签滑动)都会被终止。

我希望有一个更优雅的解决方案,但这让我的应用再次与 v4 一起工作。

定义一个指向组件的路由,该组件将决定将路由包装在什么中

<Router>
  <Route component=AppWrap />
</Router>

在 AppWrap 中执行如下操作

 var isPrivateLayout;
 for (var path of listOfPrivateLayoutPaths) 
   if (this.props.location.pathname == path) 
     isPrivateLayout= true;
   
 
 if (isPrivateLayout) 
   return <PrivateLayout>
        (routes)
      </PrivatelyLayout>
  else 
   return <PublicLayout>
        (routes)
      </PublicLayout>;
 

Route Config 或许可以用于更清晰的表示,不确定。

【讨论】:

【参考方案7】:

更新:我以另一种方式解决了它,但如果强制您使用 /app 或 /admin 来命名应用程序的不同部分。

每个组件 UserRoutes、AdminRoutes 和 PublicRoutes 基本上都是大型 Switch 组件,其根部有特定的布局。

它的外观如下:

<Router>
  <Switch>
    <Route path="/app" render=props => <UserRoutes ...props /> />
    <Route path="/admin" render=props => <AdminRoutes ...props /> />
    <Route path="/" render=props => <PublicRoutes ...props /> />
  </Switch>
</Router>

旧:一种解决方案是使用每个 Route 的 render prop,但看起来确实很麻烦:

<Router>
  <Switch>
    <Route
      path="/"
      render=() => <PublicLayout><HomePage /></PublicLayout>
    />
    <Route
      path="/about"
      render=() => <PublicLayout><AboutPage /></PublicLayout>
    />
    <Route
      path="/profile"
      render=() => <PrivateLayout><ProfilePage /></PrivateLayout>
    />
    <Route
      path="/dashboard"
      render=() => <PrivateLayout><DashboardPage /></PrivateLayout>
    />
  </Switch>
</Router>

【讨论】:

【参考方案8】:

思路和 Zaptree 的一样,但是使用 es6 语法并添加了检查,所以它可以用来代替 react-router 的 Route 组件

创建一个新组件,比如 /src/components/Route/index.js:

import React, Component from 'react'
import PropTypes from 'prop-types'
import Route as ReactRoute from 'react-router'

class Route extends Component 
  static propTypes = 
    component: PropTypes.func.isRequired,
    layout: PropTypes.func,
    path: PropTypes.string,
    exact: PropTypes.bool
  

  render = () => 
    const component, layout, path, exact = this.props
    let routeComponent = props => React.createElement(component, props)

    if (layout) 
      routeComponent = props =>
        React.createElement(layout, props, React.createElement(component, props))
    

    return <ReactRoute path=path exact=exact render=routeComponent/>
  


export default Route

使用创建的Route组件:

import Route from 'components/Route/'
...

<Router history=createHistory()>
  <Switch>
    <Route exact path='/' layout=PublicLayout component=HomePage/>
    <Route exact path='/' layout=PrivateLayout component=ProfilePage/>
    <Route path='/logins' component=Login/>
  </Switch>
</Router>

【讨论】:

【参考方案9】:

使用 Route 组件的 render prop,这不会在每次路由更改时卸载和安装您的布局组件。有关其工作原理的更多详细信息here。

假设你有两个布局组件 Primary.jsSecondary.js

在您的 App.js 渲染方法中,您只需返回

<BrowserRouter>
   <Switch>
     <Route path='/login' render=() => <Secondary><Login/></Secondary> />
     <Route path='/dashboard' render=() => <Primary><Dashboard/></Primary> />
   </Switch>
</BrowserRouter>

为了进一步完善它,您还可以定义一个高阶组件布局组件来用布局包装您的页面组件。 (未测试)

<Route to='/login' render=() => Secondary(Login)

【讨论】:

【参考方案10】:

@Qop 是正确的,但是在新的 React 路由器中,我注意到如果您将交换机内的根路径作为第一条路由,它将始终与它匹配,因此永远不会显示您的后续路由。你应该把根路径放在最后。

<Switch>
    <RouteWithLayout layout=PublicLayout path="/about" component=AboutPage/>
    <RouteWithLayout layout=PrivateLayout path="/profile" component=ProfilePage/>
    <RouteWithLayout layout=PrivateLayout path="/dashboard" component=DashboardPage/>
    <RouteWithLayout layout=PublicLayout path="/" component=HomePage/>
</Switch>

【讨论】:

能否请您指定您所指的 React Router 的版本。 我使用的是版本 4 如果你仍然有这个问题,我最终没有使用 Switch,而是创建了 DefaultLayout 和 PrivateLabel 对象。然后从路由器我调用 @Daniel 这是因为你需要使用像 这样的'exact'属性 【参考方案11】:

与@Zaptree 的想法相同

布局

function PublicLayout(props) 
  return (
      <Route ...props />
  );


function PrivateLayout(props) 
  return (
      <Route ...props />
  );

路线

<Switch>
    <PublicLayout exact path="/" component=HomePage />
    <PrivateLayout path="/profile" component=ProfilePage />
    <Route path="/callback" component=NoLayoutPage />
</Switch>

【讨论】:

【参考方案12】:

此解决方案将起作用。

<Router>
  <Switch>
    <PublicLayout>
      <Route exact path="/" component=HomePage />
      <Route exact path="/about" component=AboutPage />
    </PublicLayout>
  </Switch>       

  <Switch>
    <PrivateLayout>
      <Route exact path="/profile" component=ProfilePage />
      <Route exact path="/dashboard" component=DashboardPage />
    </PrivateLayout>
  </Switch>    
</Router>

【讨论】:

我证明了,但是它同时渲染了两个布局!,你能分享一下你的布局代码吗? @DavidNoreña 我使用相同的代码,但它不会呈现两种布局。 是的,它渲染了两种布局,但隐藏起来非常快,如果你把 console.log 放在这两种布局的渲染函数中,你就会明白我的意思......这是一种奇怪的行为...... .

以上是关于具有多种布局的 React Router v4的主要内容,如果未能解决你的问题,请参考以下文章

react-router-dom@6 的布局路由无法按预期工作

javascript 使用React Router v4的多个布局

使用 react-router 和 IndexRoute 嵌套路由(反应路由器布局)

如何在React Router v4中获取所有参数

使用带有布局页面或每页多个组件的 React-Router

react-router路由之routerRender方法(v5 v6)