该篇为翻译文章:原文为:Leveling Up With React: React Router

该系列文章目录:
第一部分:学习React Router(当前)
第二部分:React的“容器组件”和“可视化组件”-译文(原文)
第三部分:React 之 Redux-译文(原文)

当我刚开始学习React的时候,我看了很多入门教程(比如: 1, 2, 3, 4)。很多教程都只是教我们如何做一个组件放到页面当中,这些教程其实都只是讲了一些JSX的基础,但我想给大家介绍的远远不只这些,而是更广泛的一些东西,比如一个真正的单页面应用(SPA)。所以,看这篇文章之前,你要有一些React的基础。

源码

文章所写Demo的源码可以到GitHub进行下载。

为了使例子看起来一目了然,ReactReact Router都是用的CDN版本。所以在例子当中你不会看到require()import这些。但在这篇文章结束的时候,我们将介绍使用Webpack和Babel,这将全部使用ES6!

React-Router

首先,React并不是一个框架,它只是一个UI库。因此,它不能解决所有问题。我们可以用它来创建一些组件和管理组件状态,但如果单单靠它来做一个复杂的SPA应用,是远远不够的。如果用React来做应用,我们还需要其它的东西来支持。React Router是最我们第一个要用到的。

如果你之前用过其它的前端路由管理的库,会发现它们都是差不多的。但是React Router是不同的,它使用的是JSX语法,可能刚用的时候会有点奇怪。
它用起来就像一般的组件渲染一样:

var Home = React.createClass({
  render: function() {
    return (<h1>Welcome to the Home Page</h1>);
  }
});

ReactDOM.render((
  <Home />
), document.getElementById('root'));

上面的Home组件如果用React Router渲染的话是这样的:


ReactDOM.render(( <Router> <Route path="/" component={Home} /> </Router> ), document.getElementById('root'));

注意,上面的<router><route>是两个不同的东西。可以看出,它们都是React组件,但是它们不会创建出DOM元素。这些组件只是定义了一些让整个应用跑起来的规则。通过这些你会发现,有时候组件不是直接生成DOM元素的,而是为了配合其它组件的生成而存在的。
</route><route>定义了一个规则,当我访问首页/的时候,那么此时,Home组件将会被渲染到ID为root元素当中去。

配制多个路由规则

前面我们配制了一个首页,但实际项目当中不可能只有一个页面的。使用React Router来配制多个路由也非常的简单:


ReactDOM.render(( <Router> <Route path="/" component={Home} /> <Route path="/users" component={Users} /> <Route path="/widgets" component={Widgets} /> </Router> ), document.getElementById('root'));

每一个</route><route>都指定了一个组件和路径。当我们访问相应路径的时候,那么它就会渲染不同的组件到root当中去。

可重用的布局

我们已经看到了一个单页面应用的雏形,真正的SPA应用不只是这样就OK了,通过知道了上面的路由配制,我们显然知道了每一个组件就是一个完整的HTML页面,但是如何做到代码的重用呢?比如,一个网站有相同的头部、侧边栏、底部等,我们如何把它们做成可以重要使用的组件呢?

比如我们现在要做这样一个东西:

01

把这个模型拆开,做到重用度最高,你可能会这样做:

02

突然,你接到一个要求,就是不但要搜索User,还要搜索Widget,但这个功能的界面和User的搜索列表是一样的。那么现在Widget搜索页的组件的划分是这样的:

03

“搜索”的布局就可以这样进行划分,即有一个组件作为父层,然后把不同的搜索结果放到这个父层组件当中:

04

其实这是一种非常常规的策略,你以前可能用其它的模板引擎做过相同的划分。现在,我们通过HTML来看一下:

<div id="root">

  <!-- Main Layout -->
  <div class="app">
    <header class="primary-header"><header>
    <aside class="primary-aside"></aside>
    <main>

      <!-- Search Layout -->
      <div class="search">
        <header class="search-header"></header>
        <div class="results">

          <!-- User List -->
          <ul class="user-list">
            <li>Dan</li>
            <li>Ryan</li>
            <li>Michael</li>
          </ul>

        </div>
        <div class="search-footer pagination"></div>
      </div>

    </main>
  </div>

</div>

HTML写好后,我们来把它改成React组件:


