react-router

静态路由和动态路由

react-router v4是一个很是大的版本改动,具体体如今从“静态路由”到“动态路由”的转变上。通常将“静态路由”看做一种配置,当启动react项目时,会先生成好一个路由表,发生页面跳转时,react会根据地址到路由表中找到对应的处理页面或处理方法。而动态路由不是做为一个项目运行的配置文件存储在外部,它在项目render的时候才开始定义,router的做者认为route应当和其它普通组件同样,它的做用不是提供路由配置,而是一个普通的UI组件。而这也符合react的开发思想——一切皆组件。 因为我本身对以前版本的路由了解很少,这里就不作比较了,有兴趣的小伙伴能够本身去了解一下。这里引一段router做者为何要作这样大的改动的解释:html

To be candid, we were pretty frustrated with the direction we’d taken React Router by v2. We (Michael and Ryan) felt limited by the API, recognized we were reimplementing parts of React (lifecycles, and more),
and it just didn’t match the mental model React has given us for composing UI.
We ended up with API that wasn’t “outside” of React, an API that composed, or naturally fell into place, 
with the rest of React.
坦率地说,咱们对于以前版本的Route感到十分沮丧,我和个人小伙伴意识到咱们在从新实现react的部分功能,
好比生命周期和其它更多的,可是这一点都不符合react的模型设计(UI组件)。
咱们真正想要开发出的不是脱离了react的API ,而是一个自己就属于react一部分的API.这才是咱们想要的route(英语功底太差,你们将就着看吧)
——引自react-router的做者

复制代码

在Web前端开发中,咱们常常会须要处理页面路由问题。习惯上,路由信息会在一个地方集中配置好,咱们能够称之为“静态路由”,或者叫“中心化式路由”。以react-router v3版本为例,代码相似下面这样:前端

import { Router, Route, IndexRoute, browserHistory } from 'react-router'

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

render(<App />, document.getElementById('app'))


复制代码

能够看到,在程序的顶层组件上配置好了全部路由信息,并经过嵌套关系体现不一样的层次。可是,react-router v4版本进行了革命性的改动,使之更加符合React的“组件化”思想,咱们能够称之为“动态路由”,或者借用区块链中的术语,称之为“去中心化路由”。用v4版本改写后的代码相似于下面这样:node

import { BrowserRouter, Route } from 'react-router-dom'

const App = () => (
  <BrowserRouter>
    <RootPage />
  </BrowserRouter>
)

const RootPage = () => (
  <div>
    <Route path="/" exact component={HomePage} />
    <Route path="/users" component={UsersPage} />
  </div>
)

render(<App />, document.getElementById('app'))


复制代码

能够发现,路由的配置再也不是所有位于顶层组件中了,而是分散在不一样的组件中,经过组件的嵌套关系来实现路由的层次。另外,和静态路由事先定义好全部页面不一样,动态路由能够在渲染时根据路径匹配结果,动态决定渲染哪些组件,这样就能够充分实现页面的复用,减小重复渲染。react

安装

正如我前面所说,对于web应用,咱们只须要安装react-router-dom:webpack

不过在node_modules下你依然会看到react-router的身影,这是react-router-dom依赖的包,另外还有一个history包,这个下面会提到。nginx

<Router>git

<Router>是实现路由最外层的容器,通常状况下咱们再也不须要直接使用它,而是使用在它基础之上封装的几个适用于不一样环境的组件,react-router-dom的Router有四种:github

通常咱们不多会用到<MemoryRouter>和<StaticRouter>,在web应用中更多的是用react-router-dom扩展出来的<BrowserRouter>和<HashRouter>,这两个就是我前面提到的前端路由的两种解决办法的各自实现。web

为了避免被后面的一些配置弄迷糊,咱们从<Router>的实现源码来看看路由到底传了些什么东西。算法

router.js

class Router extends React.Component {
  //检测接收的参数
  static propTypes = {
    history: PropTypes.object.isRequired, //必须传入
    children: PropTypes.node
  }

  //设置传递给子组件的属性
  getChildContext() {
    return {
      router: {
        ...this.context.router, 
        history: this.props.history, //核心对象
        route: {
          location: this.props.history.location, //history里的location对象
          match: this.state.match //当路由路径和当前路径成功匹配,一些有关的路径信息会存放在这里,嵌套路由会用到它。
        }
      }
    }
  }
    state = {
      match: this.computeMatch(this.props.history.location.pathname)
    }

  computeMatch(pathname) {
    return {
      path: '/',
      url: '/', 
      params: {}, //页面间传递参数
      isExact: pathname === '/'
    }
  }
}
复制代码

