[译] 关于 React Router 4 的一切

关于 React Router 4 的一切

我在 React Rally 2016 大会上第一次遇到了 Michael Jackson,不久以后便写了一篇 an article on React Router 3。Michael 与 Ryan Florence 都是 React Router 的主要做者。遇到一位我很是喜欢的工具的建立者是激动人心的,但当他这么说的时候,我感到很震惊。“让我向大家展现咱们在 React Router 4 的想法,它的方式是大相径庭的!”。老实说,我真的不明白新的方向以及为何它须要如此大的改变。因为路由是应用程序架构的重要组成部分,所以这可能会改变一些我喜欢的模式。这些改变的想法让我很焦虑。考虑到社区凝聚力以及 React Router 在这么多的 React 应用程序中扮演着重要的角色,我不知道社区将如何接受这些改变。css

几个月后,React Router 4 发布了,仅仅从 Twitter 的嗡嗡声中我便得知,你们对于这个重大的重写存在着不一样的想法。这让我想起了第一个版本的 React Router 针对其渐进概念的推回。在某些方面,早期版本的 React Router 符合咱们传统的思惟模式,即一个应用的路由“应该”将全部的路由规则放在一个地方。然而,并非每一个人都接受使用嵌套的 JSX 路由。但就像 JSX 自身说服了批评者同样(至少是大多数),许多人转而相信嵌套的 JSX 路由是很酷的想法。html

如是,我学习了 React Router 4。无能否认,第一天是挣扎的。挣扎的倒不是其 API,而更多的是使用它的模式和策略。我使用 React Router 3 的思惟模式并无很好地迁移到 v4。若是要成功,我将不得不改变我对路由和布局组件之间的关系的见解。最终,出现了对我有意义的新模式,我对路由的新方向感到很是高兴。React Router 4 不只包含 v3 的全部功能,并且还有新的功能。此外,起初我对 v4 的使用过于复杂。一旦我得到了一个新的思惟模式,我就意识到这个新的方向是惊人的!前端

本文的意图并非重复 React Router 4 已经写得很好的文档。我将介绍最多见的 API,但真正的重点是我发现的成功模式和策略。react

对于本文,如下是一些你须要熟悉的 JavaScript 概念:android

若是你喜欢跳转到演示区的话,请点这里:ios

查看演示git

新的 API 和新的思惟模式

React Router 的早期版本将路由规则集中在一个位置,使它们与布局组件分离。固然,路由能够被划分红多个文件,但从概念上讲,路由是一个单元,基本上是一个美化的配置文件。github

或许了解 v4 不一样之处的最好方法是用每一个版本编写一个简单的两页应用程序并进行比较。示例应用程序只有两个路由,对应首页和用户页面。web

这里是 v3 的:npm

import { Router, Route, IndexRoute } from 'react-router'

const PrimaryLayout = props => (
  <div className="primary-layout">
    <header>
      Our React Router 3 App
    </header>
    <main>
      {props.children}
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('root'))复制代码

如下是 v3 中的一些核心思想,但在 v4 中是不正确的:

  • 路由集中在一个地方。
  • 布局和页面嵌套是经过 <Route> 组件的嵌套而来的。
  • 布局和页面组件是彻底纯粹的,它们是路由的一部分。

React Router 4 再也不主张集中式路由了。相反,路由规则位于布局和 UI 自己之间。例如,如下是 v4 中的相同的应用程序:

import { BrowserRouter, Route } from 'react-router-dom'

const PrimaryLayout = () => (
  <div className="primary-layout">
    <header>
      Our React Router 4 App
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </main>
  </div>
)

const HomePage =() => <div>Home Page</div>
const UsersPage = () => <div>Users Page</div>

const App = () => (
  <BrowserRouter>
    <PrimaryLayout />
  </BrowserRouter>
)

render(<App />, document.getElementById('root'))复制代码

新的 API 概念:因为咱们的应用程序是用于浏览器的,因此咱们须要将它封装在来自 v4 的 BrowserRouter 中。还要注意的是咱们如今从 react-router-dom 中导入它(这意味着咱们安装的是 react-router-dom 而不是 react-router)。提示!如今叫作 react-router-dom 是由于还有一个 native 版本