var MainLayout = React.createClass({ render: function() { // Note the `className` rather than `class` // `class` is a reserved word in JavaScript, so JSX uses `className` // Ultimately, it will render with a `class` in the DOM return ( <div className="app"> <header className="primary-header"></header><header> <aside className="primary-aside"></aside> <main> {this.props.children} </main> </header></div> ); } }); var SearchLayout = React.createClass({ render: function() { return ( <div className="search"> <header className="search-header"></header> <div className="results"> {this.props.children} </div> <div className="search-footer pagination"></div> </div> ); } }); var UserList = React.createClass({ render: function() { return ( <ul className="user-list"> <li>Dan</li> <li>Ryan</li> <li>Michael</li> </ul> ); } });

我们利用路由的嵌套将UserList放到了SearchLayout里,然后再放到MainLayout里。注意,这时React是在父层当中通过this.props.children来找到子级并放进来的。所有的组件里面都会有一个this.props.children,但只有当这个组件有嵌套组件时,这个属性才会起作用,否则它将是null

嵌套路由

上面的组件写完之后,那么我们的路由就可以这样设置:



ReactDOM.render(( <Router> <Route component={MainLayout}> <Route component={SearchLayout}> <Route path="users" component={UserList} /> </Route> </Route> </Router> ), document.getElementById('root'));

组件将会按照这个路由的嵌套来进行嵌套。当我们访问/users时,React Router将会把UserList放到SearchLayout里,然后SearchLayout放到MainLayout里,最后整个会放到root当中。

注意,我们并没有设置访问/时的首页路由,也没有设置搜索widgets的路由,现在我们把它们加上:


ReactDOM.render(( <Router> <Route component={MainLayout}> <Route path="/" component={Home} /> <Route component={SearchLayout}> <Route path="users" component={UserList} /> <Route path="widgets" component={WidgetList} /> </Route> </Route> </Router> ), document.getElementById('root'));

你会发现,JSX是遵循XML的语法的,比如一个组件可以写成一个单标签:</route><route></route>或者分开写:<route>...</route>。实际上,JSX里面不只有自定义的组件 ,还有真正的DOM元素,比如<div></div>在JSX会被解析渲染成DOM元素<div></div>

<route component={SearchLayout}>现在有两个子路由,如果访问/users或者/widgets,那么就会加载相应的组件。

IndexRoutes

React Router还有许多其它的用法,比如上面例子的写法还可以写成这样:


ReactDOM.render(( <Router> <Route path="/" component={MainLayout}> <IndexRoute component={Home} /> <Route component={SearchLayout}> <Route path="users" component={UserList} /> <Route path="widgets" component={WidgetList} /> </Route> </Route> </Router> ), document.getElementById('root'));

Route 的属性

有时,</route><route>指定了组件但是没有指定路径(path),比如上面有一条设置了SearchLayout组件,但没有path。还有时候 ,可能只需要设置path,而不需要指定组件,从这个例子开始:


<Route path="product/settings" component={ProductSettings} /> <Route path="product/inventory" component={ProductInventory} /> <Route path="product/orders" component={ProductOrders} />

/product重复了好多次,我们可以把重复的部分换成一个新的</route><route>:


<Route path="product"> <Route path="settings" component={ProductSettings} /> <Route path="inventory" component={ProductInventory} /> <Route path="orders" component={ProductOrders} /> </Route>

你会发现,当我们访问/product的时候,此时它会不知道如何去渲染,这时候可以添加之前说到过的IndexRoute:


<Route path="product"> <IndexRoute component={ProductProfile} /> <Route path="settings" component={ProductSettings} /> <Route path="inventory" component={ProductInventory} /> <Route path="orders" component={ProductOrders} /> </Route>

使用 <link /> 代替 <a>

当我们在路由当中添加链接时,你需要使用<link to=""/>来代替<a href="">。当我们使用了<link />组件时,React Router会帮你在DOM当中渲染出a标签。

让我们来添加一些链接到MainLayout


var MainLayout = React.createClass({ render: function() { return ( <div className="app"> <header className="primary-header"></header> <aside className="primary-aside"> <ul> <li><Link to="/">Home</Link></li> <li><Link to="/users">Users</Link></li> <li><Link to="/widgets">Widgets</Link></li> </ul> </aside> <main> {this.props.children} </main> </div> ); } });

<link />to属性将最终被解析成a标签的href,比如:


<Link to="/users" className="users">

当渲染到DOM当中会变成:


<a href="/users" class="users">

如果你想写一个链到外面的链接 ,那么直接使用a标签就可以了。更多资料请查看 IndexRoute和Link的官方文档

当前Link

<link />有一个很方便的属性来添加激活状态:


<Link to="/users" activeClassName="active">Users</Link>

如果现在访问/user路径 ,那么React Router会给<link />生成的a标签添加一个active的class。更多请访问这里

浏览器的历史记录

为了防止大家感到困惑,我到现在才说这个问题。<router>需要一个历史管理工具 browserHistory


var browserHistory = ReactRouter.browserHistory; ReactDOM.render(( <Router history={browserHistory}> ... </Router> ), document.getElementById('root'));

React Router之前的一些版本当中是没有history属性不是必须的。默认是用hashHistory,顾名思义,就是使用#哈希符号来管理历史,比如这样:

example.com
example.com/#/users?_k=ckuvup
example.com/#/widgets?_k=ckuvup

当用了browserHistory后,路径则会变成这样:

example.com
example.com/users
example.com/widgets

当你在前端使用了browserHistory后,服务端有一点是有一点要非常注意的。比如你现在访问example.com,然后通过里面的链接点到了/users或者/widgetsReact Router操作管理着这一切。但是,如果你直接在浏览器的地址栏里输入example.com/widgets或者之前点击链接打开了example.com/widgets,现在重新刷新了,那么浏览器就会向服务器进行发送请求了,请求什么呢?就是/widgets。如果服务器端没有相应的反馈,那么就会返回404了:

05

React Router推荐在服务端使用通配符来防止404错误的发生。但是使用这种方法后,不管向服务器发送什么样的路由请求,服务器都会返回相同的HTML内容。如果现在直接访问example.com/widgets,即使服务器返回的HTML内容和之前一样,React Router也能够加载对的组件。

对于用户来说,只要前端的页面加载内容是正确的,他不会知道服务端返回了什么。但是对于我们开发者来说可能比较担心服务器老是返回相同的HTML会不会有什么问题。在我们的示例代码当中,将一直使用“通配符”这种方法,但是当我们在实际项目当的时候,你就需要根据你的需要来配制服务端的路由了。

使用browserHistory进行重定向

browserHistory是一个单例对象,所以你可以在你的任何文章当中调用它。如果你需要在代码当中来进行手动的进行重定向,你可以使用这个方法:


browserHistory.push('/some/path');

路由的匹配

React router当中路由的匹配和其它的方式是差不多的:


<Route path="users/:userId" component={UserProfile} />

当我们访问到以user/开头,并且后面跟一个值的时候,比如/users/1,/users/143,都会进行匹配到。

React Router将会把:userId的值作为UserProfile的属性传送过去。这个属性在UserProfile当中可以用this.props.params.userId进行获取到。

一个Demo

写到这里,我们完全可以做出一个简单的Demo出来了。

See the Pen React-Router Demo by Brad Westfall (@bradwestfall) on CodePen.

上面的例子当中,当你点了链接之后,你会发现浏览器的后退和前进按钮是有用的,这就使用了历史管理。

ES6

上面的例子当中,ReactReactDOMReactRouter都是用的CDN版本。在ReactRouter当中有我们需要的RouterRoute组件。所以我们也可以这样用:


ReactDOM.render(( <ReactRouter.Router> <ReactRouter.Route ... /> </ReactRouter.Router> ), document.getElementById('root'));

但是,如果使用ES6的解构赋值语法,我们可以这样写:


var { Router, Route, IndexRoute, Link } = ReactRouter

译者:

如果不清楚ES6的解构赋值,可以看《ES6中的解构赋值》一文。

使用webpack 和 Babel进行打包

  • Webpack主要是用来进行JS的打包。(当然它还有许多其它的用途)
  • Babel主要是将ES6的语法转成ES5的讲法,因为现在不是所有浏览器都支持ES6的讲法。

如果你还不太习惯用这些工具,不用担心,在例子当中已经将所有的东西都设置好了,你只需要关注React这一块就行。但是你仍然需要看一下文档(GitHub上面的README.md)。

一些废弃的语法

当你在网上搜索关于React Router的资料时,可能会搜索到很多,但是它们用的版本可能大部分都是pre-1.0,实际上pre-1.0版本当中有很多的语法已经被废弃了,这里列举一些:

  1. <route name=""></route><route path=""></route>代替
  2. <route handler=""></route><route component=""></route>代替
  3. <notfoundroute></notfoundroute>废弃,替代品点这里
  4. <routehandler></routehandler>废弃
  5. willTransitionTo废弃,使用onEnter属性替代
  6. willTransitionFrom废弃,使用onLeave属性替代
  7. “Locations” 现在叫做 “histories”.

更详细的区别请查阅官方文档 : 1.0.02.0.0