React-Router-dom

1,313 阅读13分钟

开始
无论是使用Vue,还是React,开发的单页应用程序,可能只是该站点的一部分(某一个功能块)

一个单页应用里,可能会划分为多个页面(几乎完全不同的页面效果)(组件)
React Router
如果要在单页应用中完成组件的切换,需要实现下面两个功能:

  1. 根据不同的页面地址,展示不同的组件(核心)
  2. 完成无刷新的地址切换

我们把实现了以上两个功能的插件,称之为路由

  1. react-router:路由核心库,包含诸多和路由功能相关的核心代码
  2. react-router-dom:利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能

如果是在页面中实现路由,需要安装react-router-dom库

路由组件

Router组件

它本身不做任何展示,仅提供路由模式配置,另外,该组件会产生一个上下文,上下文中会提供一些实用的对象和方法,供其他相关组件使用

  1. HashRouter:该组件,使用hash模式匹配
  2. BrowserRouter:该组件,使用BrowserHistory模式匹配

通常情况下,Router组件只有一个,将该组件包裹整个页面

Route组件

根据不同的地址,展示不同的组件

重要属性:

  1. path:匹配的路径

    1. 默认情况下,不区分大小写,可以设置sensitive属性为true,来区分大小写
    2. 默认情况下,只匹配初始目录,如果要精确匹配,配置exact属性为true
    3. 如果不写path,则会匹配任意路径
  2. component:匹配成功后要显示的组件

  3. children:

    1. 传递React元素,无论是否匹配,一定会显示children,并且会忽略component属性
    2. 传递一个函数,该函数有多个参数,这些参数来自于上下文,该函数返回react元素,则一定会显示返回的元素,并且忽略component属性

Route组件可以写到任意的地方,只要保证它是Router组件的后代元素

Switch组件

写到Switch组件中的Route组件,当匹配到第一个Route后,会立即停止匹配

由于Switch组件会循环所有子元素,然后让每个子元素去完成匹配,若匹配到,则渲染对应的组件,然后停止循环。因此,不能在Switch的子元素中使用除Route外的其他组件。

Link

生成一个无刷新跳转的a元素

  • to

    • 字符串:跳转的目标地址

    • 对象:

      • pathname:url路径
      • search
      • hash
      • state:附加的状态信息
  • replace:bool,表示是否是替换当前地址,默认是false

  • innerRef:可以将内部的a元素的ref附着在传递的对象或函数参数上

    • 函数
    • ref对象

NavLink

是一种特殊的Link,Link组件具备的功能,它都有

它具备的额外功能是:根据当前地址和链接地址,来决定该链接的样式

  • activeClassName: 匹配时使用的类名
  • activeStyle: 匹配时使用的内联样式
  • exact: 是否精确匹配
  • sensitive:匹配时是否区分大小写
  • strict:是否严格匹配最后一个斜杠

Redirect

重定向组件,当加载到该组件时,会自动跳转(无刷新)到另外一个地址

  • to:跳转的地址

    • 字符串
    • 对象
  • push: 默认为false,表示跳转使用替换的方式,设置为true后,则使用push的方式跳转

  • from:当匹配到from地址规则时才进行跳转

  • exact: 是否精确匹配from

  • sensitive:from匹配时是否区分大小写

  • strict:from是否严格匹配最后一个斜杠

基础

一、路由配置

1.Route

 <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        <Route path="messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
URL组件
/App
/aboutApp -> About
/inboxApp -> Inbox
/inbox/messages/:idApp -> Inbox -> Message

2. 默认路由IndexRoute组件

这样写当我们的路由地址是/的时候APP没有子元素,我们可以这样做

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

添加 <IndexRoute component={Dashboard} /> 当路由是/的时候默认匹配到IndexRoutecomponent组件

3.UI 从 URL 中解耦

如果我们想 通过路由/messages/2 匹配 /inbox/messages/:id 我们可以这样配置

<Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="inbox" component={Inbox}>
        {/* 使用 /messages/:id 替换 messages/:id */}
        <Route path="/messages/:id" component={Message} />
      </Route>
    </Route>
  </Router>
URL组件
/messages/:idApp -> Inbox -> Message

messages/:id 前面加个 / 改成 /messages/:id 这样可以吧Message组件渲染到APP-Inbox下面 又可以通过 /messages/:id直接访问到, 原来需要/inbox/messages/:id

4.路由重定向Redirect组件

这样有个问题 当我们访问原来的/inbox/messages/:id 时就匹配不到了,我们可以这样做

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

添加一个<Redirect from="messages/:id" to="/messages/:id" />当访问路由/inbox/messages/:id的时候定位到/messages/:id

5.另外还有两个钩子 onEnter 和 onLeave

