你不知道的 React Router 4

原文链接:https://zhuanlan.zhihu.com/p/28585911

几个月前, React Router 4 发布,我能清晰地感觉到来自 Twitter 大家对新版本中其 大量的修改 的不同声音。诚然,我在学习 React Router 4 的第一天,也是非常痛苦的,但是,这并不是因为看它的 API,而是反复思考使用它的 模式策略,因为 V4 的变化确实有点大, V3 的功能它都有,除此之外,还增加了一些特性,我不能直接将使用 V3 的心得直接迁移过来,现在,我必须重新审视 routerlayout components 之间的关系


你不知道的 React Router 4_第1张图片


本篇文章不是把 React Router 4 API 再次呈现给读者看,而是简单介绍其中最常用的几个概念,和重点讲解我在实践的过程中发现的比较好的 模式策略

不过,在阅读下文之前,你得首先保证以下的 概念 对你来说 并不陌生

  • React stateless(Functional) 组件
  • ES6 的 箭头函数 和它的 隐式返回
  • ES6 的 解构
  • ES6 的 模板字符串

如果你就是那 万中无一 的绝世高手,那么你也可以选择直接 view demo


一个全新的 API

React Router 的早期版本是將 routerlayout components 分开,为了彻底搞清楚 V4 究竟有什么不同,我们来写两个简单的 example 就明白了

example app 就两个 routes,一个 home,一个 user

V3

import React from "react";
import { render } from "react-dom";
import { Router, Route, IndexRoute, Link, browserHistory } from "react-router";

const PrimaryLayout = props =>
  <div className="primary-layout">
    <header>Our React Router 3 App</header>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/user">User</Link>
      </li>
    </ul>
    <main>
      {props.children}
    </main>
  </div>;

const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;

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

render(<App />, document.getElementById("root"));
上篇文章给大家推荐了一个在线 react 编译器 stackblitz,本篇文章再给大家推荐一个不错的, codesandbox,专门针对 react 且开源,正所谓, 实践是检验真理的唯一标准,这也是一种良好的学习习惯

上面代码中有几个关键的点在 V4 中就不复存在了

  • 集中式 router
  • 通过 嵌套,实现 Layoutpage 嵌套
  • Layoutpage 组件 是作为 router 的一部分

我们使用 V4 来实现相同的应用程序对比一下

import React from "react";
import { render } from "react-dom";
import { BrowserRouter, Route, Link } from "react-router-dom";

const PrimaryLayout = () =>
  <div className="primary-layout">
    <header>Our React Router 4 App</header>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/User">User</Link>
      </li>
    </ul>
    <main>
      <Route path="/" exact component={HomePage} />
      <Route path="/user" component={UsersPage} />
    </main>
  </div>;

const HomePage = () => <h1>Home Page</h1>;
const UsersPage = () => <h1>User Page</h1>;

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

render(<App />, document.getElementById("root"));
注意,我们现在 import 的是 BrowserRouter,而且是从 react-router-dom 引入,而不是 react-router

接下来,我们用肉眼就能看出很多的变化,首先,V3 中的 router 不在了,在 V3 中,我们是将整个庞大的 router 直接丢给 DOM,而在 V4 中,除了 BrowserRouter, 我们丢给 DOM 的是我们的应用程序本身

另外,V4 中,我们不再使用 {props.children} 来嵌套组件了,替代的 ,当 route 匹配时,子组件会被渲染到 书写的地方


Inclusive Routing

在上面的 example 中,读者可能注意到 V4 中有 exact 这么一个 props,那么,这个 props 有什么用呢? V3 中的 routing 规则是 exclusive,意思就是最终只获取一个 route,而 V4 中的 routes 默认是 inclusive 的,这就意味着多个 可以同时匹配和呈现

还是使用上面的 example,如果我们调皮地删除 exact 这个 props,那么我们在访问 /user 的时候,HomeUser 两个 Page 都会被渲染,是不是一下就明白了

为了更好地理解 V4 的匹配逻辑,可以查看 path-to-regexp,就是它决定 routes 是否匹配 URL

为了演示 inclusive routing 的作用,我们新增一个 UserMenu 组件如下

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

现在,当访问 /user 时,两个组价都会被渲染,在 V3 中存在一些模式也可以实现,但过程实在是复杂,在 V4 中,是不是感觉轻松了很多


Exclusive Routing

如果你只想匹配一个 route,那么你也可以使用 exclusive routing

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

中只有一个 会被渲染,另外,我们还是要给 HomePage 所在 添加 exact,否则,在访问 /user/user/add 的时候还是会匹配到 /,从而,只渲染 HomePage。同理,不知有没同学注意到,我们将 /user/add 放在 /user 前面是保证正确匹配很有策略性的一步,因为,/user/add 会同时匹配 /user /user/add,如果不这么做,大家可以尝试交换它们两个的位置,看下会发生什么