这里面最重要的就是须要咱们传入的history对象,我前面提到过咱们通常不会直接使用<Router>组件,由于这个组件要求咱们手动传入history对象,但这个对象又很是重要,并且不一样的开发环境须要不一样的history,因此针对这种状况react-router才衍生了两个插件react-router-dom和react-router-native(我认为这是比较重要的缘由,浏览器有一个history对象,因此web应用的路由都是在此对象基础上扩展的)。 接着让咱们来看一下react-router-dom用到的来自history的两个方法:

  • createBrowserHistory 适用于现代浏览器(支持h5 history API)

  • createHashHistory 适用于须要兼容老版本浏览器的状况

这两个方法就分别对应了两个组件:<BrowserRouter>和<HashRouter>,它俩返回的history对象拥有的属性是同样的,可是各自的实现不一样。

//createHashHistory.js
var HashChangeEvent = 'hashchange'; //hash值改变时会触发该事件
var createHashHistory = function createHashHistory() {
  var globalHistory = window.history; //全局的history对象
  var handleHashChange = function handleHashChange() {} //hash值变化时操做的方法
}
//createBrowserHistory.js
var PopStateEvent = 'popstate'; //监听url的变化事件
var HashChangeEvent = 'hashchange'; //依然监听了hash改变的事件,可是多加了一个判断是是否须要监听hash改变,若是不须要就不绑定该事件。
var createBrowserHistory = function createBrowserHistory() {
  var globalHistory = window.history; //全局的history对象
  var handlePop = function handlePop(location) {} //出栈操做
}

//createHashHistory.js,createBrowserHistory.js导出的history对象
const history = {
    length: globalHistory.length, //globalHistory就是window.history
    action: "POP", //操做历史状态都属于出栈操做
    location: initialLocation, //最重要的!!前面的Router.js源码向子组件单独传递了这个对象,由于路由匹配会用到它。
    createHref, //生成的url地址样式,若是是hash则加一个'#'
    push, //扩展history.pushState()方法
    replace, //扩展history.replaceState()方法
    go, //history.go()方法
    goBack, //history.back()方法
    goForward, //history.forward()方法
    block,
    listen
}
复制代码

咱们从控制台打印一下看看这个history:

因此,咱们直接用<BrowserRouter>与使用<Router>搭配createBrowserHistory()方法是同样的效果。

import {
    Router,
} from 'react-router-dom'
import createBrowserHistory from 'history/createBrowserHistory';

const history = createBrowserHistory();

const App = () => (
    <Router history={history}>
        <div>{/*其它*/}</div>
    </Router>
)

复制代码

就等于:

import {
    BrowserRouter,
} from 'react-router-dom'

const App = () => (
    <BrowserRouter>
        <div>{/*其它*/}</div>
    </BrowserRouter>
)

复制代码

<BrowserRouter>和<HashRouter>使用注意点

<HashRouter>生成的url路径看起来是这样的:

http://localhost:8080/#/user
复制代码

咱们知道hash值是不会传到服务器端的,因此使用hash记录状态不须要服务器端配合,可是<BrowserRouter>生成的路径是这样的:

http://localhost:8080/user
复制代码

这时候在此目录下刷新浏览器会从新向服务器发起请求,服务器端没有配置这个路径,因此会出现can't GET /user这种错误,而解决方法就是,修改devServer的配置(前面咱们配置了热替换,其实就是用webpack-dev-server搭了一个本地服务器): webpack.config.js

devServer: {
        publicPath: publicPath,
        contentBase: path.resolve(__dirname, 'build'),
        inline: true,
        hot: true,  
        historyApiFallback: true, //增长
    },
复制代码

例子

import React from 'react'
import { Router, Route, Link } from 'react-router'

const App = React.createClass({
  render() {
    return (
      <div>
        <h1>App</h1>
        <ul>
          <li><Link to="/about">About</Link></li>
          <li><Link to="/inbox">Inbox</Link></li>
        </ul>
        {this.props.children}
      </div>
    )
  }
})

const About = React.createClass({
  render() {
    return <h3>About</h3>
  }
})

const Inbox = React.createClass({
  render() {
    return (
      <div>
        <h2>Inbox</h2>
        {this.props.children || "Welcome to your Inbox"}
      </div>
    )
  }
})

const Message = React.createClass({
  render() {
    return <h3>Message {this.props.params.id}</h3>
  }
})