这些 hook 对于一些情况非常的有用,权限验证或者在路由跳转前将一些数据持久化保存起来
onEnter进入触发
onEnter离开出发
继续我们上面的例子,如果一个用户点击链接,从 /messages/5 跳转到 /about,下面是这些 hook 的执行顺序:

  • /messages/:id 的 onLeave
  • /inbox 的 onLeave
  • /about 的 onEnter

5.我们也可以使用vue-router的方式配置

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地址组成

例:www.react.com:443/news/1-2-1.…

  1. 协议名(schema):https

  2. 主机名(host):www.react.com

    1. ip地址
    2. 预设值:localhost
    3. 域名
    4. 局域网中电脑名称
  3. 端口号(port):443

    1. 如果协议是http,端口号是80,则可以省略端口号
    2. 如果协议是https,端口号是443,则可以省略端口号
  4. 路径(path):/news/1-2-1.html

  5. 地址参数(search、query):?a=1&b=2

    1. 附带的数据
    2. 格式:属性名=属性值&属性名=属性值....
  6. 哈希(hash、锚点)

    1. 附带的数据 路由拥有三个属性来决定是否“匹配“一个 URL:
  • 1.嵌套关系
  • 2.路径法则
  • 3.优先级

1.嵌套关系

定义Route和html页面一样div嵌套div每个元素上有个path属性 根据url匹配path 如果匹配成功就显示对应的component,react-router内部使用的是proper-url-join插件进行判断的, 感兴趣的可以了解一下

2.路径语法

    1. :name : 动态匹配
    1. (name): 可选的 也可以(:name)动态可选
    1. : 匹配任意字符
    1. 正则约定 例子:
<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

如果使用相对路径 那么url是祖先的path和自己的path拼接而成, 如果使用绝对路径可以忽略嵌套关系。

3.优先级

从外到内,从浅到深
你必须确认前一个路由不会匹配后一个路由中的路径

4.正则约定

image.png

image.png

三、Histories

React Router建立在history之上。 history知道如何监听地址栏的变化,并解析这个url中的地址,然后router使用它去匹配路由,最后把组件渲染到页面。

1. 常用的三种history

    1. browserHistory
    1. hashHistory
    1. createMemoryHistory 使用es6的方式引入
import { browserHistory } from 'react-router'

<Router>中的history属性中使用他们

<Router history={browserHistory} routes={routes}

2. browserHistory

使用浏览器History的方式匹配路由, HTML5出现后,新增了History Api,从此以后,浏览器拥有了改变路径而不刷新页面的方式

History表示浏览器的历史记录,它使用栈的方式存储。

  1. history.length:获取栈中数据量

  2. history.pushState:向当前历史记录栈中加入一条新的记录

    1. 参数1:附加的数据,自定义的数据,可以是任何类型
    2. 参数2:页面标题,目前大部分浏览器不支持
    3. 参数3:新的地址
  3. history.replaceState:将当前指针指向的历史记录,替换为某个记录

    1. 参数1:附加的数据,自定义的数据,可以是任何类型
    2. 参数2:页面标题,目前大部分浏览器不支持
    3. 参数3:新的地址 example.com/some/path这样真实的 URL 。 但是刷新页面会丢失需要服务器配合使用
// 在你应用 JavaScript 文件中包含了一个 script 标签
// 的 index.html 中处理任何一个 route
app.get('*', function (request, response){
  response.sendFile(path.resolve(__dirname, 'public', 'index.html'))
})

3.hashHistory