当然,如果我们给每一个 都添加一个 exact,那就不用考虑上面的 策略 了,但不管怎样,现在至少知道了我们还有其它选择

组件不用多说,执行浏览器重定向,但它在 中时, 组件只会在 routes 匹配不成功的情况下渲染,另外,要想了解 如何在 non-switch 环境下使用,可以参考下面的 Authorized Route

"Index Routes" 和 "Not Found"

V4 中也没有 ,但 可以实现相同的功能,或者 重定向到默认的有效路径,甚至一个找不到的页面


嵌套布局

接下来,你可能很想知道 V4 中是如何实现 嵌套布局 的,V4 确实给我们了很多选择,但这并不一定是好事,表面上,嵌套布局 微不足道,但选择的空间越大,出现的问题也就可能越多

现在,我们假设我们要增加两个 user 相关的页面,一个 browse user,一个 user profile,对 product 我们也有相同的需求,实现的方法可能并不少,但有的仔细思考后可能并不想采纳

第一种,如下修改 PrimaryLayout

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

虽然这种方法可以实现,但仔细观察下面的两个 user 页面,就会发现有点潜在的 问题

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>
)
userId 通过 props.match.params 获取, props.match 赋予给了 中的任何组件。除此之外,如果组件不通过 来渲染,要访问 props.match,可以使用 withRouter() 高阶组件来实现

估计大家都发现了吧,两个 user 页面中都有一个,这明显会导致不必要的请求,以上只是一个简单实例,如果是在真实的项目中,不知道会重复消耗多少的流量,然而,这就是由我们以上方式使用路由引起的

接下来,我们再看看另一种实现方式

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

我们用 2 个 routes 替换之前的 4 个 routes

注意,这里我们没有再使用 exact,因为,我们希望 /user 可以匹配任何以 /user 开始的 routeproducts 同理

使用这种策略,子布局也开始承担起了渲染 routes 的责任,现在,UserSubLayout 长这样

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

现在是不是解决了第一种方式中的生命周期,重复渲染的问题呢?

但有一点值得注意的是,routes 需要识别它的完整路径才能匹配,为了减少我们的重复输入,我们可以使用 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 可以帮我们获取 userIdroutes

match 对象为我们提供了 match.paramsmatch.path,和 match.url 等属性

match.path vs match.url

最开始,可能觉得这两者的区别并不明显,控制台经常出现相同的输出,比如,访问 /user

const UserSubLayout = ({ match }) => {
  console.log(match.url)   // output: "/user"
  console.log(match.path)  // output: "/user"
  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>
  )
}
match 在组件的参数中被解构,意思就是我们可以使用 match.path 代替 props.match.path

虽然我们看不到什么明显的差异,但需要明白的是 match.url浏览器 URL 的一部分match.path 是我们为 router 书写的路径

如何选择

如果我们是构建 route 路径,那么肯定使用 match.path

为了说明问题,我们创建两个子组件,一个 route 路径来自 match.url,一个 route 路径来自 match.path

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

然后,我们按下面方式来访问

  • /user/5/comments
  • /user/5/settings

实践后,我们发现,访问 comments 返回 undefined,访问 settings 返回 5

正如 API 所述

match:
path - (string) The path pattern used to match. Useful for building nested s
url - (string) The matched portion of the URL. Useful for building nested s


避免 Match Collisions

假设我们的 App 是一个仪表盘,我们希望访问 /user/add/user/5/edit 添加和编辑 user。使用上面的实例,user/:userId 已经指向 UserProfilePage,我们这是需要在 UserProfilePage 中再添加一层 routes 么?显示不是这样的

const UserSubLayou = ({ match }) =>
  <div className="user-sub-layout">
    <aside>
      <UserNav />
    </aside>
    <div className="primary-content">
      <Switch>
        <Route exact path={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>;

现在,看清楚这个策略了么

另外,我们使用 ${match.path}/:userId(\\d+) 作为 UserProfilePage 对应的 path,保证 :userId 是一个数字,可以避免与 /users/add 的冲突,这样,将其所在的 丢到最前面去也能正常访问 add 页面,这一招,就是我在 path-to-regexp 学的


Authorized Route

在应用程序中限制未登录的用户访问某些路由是非常常见的,还有对于授权未授权的用户 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/forgot-password 肯定在 UnauthorizedLayout 中,另外,当用户登陆时,我们将判断所有的路径都有一个 /app 前缀以确保是否登录。如果用户访问 /app 开头的页面但并没有登录,我们将会重定向到登录页面

下面就是我写的 AuthorizedRoute 组件,这也是 V4 中一个惊奇的特性,可以为了满足某种需要而书写自己的路由

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

点击 这里 可以查看的我的整个 Authentication


总结

React Router 4 相比 V3,变化很大,若是之前的项目使用的 V3,不建议立即升级,但 V4 V3 确实存在较大的优势


原文链接:All About React Router 4


你可能感兴趣的:(React.js)