对于使用 React Router v4 构建的应用程序,首先看到的是“路由”彷佛丢失了。在 v3 中,路由是咱们的应用程序直接呈现给 DOM 的最巨大的东西。 如今,除了 <BrowserRouter> 外,咱们首先抛给 DOM 的是咱们的应用程序自己。

另外一个在 v3 的例子中有而在 v4 中没有的是,使用 {props.children} 来嵌套组件。这是由于在 v4 中,<Route> 组件在何处编写,若是路由匹配,子组件将在那里渲染。

包容性路由

在前面的例子中,你可能已经注意到了 exact 这个属性。那么它是什么呢?V3 的路由规则是“排他性”的,这意味着只有一条路由将获胜。V4 的路由默认为“包含”的,这意味着多个 <Route> 能够同时进行匹配和渲染。

在上一个例子中,咱们试图根据路径渲染 HomePage 或者 UsersPage。若是从示例中删除了 exact 属性,那么在浏览器中访问 /users 时,HomePageUsersPage 组件将同时被渲染。

要更好地了解匹配逻辑,请查看 path-to-regexp,这是 v4 如今正在使用的,以肯定路由是否匹配 URL。

为了演示包容性路由是有帮助的,咱们在标题中包含一个 UserMenu,但前提是咱们在应用程序的用户部分:

const PrimaryLayout = () => (
  <div className="primary-layout">
    <header>
      Our React Router 4 App
      <Route path="/users" component={UsersMenu} />
    </header>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </main>
  </div>
)复制代码

如今,当用户访问 /users 时,两个组件都会渲染。相似这样的事情在 v3 中经过特定的匹配模式也是可行的,但它更复杂。得益于 v4 的包容性路由,如今可以很轻松地实现。

排他性路由

若是你只须要在路由列表里匹配一个路由,则使用 <Switch> 来启用排他路由:

const PrimaryLayout = () => (
  <div className="primary-layout">
    <PrimaryHeader />
    <main>
      <Switch>
        <Route path="/" exact component={HomePage} />
        <Route path="/users/add" component={UserAddPage} />
        <Route path="/users" component={UsersPage} />
        <Redirect to="/" />
      </Switch>
    </main>
  </div>
)复制代码

在给定的 <Switch> 路由中只有一条将渲染。在 HomePage 路由上,咱们仍然须要 exact 属性,尽管咱们会先把它列出来。不然,当访问诸如 /users/users/add 的路径时,主页路由也将匹配。事实上,战略布局是使用排他路由策略(由于它老是像传统路由那样使用)时的关键。请注意,咱们在 /users 以前策略性地放置了 /users/add 的路由,以确保正确匹配。因为路径 /users/add 将匹配 /users/users/add,因此最好先把 /users/add 放在前面。

固然,若是咱们以某种方式使用 exact,咱们能够把它们放在任何顺序上,但至少咱们有选择。

若是遇到,<Redirect> 组件将会始终执行浏览器重定向,可是当它位于 <Switch> 语句中时,只有在其余路由不匹配的状况下,才会渲染重定向组件。想了解在非切换环境下如何使用 <Redirect>,请参阅下面的受权路由

“默认路由”和“未找到”

尽管在 v4 中已经没有 <IndexRoute> 了,但可使用 <Route exact> 来达到一样的效果。若是没有路由解析,则可使用 <Switch><Redirect> 重定向到具备有效路径的默认页面(如同我对本示例中的 HomePage 所作的),甚至能够是一个“未找到页面”。

嵌套布局

你可能开始期待嵌套子布局,以及如何实现它们。我本来不认为我会纠结这个概念,但我确实纠结了。React Router v4 给了咱们不少选择,这使它变得很强大。可是,选择意味着有选择不理想策略的自由。表面上看,嵌套布局很简单,但根据你的选择,可能会由于你组织路由的方式而遇到阻碍。