使用 URL 中的 hash(#)部分去创建形如 example.com/#/some/path 的路由。 我们刚开始学习可以使用这种方式, 上线最好使用browserHistory,因为它更像真实的路由

3.createMemoryHistory

Memory history 不会在地址栏被操作或读取,通常用于服务器渲染,测试。另外和上面两种不同的是我们需要创建他

const history = createMemoryHistory(location)

history的实例应用。

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

import App from '../components/App'
import Home from '../components/Home'
import About from '../components/About'
import Features from '../components/Features'

render(
  <Router history={browserHistory}>
    <Route path='/' component={App}>
      <IndexRoute component={Home} />
      <Route path='about' component={About} />
      <Route path='features' component={Features} />
    </Route>
  </Router>,
  document.getElementById('app')

4.路由信息

history

它并不是window.history对象,我们利用该对象无刷新跳转地址

为什么没有直接使用history对象

  1. React-Router中有两种模式:Hash、History,如果直接使用window.history,只能支持一种模式
  2. 当使用windows.history.pushState方法时,没有办法收到任何通知,将导致React无法知晓地址发生了变化,结果导致无法重新渲染组件
  • push:将某个新的地址入栈(历史记录栈)

    • 参数1:新的地址
    • 参数2:可选,附带的状态数据
  • replace:将某个新的地址替换掉当前栈中的地址

  • go: 与window.history一致

  • forward: 与window.history一致

  • back: 与window.history一致

location

与history.location完全一致,是同一个对象,但是,与window.location不同

location对象中记录了当前地址的相关信息

我们通常使用第三方库query-string,用于解析地址栏中的数据.

match

该对象中保存了,路由匹配的相关信息

  • isExact:事实上,当前的路径和路由配置的路径是否是精确匹配的
  • params:获取路径规则中对应的数据

实际上,在书写Route组件的path属性时,可以书写一个string pattern(字符串正则)

react-router使用了第三方库:Path-to-RegExp,该库的作用是,将一个字符串正则转换成一个真正的正则表达式。

向某个页面传递数据的方式:

  1. 使用state:在push页面时,加入state
  2. 利用search:把数据填写到地址栏中的?后
  3. 利用hash:把数据填写到hash后
  4. params:把数据填写到路径中

非路由组件获取路由信息

某些组件,并没有直接放到Route中,而是嵌套在其他普通组件中,因此,它的props中没有路由信息,如果这些组件需要获取到路由信息,可以使用下面两种方式:

  1. 将路由信息从父组件一层一层传递到子组件
  2. 使用react-router提供的高阶组件withRouter,包装要使用的组件,该高阶组件会返回一个新组件,新组件将向提供的组件注入路由信息。

高级

1. 动态路由

在做大型应用中我们为了优化项目通常吧大文件才分成小文件,避免冲突, 并且对文件配置按需加载,以拆分代码的方式优化项目。 于react代码分割配合使用。

另外一种情况比如编制页面,我们需要通过id进入对应的编制页面, 还有分享出去的链接/user/3当使用浏览器打卡地址时希望看到id为3的详情页。

React Router 里的路径匹配以及组件加载都是异步完成的,不仅允许你延迟加载组件,并且可以延迟加载路由配置。在首次加载包中你只需要有一个路径定义,路由会自动解析剩下的路径。

Route 可以定义 getChildRoutesgetIndexRoute 和 getComponents 这几个函数。它们都是异步执行,并且只有在需要时才被调用。我们将这种方式称之为 “逐渐匹配”。 React Router 会逐渐的匹配 URL 并只加载该 URL 对应页面所需的路径配置和组件。

配合 webpack 这类的代码分拆工具使用的话,一个原本繁琐的构架就会变得更简洁明了。

const CourseRoute = {
  path: 'course/:courseId',

  getChildRoutes(location, callback) {
    require.ensure([], function (require) {
      callback(null, [
        require('./routes/Announcements'),
        require('./routes/Assignments'),
        require('./routes/Grades'),
      ])
    })
  },

  getIndexRoute(location, callback) {
    require.ensure([], function (require) {
      callback(null, require('./components/Index'))
    })
  },

  getComponents(location, callback) {
    require.ensure([], function (require) {
      callback(null, require('./components/Course'))
    })
  }
}

2. 跳转前确认

提供一个 routerWillLeave 生命周期钩子,这使得 React 组件可以拦截正在发生的跳转,或在离开 route 前提示用户。routerWillLeave 返回值有以下两种:

  1. return false 取消此次跳转
  2. return 返回提示信息,在离开 route 前提示用户进行确认。
const NestedForm = React.createClass({

  // 后代组件使用 Lifecycle mixin 获得
  // 一个 routerWillLeave 的方法。
  mixins: [ Lifecycle ],

  routerWillLeave(nextLocation) {
    if (!this.state.isSaved)
      return 'Your work is not saved! Are you sure you want to leave?'
  },

  // ...

})

3.服务端渲染

服务端渲染与客户端渲染有些许不同,因为你需要:

  • 发生错误时发送一个 500 的响应
  • 需要重定向时发送一个 30x 的响应
  • 在渲染之前获得数据 (用 router 帮你完成这点)

为了迎合这一需求,你要在 <Router> API 下一层使用:

  • 使用 match 在渲染之前根据 location 匹配 route
  • 使用 RoutingContext 同步渲染 route 组件

它看起来像一个虚拟的 JavaScript 服务器:

import { renderToString } from 'react-dom/server'
import { match, RoutingContext } from 'react-router'
import routes from './routes'

serve((req, res) => {
  // 注意!这里的 req.url 应该是从初始请求中获得的
  // 完整的 URL 路径,包括查询字符串。
  match({ routes, location: req.url }, (error, redirectLocation, renderProps) => {
    if (error) {
      res.send(500, error.message)
    } else if (redirectLocation) {
      res.redirect(302, redirectLocation.pathname + redirectLocation.search)
    } else if (renderProps) {
      res.send(200, renderToString(<RoutingContext {...renderProps} />))
    } else {
      res.send(404, 'Not found')
    }
  })
})

至于加载数据,你可以用 renderProps 去构建任何你想要的形式——例如在 route 组件中添加一个静态的 load 方法,或如在 route 中添加数据加载的方法——由你决定。

4. 组件生命周期

在开发应用时,理解路由组件的生命周期是非常重要的。 后面我们会以获取数据这个最常见的场景为例,介绍一下路由改变时,路由组件生命周期的变化情况。

路由组件的生命周期和 React 组件相比并没有什么不同。 所以让我们先忽略路由部分,只考虑在不同 URL 下,这些组件是如何被渲染的。

路由配置如下:

<Route path="/" component={App}>
  <IndexRoute component={Home}/>
  <Route path="invoices/:invoiceId" component={Invoice}/>
  <Route path="accounts/:accountId" component={Account}/>
</Route>

路由切换时,组件生命周期的变化情况

1. 当用户打开应用的 '/' 页面

组件生命周期
AppcomponentDidMount
HomecomponentDidMount
InvoiceN/A
AccountN/A

2. 当用户从 '/' 跳转到 '/invoice/123'

组件生命周期
AppcomponentWillReceivePropscomponentDidUpdate
HomecomponentWillUnmount
InvoicecomponentDidMount
AccountN/A
  • App 从 router 中接收到新的 props(例如 childrenparamslocation 等数据), 所以 App 触发了 componentWillReceiveProps 和 componentDidUpdate 两个生命周期方法
  • Home 不再被渲染,所以它将被移除
  • Invoice 首次被挂载

3. 当用户从 /invoice/123 跳转到 /invoice/789

组件生命周期
AppcomponentWillReceiveProps, componentDidUpdate
HomeN/A
InvoicecomponentWillReceiveProps, componentDidUpdate
AccountN/A

所有的组件之前都已经被挂载, 所以只是从 router 更新了 props.

** 4. 当从 /invoice/789 跳转到 /accounts/123**

组件生命周期
AppcomponentWillReceiveProps, componentDidUpdate
HomeN/A
InvoicecomponentWillUnmount
AccountcomponentDidMount

获取数据

虽然还有其他通过 router 获取数据的方法, 但是最简单的方法是通过组件生命周期 Hook 来实现。 前面我们已经理解了当路由改变时组件生命周期的变化, 我们可以在 Invoice 组件里实现一个简单的数据获取功能。

let Invoice = React.createClass({

  getInitialState () {
    return {
      invoice: null
    }
  },

  componentDidMount () {
    // 上面的步骤2,在此初始化数据
    this.fetchInvoice()
  },

  componentDidUpdate (prevProps) {
    // 上面步骤3,通过参数更新数据
    let oldId = prevProps.params.invoiceId
    let newId = this.props.params.invoiceId
    if (newId !== oldId)
      this.fetchInvoice()
  },

  componentWillUnmount () {
    // 上面步骤四,在组件移除前忽略正在进行中的请求
    this.ignoreLastFetch = true
  },

  fetchInvoice () {
    let url = `/api/invoices/${this.props.params.invoiceId}`
    this.request = fetch(url, (err, data) => {
      if (!this.ignoreLastFetch)
        this.setState({ invoice: data.invoice })
    })
  },

  render () {
    return <InvoiceView invoice={this.state.invoice}/>
  }

})

5.导航守卫

导航守卫:当离开一个页面,进入另一个页面时,触发的事件

1.route属性: onEnter进入触发 onLeave离开出发

2.history对象

返回卸载监听回调
history.listen((location,action) =>{ 
if(this.props.onChange){ 
    const prevLocation = this.props.location;
    this.props.onChange(location,action,prevLocation) 
    }
  })
  • listen: 添加一个监听器,监听地址的变化,当地址发生变化时,会调用传递的函数

    • 参数:函数,运行时间点:发生在即将跳转到新页面时

      • 参数1:location对象,记录当前的地址信息

      • 参数2:action,一个字符串,表示进入该地址的方式

        • POP:出栈

          • 通过点击浏览器后退、前进
          • 调用history.go
          • 调用history.goBack
          • 调用history.goForward
        • PUSH:入栈

          • history.push
        • REPLACE:替换

          • history.replace
    • 返回结果:函数,可以调用该函数取消监听

  • block:设置一个阻塞,并同时设置阻塞消息,当页面发生跳转时,会进入阻塞,并将阻塞消息传递到路由根组件的getUserConfirmation方法。

    • 返回一个回调函数,用于取消阻塞器

3.路由根组件

getUserConfirmation

  • 参数:函数

    • 参数1:阻塞消息

      • 字符串消息

      • 函数,函数的返回结果是一个字符串,用于表示阻塞消息

        • 参数1:location对象
        • 参数2:action值
    • 参数2:回调函数,调用该函数并传递true,则表示进入到新页面,否则,不做任何操作

image.png

image.png 另外还有一篇详细的react-router-dom