ReactRouter 从零开始

ReactRouter 是什么

往常的开发经验告诉我们,一个 web 应用(网站)的 router 都是在后端控制的。
例如说,你访问 http://blog.bluerain.io 其实是访问的后端应用的 ‘/’ 路由,访问本博文对于后端来讲是访问的 ‘/p/*.html’ 路由。 再由后端进行页面的转发而呈现内容。
在这种情况下,基本上大部分不同的路由对应不同的页面内容(源码)。对于后端应用就是不同的页面模板。
但是某些情况下,这种规则等同于无效。例如今天的主题相关的 React 单页应用,整站可能都是一个页面(源码)。
举个网站例子:https://store.docker.com

而 ReactRouter 就是配合 React 单页应用而诞生的前端路由。当你用特殊的 Router 链接标签时,点击并不会传递给后端,但是数据能在 React 组件之间传递,显示出需要的组件以及通过后端获取的异步加载的数据内容,并且会更新你的地址栏。
毕竟不同的 URL 实质上是在传递不同的参数(ReactRouter 相当于把参数传递给组件)和请求不同类型的网页(ReactRouter 相当于根据规则显示不同的组件)。所以此时后端可以将全部或者需要的一部分都转发给同一个页面,交给前端 router 来根据 URL 呈现不同的内容。
例如本博客的首页(/)和文章页面(/p/*.html)以及 Tweet (/tweet)其实是一同个页面。

从最基本开始

首先当然是安装 react-router 库啦~

npm i --save-dev react-router

假设已经搭好前端脚手架,只需一个带有三个组件的 JSX 文件即可体验效果,创建 entry.jsx :

import React from 'react'
import { render } from 'react-dom'
import { Router, Route, browserHistory } from 'react-router'

// App 组件
const App = (props) => (
  <div>
    <h1>App</h1>
    <ul>
      <li>
        <Link to='/about'>About</Link>
      </li>
      <li>
        <Link to='/inbox'>Inbox</Link>
      </li>
    </ul>
  </div>
)
// About组件
const About = (props) => <h3>About</h3>

// Inbox组件
const Inbox = (props) => (
  <div>
    <h2>Inbox</h2>
  </div>
)

// 配置 Router
render((
  <Router history={browserHistory}>
    <Route path='/' component={App} />
    <Route path='/about' component={About} />
    <Route path='/inbox' component={Inbox} />
  </Router>
), document.body)

运行页面,会发现有两个链接。About 和 Inbox ,对 这就是 App 组件的内容。点击 About 或者 Inbox 也会显示相应组件的内容。
很显而易见的配置项和其结果,Route 标签的 path 路由到指定的 component 。Link 标签的 to 属性访问指定路由。path=‘/‘,有缺省显示(index)的效果。
Router 标签的 history 属性跟地址栏相关,browserHistory 是 react-router 提供的 history 参数之一,也是最应该使用的那个,之后会有其它的介绍。现在先记住传递这个参数即可。

传参和包含

其实上面的例子就已经可以适应一部分场景了,例如几个不同的页面组件的切换,没有 URL 参数传递和共同元素内容。
当然,一个 App 应用是不会有那么简单的。所有还需要两种很重要的东西。

  1. 组件从 URL 中接受参数,例如:我想访问某 id 的用户,路由规则为 : /user/:id
  2. 不同组件页面之间公共的组件,例如:使用百度的时候,搜索框上面的 “网页 图片 贴吧 知道” 就是公共的,无论是那种结果的搜索页面。

第一点,ReactRouter 支持了这种规则的写法。并且会将参数放入 props.params 属性中传递给 target 组件。

// ....
// 1. 添加组件和数据源
// 用户数据来源
const users = ['小明', '小红', '张三', '李四'];
// 用户组件
const User = (props) => <div>{users[props.params.id]}</div>
// ....
// 2. 给 App 组件添加一个 Link
// App 组件
const App = (props) => (
  <div>
    <h1>App</h1>
    <ul>
      <li>
        <Link to='/about'>About</Link>
      </li>
      <li>
        <Link to='/inbox'>Inbox</Link>
      </li>
      <li>
        <Link to='/users/1'>User-1</Link>
      </li>
    </ul>
  </div>
)
// ....
// 3. 添加一个路由
<Route path='/users/:id' component={User} />

此时,点击首页的 User-1 链接会显示 小红。因为 传递的 id 是1,对应的 users[1] 的数据就是”小红”。页面 URL 会变成 “/users/1”。
当然,把 Link 中的 /users/+任意参数 都行的。这点和大部分语言和相关 web 框架的后端路由模式是一模一样的。

第二点,类似于 bootstrap 的“导航”。几个不同的组件切换显示但是有公共的组件是不变的。这里我们把 App 看做一个全局组件,其余组件都是它的子组件,被它包含。

修改 App 组件(加上 children 以及修改 Link)

const App = (props) => (
  <div>
    <h1>APP!</h1>
    <ul>
      <li><Link to="/" >/</Link></li>
      <li><Link to="/about">/about</Link></li>
      <li><Link to="/inbox">/inbox</Link></li>
    </ul>

    <div id="children">
      {props.children}
    </div>
  </div>
)
// 添加一个 Index 组件
const Index = () => (
  <div>
    <h2>Index!</h2>
  </div>
)
// 修改 router
render((
  <Router history={browserHistory}>
    <Route path="/" component={App}>
      <IndexRoute component={Index} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox} />
    </Route>
  </Router>
), document.body)

可以看到,经过 router 的配置,About、Inbox 和 Index 的 Route 成了 App 的子 Route,Index 被配置成 App 下的默认显示 Route 。
接着再运行页面,默认显示出 App 的内容 以及 Index 的内容。点击链接会把 Index 的内容替换成相应组件内容,但是 App 组件不会被改变,这就是效果。

细节部分

  1. 复杂的 Link 参数传递:Link 标签的 to 属性可以赋值一个 { { pathname: “, query: { } } 的对象来传递多个参数:

    <li><Link to={{ pathname: '/users', query: { name: '小明' } }>查找小明</Link></li>
    

    接收 name 参数:this.props.location.query.name
    有一点要注意,传参(查询参数或者路径参数)都只能在 Link 标签中做,在 Route 标签是无法传递参数的。

  2. 路由重定向(Redirect 标签):例如在访问 inbox 以后,有一个 inbox/user/1 链接。但是需要 URL 是 /users/1 并且路由的嵌套关系不变。

    <Route path="/" component={App}>
      <Route path="inbox" component={Inbox}>
        <Redirect from="users/:id" to="/users/:id" />
      </Route>
      <Route component={Inbox}>
        <Route path="users/:id" component={User} />
      </Route>
    </Router>
    

后端路由

例如我的后端是 ruby 语言,Web 框架:Sinatra 。我将首页(/)和文章页(/p/*.html)还有 Tweet 页(/tweet)转发到同一个页面:

get /(\/$)|(\/p\/[^.]+.html)|(\/tweet$)/ do
  # ... 页面转发
end

匹配上述规则的 request.path 会被转发到一个 haml 文件,内容是:

!!!
%html{:lang => 'zh-Hans'}
  %head
    %meta{:content => :'text/html; charset=UTF-8', :'http-equiv' => :'Content-Type'}/
    %meta{:charset => :'UTF-8'}/
    %title Blog for BlueRain
  %body
    #app
  %script{:src => :"http://7xl780.com1.z0.glb.clouddn.com/blog/prd_blog.#{hash}.js"}

这样下来,在首页和文章页切换的时候并不会进入后端 router,由 ReactRouter 控制。而且还能保证在单独打开链接的情况下(不被 ReactRouter 控制跳转)能正确的被后端实现页面转发,即使可能不同的的 URL 类型和参数都被后端输出为同一个页面源码。不过没关系,因为由前端路由会根据 URL 渲染组件而呈现正确内容。

开发中遇到的坑

  • 关于博客右上角导航

    目前,右上角导航的“建议、订阅、关于”都是文章的链接,所以在一个导航下(或者一个文章下)访问另一个导 航链接,就会出现这个问题:
    即如果当前组件已经是 User 了。例如当前访问的是:/user/1。假若我再访问一个 /user/2 的路由,数据会变化 吗? 答案是,不一定会:

    在这种情况下,也就是在一个 Route 的基础上又访问了一遍这个 Route,组件实例会是同一个。构造函数、componentWillDidMount 等方法都不会再调用第二次,而通常你会把数据更新逻辑写在这些方法中。既然 没有被调用,render 方法都不会执行,所以你的页面不会有任何的变化。虽然你访问的是一个新 User(参数不同)的 Route。
    不过,虽然是一个组件的实例,但是并不表示没有任何状态产生。因为 Route 会带进新的参数,所以会调用组件的 componentWillReceiveProps 方法,那么把数据更新逻辑写在此方法中即可。当然正常来讲,会把更新状态的逻辑独立出来,供多个方法调用,问题即可解决。

    这个问题困扰了我一小会儿,起初我以为是没有仔细读 React-Router 的文档遗漏了细节,准备下次仔细看一下。
    后来发现好像并不是这么回事,把几个生命周期方法都添加上调试才发现的问题所在。当然,定位到问题了就好解决了。

最后

至此,入门结束。后续内容,不定时更新。