为了演示,假设咱们想扩展咱们的用户部分,因此咱们会有一个“用户列表”页面和一个“用户详情”页面。咱们也但愿产品也有相似的页面。用户和产品都须要其个性化的子布局。例如,每一个可能都有不一样的导航选项卡。有几种方法能够解决这个问题,有的好,有的很差。第一种方法不是很好,但我想告诉你,这样你就不会掉入这个陷阱。第二种方法要好不少。

第一种方法,咱们修改 PrimaryLayout,以适应用户和产品对应的列表及详情页面:

const PrimaryLayout = props => {
  return (
    <div className="primary-layout">
      <PrimaryHeader />
      <main>
        <Switch>
          <Route path="/" exact component={HomePage} />
          <Route path="/users" exact component={BrowseUsersPage} />
          <Route path="/users/:userId" component={UserProfilePage} />
          <Route path="/products" exact component={BrowseProductsPage} />
          <Route path="/products/:productId" component={ProductProfilePage} />
          <Redirect to="/" />
        </Switch>
      </main>
    </div>
  )
}复制代码

虽然这在技术上可行的,但仔细观察这两个用户页面就会发现问题:

const BrowseUsersPage = () => (
  <div className="user-sub-layout"> <aside> <UserNav /> </aside> <div className="primary-content"> <BrowseUserTable /> </div> </div>
)

const UserProfilePage = props => (
  <div className="user-sub-layout"> <aside> <UserNav /> </aside> <div className="primary-content"> <UserProfile userId={props.match.params.userId} /> </div> </div> )复制代码

新 API 概念props.match 被赋到由 <Route> 渲染的任何组件。你能够看到,userId 是由 props.match.params 提供的,了解更多请参阅 v4 文档。或者,若是任何组件须要访问 props.match,而这个组件没有由 <Route> 直接渲染,那么咱们可使用 withRouter() 高阶组件。

每一个用户页面不只要渲染其各自的内容,并且还必须关注子布局自己(而且每一个子布局都是重复的)。虽然这个例子很小,可能看起来微不足道,但重复的代码在一个真正的应用程序中多是一个问题。更不用说,每次 BrowseUsersPageUserProfilePage 被渲染时,它将建立一个新的 UserNav 实例,这意味着全部的生命周期方法都将从新开始。若是导航标签须要初始网络流量,这将致使没必要要的请求 —— 这都是咱们决定使用路由的方式形成的。

这里有另外一种更好的方法:

const PrimaryLayout = props => {
  return (
    <div className="primary-layout">
      <PrimaryHeader />
      <main>
        <Switch>
          <Route path="/" exact component={HomePage} />
          <Route path="/users" component={UserSubLayout} />
          <Route path="/products" component={ProductSubLayout} />
          <Redirect to="/" />
        </Switch>
      </main>
    </div>
  )
}复制代码

与每一个用户和产品页面相对应的四条路由不一样,咱们为每一个部分的布局提供了两条路由。

请注意,上述示例没有使用 exact 属性,由于咱们但愿 /users 匹配任何以 /users 开头的路由,一样适用于产品。

经过这种策略,渲染其它路由将成为子布局的任务。UserSubLayout 可能以下所示:

const UserSubLayout = () => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route path="/users" exact component={BrowseUsersPage} />
        <Route path="/users/:userId" component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)复制代码

新策略中最明显的胜出在于全部用户页面之间的不重复布局。这是一个共赢,由于它不会像第一个示例那样具备相同生命周期的问题。

有一点须要注意的是,即便咱们在布局结构中深刻嵌套,路由仍然须要识别它们的完整路径才能匹配。为了节省重复输入(以防你决定将“用户”改成其余内容),请改用 props.match.path

const UserSubLayout = props => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route path={props.match.path} exact component={BrowseUsersPage} />
        <Route path={`${props.match.path}/:userId`} component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)复制代码

匹配

到目前为止,props.match 对于知道详情页面渲染的 userId 以及如何编写咱们的路由是颇有用的。match 对象给咱们提供了几个属性,包括 match.paramsmatch.pathmatch.url其余几个

match.path vs match.url

