React Router 4:痛过之后的豁然开朗

96
一个笑点低的妹纸
2.0 2017.10.07 14:41* 字数 2591

第一次独自承担一个项目,在进行技术选型时,由于看到了 React Router 4 的文档十分完善,果断地选择了它,尽管公司里现有的项目用的是它之前的版本。然而,看着这份华丽丽的文档对我来说也是一次痛苦的旅程。

刚开始构建项目的整体框架时,我照猫画虎,将以前项目中的 Router 结构照搬了过来,只不过路由库是最新的 v4,也采用了其中比较简单的语法。然而,到项目结构越发深入时,看着那篇完善的参考文档,我却犯难了,对路由和模块之间的对应关系感到困惑,不知如何下手o(╥﹏╥)o。直到看到了这篇文章 All About React Router 4,我才豁然开朗。原来 React Router 4 与之前的版本是完全不同的两种模式,只有理解了它的模式,才能在项目中使用起来游刃有余。

对于 React Router v2/v3 版本的学习,阮老师的文章 React Router 使用教程 写得通俗易懂,而 v4 版本则可直接去看 官网的资源

All About React Router 4 中,作者介绍了 v4 中一些新的基本特性,并与 v3 的某些特性进行了对比(VIEW DEMO),看完获益颇丰,以下是我的不完全翻译,以及自己实践的心得。

一、核心区别

React Router 4 与之前的版本最大的不同便是 router 在项目中的位置:

  • v2/v3 的版本采用的方式是将路由看成是一个整体的单元,与别的组件是分离的,一般会单独放到一个 router 文件中,对其进行集中式管理;并且,布局和页面的嵌套由路由的嵌套所决定。
import { Router, Route, IndexRoute, browserHistory } 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>

// 集中的 router,也可将其单独放到一个 router 文件中
const App = () => (
  <Router history={browserHistory}>
    <Route path="/" component={PrimaryLayout}>
      <IndexRoute component={HomePage} />
      <Route path="/users" component={UsersPage} />
    </Route>
  </Router>
)

render(<App />, document.getElementById('root'))
  • v4 的版本则将路由进行了拆分,将其放到了各自的模块中,不再有单独的 router 模块,充分体现了组件化的思想;另外,<BrowserRouter> 的使用与之前作为 history 属性传入的方式也不同了。
// v4 中改从 'react-router-dom' 引入的原因是因为还有个 native 版本,这个意味着是 web 版本
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'))

React Router v4 的这种方式让路由和组件之间的关系变得特别好理解,可以将 Route 就当做 component 组件一样使用,只不过此时的 URL 是其对应的 path;当 path 匹配时,则会渲染 Route 所对应的组件内容。

二、包含式路由与exact

在之前的版本中,在 Route 中写入的 path,在路由匹配时是独一无二的,而 v4 版本则有了一个包含的关系:如匹配 path="/users" 的路由会匹配 path="/"的路由,在页面中这两个模块会同时进行渲染。因此,v4中多了 exact 关键词,表示只对当前的路由进行匹配。

// 当匹配 /users 时,会同时渲染 UsersMenu 和 UsersPage
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>
)

三、独立路由:Switch

如果想要只匹配一个路由,除了 exact 属性之外,还可以使用 Swtich 组件。

const PrimaryLayout = () => (
  <div className="primary-layout">
    <PrimaryHeader />
    <main>
      <Switch>
        <Route path="/" exact component={HomePage} />  // 必须加上 exact,要不然 /users 也会匹配到该路由
        <Route path="/users/add" component={UserAddPage} />
        <Route path="/users" component={UsersPage} />
        <Redirect to="/" />
      </Switch>
    </main>
  </div>
)

采用 <Switch>,只有一个路由会被渲染,并且总是渲染第一个匹配到的组件。因此,在第一个路由中,还是需要使用 exact,否则,当我们渲染 '/users' 或 '/users/add' 时,只会显示匹配 '/' 的组件(PS:如果不使用 <Switch>,当我们不使用 exact 时,会渲染匹配的多个组件)。所以,将 '/user/add' 路由放在 '/users' 之前更好,因为后者包含了前者,当然,我们也可以同样使用 exact,这样就可以不用关注顺序了。

再来说一下 <Redirect> 组件,单独使用时,一旦当路由匹配到的时候,浏览器就会进行重定向跳转;而配合 <Switch> 使用时,只有当没有路由匹配的时候,才会进行重定向。例如,上面的例子,地址栏输入 '/test' 时,则会跳转到 '/',渲染 HomePage 页面。

四、"Index Routes" 和 "Not Found"

在 v4 的版本中废弃了 <IndexRoute>,而该用 <Route exact> 的方式进行代替。如果没有匹配的路由,也可通过 <Redirect> 来进行重定向到默认页面或合理的路径。

五、嵌套布局

首先,作者在文中给出了嵌套布局的场景:如想要扩展“用户模块”,需要有一个“浏览用户”的页面和“每个用户个人信息”的页面,对于“产品模块”也是同样。对于此类场景,作者在文中给出了两种实现方案进行对比。

首先,是最容易想到的方案,但是却不是很理想:

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>
)

很容易看到,BrowseUsersPageUserProfilePage 的布局是重复的,每次渲染子页面的时候,都会渲染整体的布局。如果项目比较大的话,会产生很多重复冗余的代码,也会影响整体的性能。

来看一下另一种 较优的方法,充分利用了 Route 的组件化思想:

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>
)

const BrowseUsersPage = () => <BrowseUserTable />
const UserProfilePage = props => <UserProfile userId={props.match.params.userId} />