React.render((
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body)
复制代码

添加首页

想象一下当 URL 为 / 时,咱们想渲染一个在 App 中的组件。不过在此时,App 的 render 中的 this.props.children 仍是 undefined。这种状况咱们可使用 IndexRoute 来设置一个默认页面。

import { IndexRoute } from 'react-router'

const Dashboard = React.createClass({
  render() {
    return <div>Welcome to the app!</div>
  }
})

React.render((
  <Router>
    <Route path="/" component={App}>
      {/* 当 url 为/时渲染 Dashboard */}
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body)
复制代码

如今,App 的 render 中的 this.props.children 将会是 <Dashboard>这个元素。这个功能相似 Apache 的DirectoryIndex 以及 nginx的 index指令,上述功能都是在当请求的 URL 匹配某个目录时,容许你制定一个相似index.html的入口文件。

让 UI 从 URL 中解耦出来

若是咱们能够将 /inbox 从 /inbox/messages/:id 中去除,而且还可以让 Message 嵌套在 App -> Inbox 中渲染,那会很是赞。绝对路径可让咱们作到这一点。

React.render((
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        {/* 使用 /messages/:id 替换 messages/:id */}
        <Route path="/messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
), document.body)
复制代码

在多层嵌套路由中使用绝对路径的能力让咱们对 URL 拥有绝对的掌控。咱们无需在 URL 中添加更多的层级,从而可使用更简洁的 URL。

兼容旧的 URL

等一下,咱们刚刚改变了一个 URL! 这样很差。 如今任何人访问 /inbox/messages/5 都会看到一个错误页面。:(

不要担忧。咱们可使用 <Redirect> 使这个 URL 从新正常工做。

import { Redirect } from 'react-router'

React.render((
  <Router>
    <Route path="/" component={App}>
      <IndexRoute component={Dashboard} />
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="/messages/:id" component={Message} />

        {/* 跳转 /inbox/messages/:id 到 /messages/:id */}
        <Redirect from="messages/:id" to="/messages/:id" />
      </Route>
    </Route>
  </Router>
), document.body)
复制代码

如今当有人点击 /inbox/messages/5 这个连接,他们会被自动跳转到 /messages/5。 :raised_hands:

进入和离开的Hook

Route 能够定义 onEnter 和 onLeave 两个 hook ,这些hook会在页面跳转确认时触发一次。这些 hook 对于一些状况很是的有用,例如权限验证或者在路由跳转前将一些数据持久化保存起来。

在路由跳转过程当中,onLeave hook 会在全部将离开的路由中触发,从最下层的子路由开始直到最外层父路由结束。而后onEnter hook会从最外层的父路由开始直到最下层子路由结束。

继续咱们上面的例子,若是一个用户点击连接,从 /messages/5 跳转到 /about,下面是这些 hook 的执行顺序:

  • /messages/:id 的 onLeave

  • /inbox 的 onLeave

  • /about 的 onEnter

替换的配置方式

由于 route 通常被嵌套使用,因此使用 JSX 这种自然具备简洁嵌套型语法的结构来描述它们的关系很是方便。然而,若是你不想使用 JSX,也能够直接使用原生 route 数组对象。

上面咱们讨论的路由配置能够被写成下面这个样子:

const routeConfig = [
  { path: '/',
    component: App,
    indexRoute: { component: Dashboard },
    childRoutes: [
      { path: 'about', component: About },
      { path: 'inbox',
        component: Inbox,
        childRoutes: [
          { path: '/messages/:id', component: Message },
          { path: 'messages/:id',
            onEnter: function (nextState, replaceState) {
              replaceState(null, '/messages/' + nextState.params.id)
            }
          }
        ]
      }
    ]
  }
]

React.render(<Router routes={routeConfig} />, document.body)
复制代码

路由匹配原理

路由拥有三个属性来决定是否“匹配“一个 URL:

  • 嵌套关系

  • 路径语法

  • 优先级

嵌套关系

React Router 使用路由嵌套的概念来让你定义 view 的嵌套集合,当一个给定的 URL 被调用时,整个集合中(命中的部分)都会被渲染。嵌套路由被描述成一种树形结构。React Router 会深度优先遍历整个路由配置来寻找一个与给定的 URL 相匹配的路由。

路径语法

路由路径是匹配一个(或一部分)URL 的 一个字符串模式。大部分的路由路径均可以直接按照字面量理解,除了如下几个特殊的符号:

  • :paramName – 匹配一段位于 /、? 或 # 以后的 URL。 命中的部分将被做为一个参数
  • () – 在它内部的内容被认为是可选的
    • – 匹配任意字符(非贪婪的)直到命中下一个字符或者整个 URL 的末尾,并建立一个 splat 参数
<Route path="/hello/:name">         // 匹配 /hello/michael 和 /hello/ryan
<Route path="/hello(/:name)">       // 匹配 /hello, /hello/michael 和 /hello/ryan
<Route path="/files/*.*">           // 匹配 /files/hello.jpg 和 /files/path/to/hello.jpg
复制代码

若是一个路由使用了相对路径,那么完整的路径将由它的全部祖先节点的路径和自身指定的相对路径拼接而成。使用绝对路径可使路由匹配行为忽略嵌套关系。

优先级

最后,路由算法会根据定义的顺序自顶向下匹配路由。所以,当你拥有两个兄弟路由节点配置时,你必须确认前一个路由不会匹配后一个路由中的路径。例如,千万不要这么作:

<Route path="/comments" ... />
<Redirect from="/comments" ... />
复制代码
相关文章
相关标签/搜索