(译)React-Router4的变化

栏目: IOS · Android · 发布时间: 6年前

内容简介:首先,这篇文章的目的并不是为了重新叙述一遍React-Router4的文档。接下来我要说的内容,将会覆盖React-Router的大多数API,但是真正的目的是揭开React-Router4成功的模式和策略。在开始本文之前,你需要了解一些JS的概念:如果你喜欢直接看demo来了解,请点击此链接查看。
  • 原文地址:戳这里
  • 项目地址:传送门

首先,这篇文章的目的并不是为了重新叙述一遍React-Router4的文档。接下来我要说的内容,将会覆盖React-Router的大多数API,但是真正的目的是揭开React-Router4成功的模式和策略。

在开始本文之前,你需要了解一些JS的概念:

  • React无状态函数式组件
  • ES6箭头函数以及它的“隐式返回”
  • ES6解构赋值
  • ES6模板字符串

如果你喜欢直接看demo来了解,请点击此链接查看。

新的API以及新的心智模型

React-Router早一些的版本中,是在一个地方统一地管理所有路由规则,将它们与布局组件分离。当然,路由也可以被拆分和组织到几个文件当中,但是从概念上来说,路由本身是一个单元,基本上是一个美化的配置文件。

要了解版本4到底有哪些不同,最好的方式可能是用每一个版本写一个两个页面的应用,并进行比较。示例的应用只有home页面和user页面。

以下是版本3的写法:

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'))
复制代码

版本3中的一些概念在版本4中可能不再适用:

<Route>

React-Router4不在提倡集中管理路由。相反,路由规则存在于布局和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 ,这意味着它也提供了原生版本。

在使用V4版本构建的应用中,最显著的一点是“路由”似乎消失了。在V3中,路由是我们渲染到dom的一个大玩意儿,它统筹了我们的整个项目。现在,除了 <BrowserRouter> 以外,第一个抛进dom的是我们的应用本身。

另一个在V4示例中没有展现的V3的用法是使用props.children来嵌套组件。这是因为在V4中,只要路由匹配到了,<Router>组件在哪里编写,子组件就会渲染在哪里。

包含式路由

在先前的例子中,你可能已经注意到了 exact 属性。那么它到底是干嘛的呢?在V3中,路由规则是独一无二的,这意味着最终只有一个路由会匹配到。但是在V4中,路由是包含式的,这意味着可能会有多个路由同时匹配到并且渲染。

在先前的例子中,我们试图依靠路径来区分渲染home页面或者user页面。如果把 exact 属性从该示例中移除,那么在浏览‘/user’路径时,home页面和user页面会同时渲染。

想要更好的了解匹配规则,您可以查看path-to-regexp,React-Router4 就是使用它来匹配路由的。

为了更好地演示包含式路由起到了什么作用,我们实现一个这样的功能:只有在user页面我们才在头部添加一个 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> 组件来实现。 <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> 中的路由只有一个会被渲染。我们仍然需要在home页面的路由上添加exact属性,否则,当我们访问‘/users’或者‘/users/add’时,home页面将会首先匹配到。事实上,在使用排他性路由时,路由的位置排列是最为关键的事,传统路由一直如此。我们把‘/users/add’排列在‘/users’之前以保证正确的匹配路由。因为‘/users/add’会匹配‘/users’和‘/users/add’,把‘/users/add’放在前面是最好的。

当然,如果你使用了 exact 属性,那放在哪儿都无所谓,但至少我们还有选择。

在遇到 <Redirect> 组件时,总是会做浏览器重定向,但是如果它在 <Switch> 组件中,那么只有当其他路由都没有匹配到时,才会重定向。

“Index Routes” 和 “Not Found”

在V4中我们用 exact 属性代替了 <IndexRoute> 来做同样的事。在没有路由匹配到时,使用 <Redirect> 来重定向到默认页面的路径,或者404页面。

嵌套布局

现在你可能已经开始思考如何实现嵌套布局了。我原本以为我不会纠结于这个概念,但我没有...V4提供了很多选择,这就是为什么V4如此强大。但是,多样的选择意味着我们可能选择并不理想的方案。表面上看来,嵌套布局很简单,但是根据你的选择不同,你可能会遇到一些麻烦因为你组织路由的方式。

想象这样一个场景,我们需要一个‘浏览用户’的页面以及‘用户信息’的页面。对于产品页面我们也有类似需求。用户和产品都需要一个子布局,并且子布局都是唯一特有的。举例来说,每一个子布局拥有不同的导航卡。我们有几种不同的方式来解决这个问题,有些比较好有些则不太好。第一种方案可能并不是那么好,但我还是想让你们看一看,以免以后踩入这个坑。第二种方案就显得更好一些。

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概念:任何由 <Route> 渲染的组件,V4都会提供 props.match 。就像你看到的,userId这个参数是有 props.match.params 提供的。了解更多V4文档。如果一个组件并不是直接通过 <Route> 组件渲染的,但是却要用到 props.match ,那么我们可以借助 withRouter() 这个高阶组件。

实现上来说,第一种方案并没有什么问题。但是我们仔细观察一下,会发现 UserProfilePageBrowserUsersPage 的布局是一样的,不同的只是 UserProfileBrowserUserTable 。这种实现方法会导致一些冗余代码,并且在 UserProfilePageBrowserUsersPage 之间切换时, 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>
  )
}

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>
)
复制代码