起初这二者之间的区别彷佛并不清楚。控制台日志有时会显示相同的输出,这使得它们之间的差别更加模糊。例如,当浏览器路径为 /users 时,它们在控制台日志将输出相同的值:

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // 输出:"/users"
  console.log(match.path)  // 输出:"/users"
  return (
    <div className="user-sub-layout">
      <aside>
        <UserNav />
      </aside>
      <div className="primary-content">
        <Switch>
          <Route path={match.path} exact component={BrowseUsersPage} />
          <Route path={`${match.path}/:userId`} component={UserProfilePage} />
        </Switch>
      </div>
    </div>
  )
}复制代码

ES2015 概念: match 在组件函数的参数级别将被解构

虽然咱们看不到差别,但 match.url 是浏览器 URL 中的实际路径,而 match.path 是为路由编写的路径。这就是为何它们是同样的,至少到目前为止。可是,若是咱们更进一步,在 UserProfilePage 中进行一样的控制台日志操做,并在浏览器中访问 /users/5,那么 match.url 将是 "/users/5"match.path 将是 "/users/:userId"

选择哪个?

若是你要使用其中一个来帮助你构建路由路径,我建议你选择 match.path。使用 match.url 来构建路由路径最终会致使你不想看到的场景。下面是我遇到的一个情景。在一个像 UserProfilePage(当用户访问 /users/5 时渲染)的组件中,我渲染了以下这些子组件:

const UserComments = ({ match }) => (
  <div>UserId: {match.params.userId}</div>
)

const UserSettings = ({ match }) => (
  <div>UserId: {match.params.userId}</div>
)

const UserProfilePage = ({ match }) => (
  <div>
    User Profile:
    <Route path={`${match.url}/comments`} component={UserComments} />
    <Route path={`${match.path}/settings`} component={UserSettings} />
  </div>
)复制代码

为了说明问题,我渲染了两个子组件,一个路由路径来自于 match.url,另外一个来自 match.path。如下是在浏览器中访问这些页面时所发生的事情:

  • 访问 /users/5/comments 渲染 "UserId: undefined"。
  • 访问 /users/5/settings 渲染 "UserId: 5"。

那么为何 match.path 能够帮助咱们构建路径 而 match.url 则不能够呢?答案就是这样一个事实:{${match.url}/comments} 基本上就像和硬编码的 {'/users/5/comments'} 同样。这样作意味着后续组件将没法正确地填充 match.params,由于路径中没有参数,只有硬编码的 5

直到后来我看到文档的这一部分,才意识到它有多重要:

match:

  • path - (string) 用于匹配路径模式。用于构建嵌套的 <Route>
  • url - (string) URL 匹配的部分。 用于构建嵌套的 <Link>

避免匹配冲突

假设咱们制做的应用程序是一个仪表版,因此咱们但愿可以经过访问 /users/add/users/5/edit 来新增和编辑用户。可是在前面的例子中,users/:userId 已经指向了 UserProfilePage。那么这是否意味着带有users/:userId 的路由如今须要指向另外一个子子布局来容纳编辑页面和详情页面?我不这么认为,由于编辑和详情页面共享相同的用户子布局,因此这个策略是可行的:

const UserSubLayout = ({ match }) => (
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route exact path={props.match.path} component={BrowseUsersPage} />
        <Route path={`${match.path}/add`} component={AddUserPage} />
        <Route path={`${match.path}/:userId/edit`} component={EditUserPage} />
        <Route path={`${match.path}/:userId`} component={UserProfilePage} />
      </Switch>
    </div>
  </div>
)复制代码

请注意,为了确保进行适当的匹配,新增和编辑路由须要战略性地放在详情路由以前。若是详情路径在前面,那么访问 /users/add 时将匹配详情(由于 "add" 将匹配 :userId)。

或者,若是咱们这样建立路径 ${match.path}/:userId(\\d+),来确保 :userId 必须是一个数字,那么咱们能够先放置详情路由。而后访问 /users/add 将不会产生冲突。这是我在 path-to-regexp 的文档中学到的技巧。

受权路由

在应用程序中,一般会根据用户的登陆状态来限制用户访问某些路由。对于未经受权的页面(如“登陆”和“忘记密码”)与已受权的页面(应用程序的主要部分)看起来不同也是常见的。为了解决这些需求,须要考虑一个应用程序的主要入口点:

class App extends React.Component {
  render() {
    return (
      <Provider store={store}>
        <BrowserRouter>
          <Switch>
            <Route path="/auth" component={UnauthorizedLayout} />
            <AuthorizedRoute path="/app" component={PrimaryLayout} />
          </Switch>
        </BrowserRouter>
      </Provider>
    )
  }
}复制代码

使用 react-redux 与 React Router v4 很是相似,就像以前同样,只需将 BrowserRouter 包在 <Provider> 中便可。

经过这种方法能够获得一些启发。第一个是根据咱们所在的应用程序的哪一个部分,在两个顶层布局之间进行选择。像访问 /auth/login/auth/forgot-password 这样的路径会使用 UnauthorizedLayout —— 一个看起来适于这种状况的布局。当用户登陆时,咱们将确保全部路径都有一个 /app 前缀,它使用 AuthorizedRoute 来肯定用户是否登陆。若是用户在没有登陆的状况下,尝试访问以 /app 开头的页面,那么将被重定向到登陆页面。

虽然 AuthorizedRoute 不是 v4 的一部分,可是我在 v4 文档的帮助下本身写了。v4 中一个惊人的新功能是可以为特定的目的建立你本身的路由。它不是将 component 的属性传递给 <Route>,而是传递一个 render 回调函数:

class AuthorizedRoute extends React.Component {
  componentWillMount() {
    getLoggedUser()
  }

  render() {
    const { component: Component, pending, logged, ...rest } = this.props
    return (
      <Route {...rest} render={props => {
        if (pending) return <div>Loading...</div>
        return logged
          ? <Component {...this.props} />
          : <Redirect to="/auth/login" />
      }} />
    )
  }
}

const stateToProps = ({ loggedUserState }) => ({
  pending: loggedUserState.pending,
  logged: loggedUserState.logged
})

export default connect(stateToProps)(AuthorizedRoute)复制代码

可能你的登陆策略与个人不一样,我是使用网络请求来 getLoggedUser(),并将 pendinglogged 插入 Redux 的状态中。pending 仅表示在路由中请求仍在继续。

点击此处查看 CodePen 上完整的身份验证示例

其余提示

React Router v4 还有不少其余很酷的方面。最后,必定要提几件小事,以避免到时它们让你措手不及。

在 v4 中,有两种方法能够将锚标签与路由集成:<Link><NavLink>

<NavLink><Link> 同样,但若是 <NavLink> 匹配浏览器的 URL,那么它能够提供一些额外的样式能力。例如,在示例应用程序中,有一个<PrimaryHeader> 组件看起来像这样:

const PrimaryHeader = () => (
  <header className="primary-header"> <h1>Welcome to our app!</h1> <nav> <NavLink to="/app" exact activeClassName="active">Home</NavLink> <NavLink to="/app/users" activeClassName="active">Users</NavLink> <NavLink to="/app/products" activeClassName="active">Products</NavLink> </nav> </header>
)复制代码

使用 <NavLink> 可让我给任何一个激活的连接设置一个 active 样式。并且,须要注意的是,我也能够给它们添加 exact 属性。若是没有 exact,因为 v4 的包容性匹配策略,那么在访问 /app/users 时,主页的连接将处于激活中。就我的经历而言,NavLinkexact 属性等价于 v3 的 <link>,并且更稳定。

URL 查询字符串

再也没法从 React Router v4 中获取 URL 的查询字符串了。在我看来,作这个决定是由于没有关于如何处理复杂查询字符串的标准。因此,他们决定让开发者去选择如何处理查询字符串,而不是将其做为一个选项嵌入到 v4 的模块中。这是一件好事。

就我的而言,我使用的是 query-string,它是由 sindresorhus 大神写的。

动态路由

关于 v4 最好的部分之一是几乎全部的东西(包括 <Route>)只是一个 React 组件。路由再也不是神奇的东西了。咱们能够随时随地渲染它们。想象一下,当知足某些条件时,你的应用程序的整个部分均可以路由到。当这些条件不知足时,咱们能够移除路由。甚至咱们能够作一些疯狂并且很酷的递归路由

由于它 Just Components™,React Router 4 更简单了。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索