这种方法将包含子页面的模块单独看成一个整体模块,然后将子模块嵌套在该模块中,那么当整体模块渲染的时候,布局就一次性渲染了;当匹配子模块路由的时候,就只会单独渲染子模块的那一部分。要 注意 的一点是:子页面还是需要明确父模块的路径(如 '/users'),以保证能够被匹配到。

将路由随着模块走,以组件化搭积木的方式来完成项目,真的是清晰很多啊~~

还可以通过 match 等对路径进行优化,减少重复性的代码输入:

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

const UserProfilePage = ({match}) => <UserProfile userId={match.params.userId} />

// 以下是自己加的测试代码
const UserNav = () => (
    <div>User Nav</div>
)
const BrowseUserTable = ({match}) => (
    <ul>
        <li><Link to={`${match.path}/bob`}>Bob</Link></li>
        <li><Link to={`${match.path}/Tom`}>Tom</Link></li>
        <li><Link to={`${match.path}/Jack`}>Jack</Link></li>
    </ul>
)
const UserProfile = ({ userId }) => <div>User: {userId}</div>;

六、Match

props.match 包含4个属性match.paramsmatch.isExactmatch.pathmatch.url

看一下 <UserProfile />match 属性:

match

1)match.path vs match.url

当没有参数的时候,match.pathmatch.url 是一样的,而当有参数的时候,两者就有区别了:

  • match.path:是指写在 <Route> 中的 path 参数;
  • match.url:是指在浏览器中显示的真实 URL。
const UserSubLayout = ({ match }) => {
    console.log(match.path)   // output: "/users"
    console.log(match.url)  // output: "/users"
    return (
      <div className="user-sub-layout">
        <aside>
          <UserNav />
        </aside>
        <div className="primary-content">
          <Switch>
            <Route path={match.path} exact component={BrowseUserTable} />
            <Route path={`${match.path}/:userId`} component={UserProfilePage} />
          </Switch>
        </div>
      </div>
    )
  }

const UserProfilePage = ({match}) => {
    console.log(match.path); // output: "/users/:userId"
    console.log(match.url); // output: "/users/bob"
    return <UserProfile userId={match.params.userId} />
}

作者强烈建议在写路由路径时使用 match.path,因为使用 match.url 最终会产生不可预料的场景,如下面这个例子:

const UserComments = ({ match }) => {
    console.log(match.params);  // output: {}
    return <div>UserId: {match.params.userId}</div>
}

const UserSettings = ({ match }) => {
    console.log(match.params);  // output: {userId: "5"}
    return <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>
)
  • 当访问 '/users/5/comments' 时渲染 'UserId: undefined';
  • 当访问 '/users/5/settings' 时渲染 'UserId: 5'。

为什么会 match.path 能够正常渲染,而使用 match.url 则不能呢?造成这种区别的原因是由于 {${match.url}/comments} 相当于硬编码 {'/users/5/comments'},在路径中并没有参数,只有一个写死的 5,这样,子模块便无法获取到 match.params 参数,因此,便不能正常渲染。

match.path 可用于构造嵌套的 <Route>,而 match.url 可用于构造嵌套的 <Link>

2)如何避免 Match 的冲突?

考虑这样一种情况:如果我们想要通过 '/users/add' 和 '/users/5/edit' 来对用户进行添加和编辑,但是在前面的例子中,我们知道 users/:userId 已经指向了 UserProfilePage,那按照之前的例子,是否意味着 users/:userId 需要指向一个可以同时实现编辑和预览功能的子页面模块?未必如此,因为 edit 和 profile 共享一个子页面,则可以通过以下方式进行实现:

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>
)

将 add 和 edit 页面放在 profile 之前,这样就可以实现按需匹配,如果将 profile 路径放在第一位的话,那么当访问 add 页面时,则会匹配 profile 页面,因为 add 匹配了 :userId

还有一种替代方法,可以将 profile 放在第一位:采用正则(path-to-regexp)对路径进行约束,如${match.path}/:userId(\\d+),这样 :userId 只能为 number 类型,则访问 /users/add 路径时便不会产生冲突。

七、其它

文中,作者最后自行实现了一个授权路由,另外,他还提到了React Router v4 中的其它部分,如 <Link> vs <NavLink>、URL Query Strings 以及 Dynamic Routes。

这里我比较感兴趣的是 URL 查询,因为在项目开发中,我需要通过 URL 的查询字符串来实现一些功能。如 URL的查询字符串为 /users?bar=baz:

  • 在之前的版本中,可以通过 this.props.location.query.bar 进行获取(React Router 使用教程 );
  • 在 v4 中,首先看一下 this.props.location
props.location

由此可见,v4 版本中的 query 参数已经不见了,也就意味着,在该版本中,已经无法获得 URL 的查询字符串了。但是,我们却可以获取到 search 字段,只不过需要我们自行对其进行处理。文中,作者推荐了 query-string 用于处理 URL 查询字符串。

★、彩蛋(^_-)

嘿嘿~推荐一部国庆看了2遍的电影:《Me Before You》

刚开始只觉得女主的眉毛很喜感,看完深深地喜欢上了这个乐观善良、十分有趣的姑娘(有趣的灵魂万里挑一);特别喜欢她的穿着打扮,给绝望之中的人带去生机与活力。印象最深刻的是女主在洒满阳光的床上被闹铃叫醒,然后打扮地美美的迎接每一天,搭配着《Happy with me》的音乐,感觉为每天沉闷单调的生活点缀上了一抹绚丽的色彩。

一句话概括:美好而感伤的爱情故事啊~ps:看完电影去刷豆瓣的时候,才发现女主是《权利的游戏》中的龙妈~~

React&Redux
Web note ad 2