和之前实现方式不同的是,我们的路由从4个变成了2个。 BrowseUsersPageUserProfilePage 组件只负责渲染不同的部分,作为 UserSubLayout 的子组件。需要注意的一点是,不论你路由嵌套多深,还是需要写完整路径去匹配。如果你不想重复书写路径或者方便统一修改路径,你可以使用 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>
)
复制代码

Match

正如我们所见,props.match在获取userId这类参数和简化路由书写方面很有用处。match对象提供了一些属性: match.paramsmatch.pathmatch.url 等等

match.path 和 match.url

第一眼我们并不能清楚的区分这两者。控制台打印时,多数情况下也是相同的结果。举例来说,当浏览器路径是‘/users’时, match.pathmatch.url 打印输出的是相同的值。

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // output: "/users"
  console.log(match.path)  // output: "/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>
  )
}
复制代码

现在我们还是分不清这两者的区别,但是,如果我们用嵌套的路由,特别是带有参数的匹配模式,在更深一层的组件中打印,我们就能看到区别。比如我们浏览的是‘/users/5’这个路径, match.url 返回的是‘/users/5’,而 match.path 返回的是‘/users/:userId’。这样就一目了然了, match.url 返回的是具体的路由地址字符串,而 match.path 返回的是路由匹配模式的字符串。

如何选择?

在用 <Route> 做嵌套路由时,建议使用 match.path ,因为你可能在你的子组件中需要使用 match.param 对象,在 <Link> 组件做路由跳转时,使用 match.url

避免match冲突

假设现在我们需要一个编辑页面对用户信息进行编辑,那此页面的路由规则应该类似于‘/users/:userId/edit’。那么问题来了,前面的例子中,用户信息页面的路由匹配规则是 /users/:userId ,当我们访问‘/users/5/edit’的时候,将会首先匹配到用户信息页面而不是编辑页。那是否意味着我们要像之前做的那样,在原有的信息页面同时添加一个编辑的子页面,再去匹配呢?并不一定。我们可以通过将‘/users/:userId/edit’规则放在 /users/:userId 之前来达到此效果。或者对 /users/:userId 进行进一步约定,比如限制:userId为数字: users/:userId(\\d+)

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>
)
复制代码

授权路由

在我们的项目中,根据用户的登录状态来限制用户访问某些路由的功能是很常见的。让授权页面(应用程序的主要页面)和未授权页面(比如登录页面以及忘记密码页面)拥有不同的UI和感受也是常见的需求。

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>
    )
  }
}
复制代码

这种方法有几个关键点。首先,根据我们所处的应用程序的哪个部分,我在两个顶级布局之间进行选择。访问路径,如“/auth/login”或“/auth/forget-password”将使用未经授权的布局。当用户登录时,我们将保证所有路径都有一个' /app '前缀,它使用 AuthorizedRoute 组件来确定用户是否登录。如果用户试图访问以' /app '开头的页面,但他们没有登录,他们将被重定向到登录页面。

但是 AuthorizedRoute 并不是V4本身提供的,而是我自己实现的。V4中一个惊人的新特性是能够为特定目的创建自己的路由。不同于通过传递一个组件属性到 <Route> ,而是传递一个 rendr 回调:

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() 去获取状态并把pending和logged的值插入 redux 管理的状态中。pending意味着请求还没结束。

点击此链接你可以查看完整的登录限制的代码实现。

(译)React-Router4的变化

其他注意点

React-Router V4还有其他更多炫酷的特性。最后,我们来了解一些小特性,以防遇到时犯迷糊。

<Link><NavLink>

在V4中,有两种方式可以将锚点和路由集成: <Link><NavLink>

<NavLink> 的原理和 <Link> 一样,不同的是, <NavLink> 能根据浏览器URL是否匹配从而提供额外的样式定制。详情可以在线查看该demo。部分代码如下:

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> 匹配到当前URL时,它允许我们在此 <NavLink> 上添加一个自定义的class,以便控制样式。

URL查询字符串

在V4中已经没有方式可以直接得到URL的查询字符串了。在我看来,做出这个决定是因为对于如何处理复杂的查询字符串没有标准。因此,v4没有在模块中引入相关方法,而是决定让开发人员选择如何处理查询字符串。这是一件好事。

就我个人而言,我使用sindresorhus大神(twitter)编写的query-string模块来处理查询字符串。

动态路由

V4最出色的一点是几乎所有的东西都是一个 React 组件,包括 <Route> 。路由再也不是什么魔法,我们可以在任何需要的时候有条件的渲染它们。想象一下,当满足某些条件时,您的应用程序的整个部分都可用于路由。当这些条件不满足时,我们可以删除路由。我们甚至可以做一些炫酷的事,比如递归路由。

React-Router4 变得更为简单了,因为万物皆组件。


以上所述就是小编给大家介绍的《(译)React-Router4的变化》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Zero to One

Zero to One

Peter Thiel、Blake Masters / Crown Business / 2014-9-16 / USD 27.00

“This book delivers completely new and refreshing ideas on how to create value in the world.” - Mark Zuckerberg, CEO of Facebook “Peter Thiel has built multiple breakthrough companies, and ......一起来看看 《Zero to One》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

SHA 加密
SHA 加密

SHA 加密工具