react-router 究竟怎么玩儿?

啥是 router ?

router 是路由啊! 路由又是啥? 路由器? 在前端领域, 路由是用来保持UI界面与Url地址映射一致的工具。

Router 是 react-router 中的一个重要组件。所有的路由路径与组件的映射规则都应该被放在 Router 里面。

Route

说 Router 之前, 先说说 Route。

Route 是用于声明路由路径组件之间映射关系组件

对,没错, Route 是一个高阶组件, 但是它本身不做任何展示, 渲染的是传入的 component 组件。

使用 Route 的时候一般是这样的:

<Route path='/home' component={Home}></Route>

传入的两个属性(path,component)确定了一个组件和一个路径的映射规则。

Router 将会根据这个规则,监听页面url的变化,每一次变化进行一次上述规则的匹配,path 匹配成功则展示相应的 component

就这么回事儿。

如果不明确在 Route 标签上标注 exact,匹配规则将会是模糊匹配

  • 例如有两个路由:/a, /a/b 分别对应了A,B两个组件。如果此时路由的地址是/a/b, 本来你是只想显示B,但是A也会跟着跳出来。

  • 如果给 Route 组件里面写了 React 元素 ,path属性将失去作用,component自然也失效,React 元素一定会被渲染出来。

<Route path='/home' component={Home}>
  <span>一定会渲染</span>
</Route>
  • 既然能传递 React 元素,那自然也可以是一个函数返回一个React元素咯。
<Route path='/home' component={Home}>
  {() => <p>一定会渲染</p>}
</Route>

Router

  • Router 会生成一个路由上下文。这个上下文对象里面有(history,location, match)等对象,Router 一直监听着路由的变化,每一次变化的详细信息就存放在这个对象里面。
  • Router 一般会把整个页面包裹起来,因为它要为全局的提供上述上下文。
  • Router 相关的子组件能够拿到这个上下文对象。某个子组件如果匹配上路径, 在这个组件的props里面, 就会有上下文里面的对象。
<Route path='/home' component={Home}></Route>

还是这行熟悉的代码(一个例子用一年系列)。


下面就来慢慢看:

history

  • action: 表示路由是以什么样的方式改变的。只有三种,POPPUSHREPLACE。是否想起了栈?浏览器的历史记录栈啊。
  • listen:监听页面路径变化的函数。
  • location:下面说。
  • push:代码跳转路由方法。参数可选。
  this.props.history.push('/home', {a: 1, b: 'abc'})
// 路由跳转理所当然可以传递参数
// 在Home里面, 使用 this.props.history.location.state 获取
// 注意上下文中有一个 location 对象, 二者是一样的 this.props.location.state。但是与 window.location 不一样。
  • replace:同push。区别是 replace 将当前页面地址替换为一个新的地址(当前地址在栈顶,替换掉栈顶的地址)。push 是将一个新的地址入栈。浏览器表现上看就是 replace 之后, 不能回退到之前的页面, push可以。

location

顾名思义,定位。当前页面的地址信息。与history中的location是同一东西。

  • pathname: 当前页面匹配上的路径名。
  • search: url 携带的 ?后面的参数。
  • hash: url 携带的 # 后面的参数。
  • state: 使用 push, replace 方法传递的参数。
    可以使用 query-string 这个第三方库解析 search, hash
import qs from 'query-string';
...
let query = qs.parse(this.props.location.search)
let hash = qs.parse(this.props.location.hash)
...

match

当前路由的匹配信息

  • isExact: 不是上面说的 Route 上的 exact。它表示的是当前这个路由是否是由精确匹配匹配上的。(事实上的精确匹配)
  • params: 动态路由上携带的参数
// 将参数写入路径中,以 news/2020/04/21 的方式传递。
// 然后 react-router 使用类似正则的方式获取数据。
// 正则的规则(string-pattern)定义在path中
<Route path='/news/:year/:month/:day' component={News}></Route>

在News 组件中, 看看props:



注意到 params 了嘛?
其实格式不是固定的

<Route path='/news-:year-:month-:day' component={News}></Route>

这样也是可以的(尽情开脑洞吧), 只是一个约定而已, 怎么约定就怎么传。

<Route path='/news/:year?/:month?/:day(\d+)/*' component={News}></Route>

:year?:冒号后面表示变量, 如果再加上一个问号, 表明这个参数是可选的。 即使规定要传,实际没有传,不会报错,但是匹配会失败。
:day(\d+): 对字段 day 进行正则约束,需要是一个数字, 如果不是数字, 匹配会失败。
/*: 必须要传点东西, 任意东西都行, 就是不能不传。
react-router 中依赖了path-to-RegExp 这个第三方库来完成上述匹配。

  • path: 当前页面是用哪一个规则匹配上的。
  • url: 当前页面被真实匹配上的路径。

Switch

Switch 也是 react-router 中的一个组件, 它提供一个容器, 里面放很多的 Route,当路由发生变化的时候,匹配这些 Route 。但是如果匹配上多个路由(模糊匹配,或者多个同路径Route),只有匹配上的第一个会生效。

原理:当浏览器url发生改变, 循环所有的子组件(Route),匹配子组件的path属性,渲染第一匹配上的 Route 的 component。所以,当Switch 里面如果不是 Route,报错就理所应当了吧。

跨组件的路由信息 - withRouter

现在有两个页面,第一个是 home, 第二个是news。
在home 里面嵌套了另外一个组件A, 我希望在A中使用一个按钮去看新闻。

export default function Home(props) {
  console.log(props)
  return (
    <div> Home
      <button onClick={() => {
        props.history.push('/news')
      }}>去News</button>
      <hr/>
      <A></A>
    </div>
  )
}
function A (props) {
  return <div>
    <button onClick={() => {
      props.history.push('/news')
    }}>去看新闻</button>
  </div>
}

这样跳转肯定会出错。因为我们知道在React中, 组件的props属性全是来自父组件的传递: <A a='1' b={a:1, c: 'str'}></A>。可是A啥都没有。
于是:

function Home(props){
 return {
   ...
   <A {...props}></A>
   ...
 }
}
Home 组件的 props 里面是含有路由信息的,
所以可以考虑直接将Home组件的props也给A组件,
但是如果组件嵌套层级比较深的话,那就太快乐了😄。

使用 withRouter:

import {withRouter} from 'react-router-dom';
A = withRouter(A)
// 使用A

withRouter 原理:

withRouter = Component => <Route component={Component}></Route>

A 组件不是不能拿到路由信息嘛?为什么不能拿到? 因为A组件没有被放在 Route 的 component 里面。那我给你放进去就完了😂。

Link

原生 dom 里面的a标签跳转地址是需要刷新页面的, 不信去试试吧。
Link 用于生成一个无刷新跳转页面的 a 元素。

class Link extends Component {
  render() {
    return (
      <a href="javascript: void(0);"
        onClick={() => {
          e.preventDefault();
          this.props.history.push(this.props.to)
        }}
      >{this.props.children}</a>
    )
  }
}
export default withRouter(Link)
// 别忘了withRouter 包一下,否则... 😁

Link:

  • to 要跳转的地址
// 使用对象的方式跳转
<Link to={{pathname: '/home', search: {}, hash: {}, state: {}}}>Home</Link>

// 直接使用字符串跳转
<Link to='/home'>Home</Link>

// 可以传递 ref
<Link to='/home' innerRef={ref => console.log(ref)}>Home</Link>

// 使用 replace 的方式跳转, 默认为 push
<Link replace to='/home'>Home</Link>

NavLink

Link 的升级版。
浏览器的地址,NavLink 实体a元素的 href 属性,这两个东西对应上的时候,会给a标签加上一个 active 属性。表示a被点击激活了。

// 自定义 active 类名
<NavLink to={{pathname: '/home'}} activeClassName='_active'>Home</NavLink>

Redirect

跳转到指定页面。默认使用 replace 的方式跳转。

  • from: 如果匹配到 from 的地址, 进行重定向到 to 的地址。

嵌套的路由

function A (){
  return (
    <div>这是一条新闻~</div>
  )
}

function B (){
  return (
    <div>这是一条新闻评论~</div>
  )
}

export default function Page(props) {
  return (
    <div>
      <Route path={`${props.match.url}/news_detail`} component={A}></Route>
      <Route path={`${props.match.url}/news_comments`} component={B}></Route>
      <NavLink to={`${props.match.url}/news_detail`}>看新闻</NavLink>
      <NavLink to={`${props.match.url}/news_comments`}>看新闻</NavLink>
    </div>
  )
}

在根组件中套了一层Router,可以在任意页面写 Route.
<Route path='/news/news_detail' component={A}></Route>
这个东西写在什么地方,只要path属性匹配上了,就在对应的位置渲染component。嵌套路由就是这么实现的。

受保护的路由

import React from 'react'
import { Route, Redirect } from 'react-router-dom'

export default function ProtectRoute({ component: Component, children, render, ...rest }) {
  return (
    <Route
      {...rest}
      render={context => {
        if (isLogin) { // 鉴权逻辑, 此处为示例
          return <Component></Component>
        } else {
          return <Redirect to={{
            pathname: '/login',
            state: context.location.pathname
          }} />
        }
      }}
    >
    </Route>
  )
}
// use
<ProtectRoute path="/user" component={User}></ProtectRoute>

这里返回了一个Route组件, 前面没有提到的是, Route 组件可以传递一个render 属性, 这个属性接收一个函数, 函数默认参数是路由上下文对象。在 ProtectRoute 的参数里, 解构出了Component, children,render, 因为这三个参数会影响Route的表现行为, 这种表现行为不是此处场景需要的(它们会直接渲染,就没办法处理业务逻辑)。其他的参数通过对象展开运算符收纳在rest中,rest中的参数是可以直接作为参数再传递给Route的。
如果鉴权失败,返回一个重定向的路由去登录页, 同时携带上当前访问被拦截的pathname,等登陆完成之后, 可以通过这个pathname再跳转回来。简化用户操作。

导航守卫

vuebeforeRouteEnter 香不香?那肯定香的呀,放心,react 里面也....没有...但是.....可以自己实现😂。

history 对象 中有一个 listen 函数, 这个之前只是一提,没有深入细说, 就在这里等着呢。

函数默认接收两个参数:

  1. location 当前路由的定位信息
  2. action 当前监听到的路由变化以何种方式触发。POP,PUSH,REPLACE。
class RouteGuard extends Component {
  componentDidMount(){
    this.props.history.listen((location, action) => {
      console.log(action, location.pathname)
    })
  }
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}
export default withRouter(RouteGuard)

function App() {
  return (
    <Router>
      <RouteGuard>
        <Route path='/home' component={Home}></Route>
        <Route path='/news' component={News}></Route>
      </RouteGuard>
    </Router> 
  );
}


每一次路由的变化都会被history.listen监听到。当然, RouteGuard 的位置也很关键。
此时还没有什么实际用处, 仅仅是能够看到路由跳转确实经过了这里,这个卡还没设起来。

function App() {
  return (
    <Router
      getUserConfirmation={(msg, callback) => {
        // do something
        callback(true)
      }}
    >
      <RouteGuard
        onUrlChange={(location, action, unListen) => {
          // do something
        }}
      >
        <Route path='/home' component={Home}></Route>
        <Route path='/news' component={News}></Route>
      </RouteGuard>
    </Router>
  );
}

class RouteGuard extends Component {
  componentDidMount(){
    this.props.history.listen((location, action) => {
      console.log(action, location.pathname)
      this.props.onUrlChange && this.props.onUrlChange(location, action)
    })
    this.props.history.block('真的要跳转吗?')
  }
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    )
  }
}

Router下的任意能接收到路由信息的组件中, 设置阻塞(此阻塞只能设置一个)(history.block('msg'))之后,才能真正实现守卫的功能,在Router中有一个属性:getUserConfirmation,这个属性需要配置一个函数,函数接收两个参数,msgcallbackmsg 就是阻塞处传递的字符串,callback 则决定了此次路由跳转请求是否跳转。在getUserConfirmation 函数中进行业务逻辑处理(比如鉴权)之后,使用callback(true or false)

现在, 写了一个守卫组件, 组件中将当前路由跳转信息抛出, 在Router 中去处理守卫逻辑。当路径发生改变的时候, 执行onUrlChange 函数,比如打个日志啥的。然后会在 Router 中的 getUserConfirmation 看是否允许此次跳转。但是这样好像有点不满足,有点麻烦。

  • 在增强 RouteGuard 之前, 这里补充两个概念
    1. history.listen: 用来监听路由变化, 每次变化都会触发这个函数。
    2. history.block:每次路由变化设置阻塞, 需要通过getUserConfirmation 决定是否放行。

增强 RouteGuard

import React, { Component } from 'react'
import { BrowserRouter as Router, withRouter } from 'react-router-dom'

let prevLocation, nextLocation, action, unBlock;

class RouteGuard extends Component {

  handleComfirm = (msg, callback) => {
    this.props.onUrlChange ?  
    this.props.onUrlChange(prevLocation, nextLocation, action, unBlock, callback) : callback(true)
  }

  render() {
    return (
      <Router
        getUserConfirmation={this.handleComfirm}
      >
        <GuardHelper />
        {this.props.children}
      </Router>
    )
  }
}

class GuardHelper extends Component {
  componentDidMount() {
    this.unListen = this.props.history.listen((location, action) => {
      console.log(action, location.pathname, this.unListen)
    })
    unBlock = this.props.history.block((location, ac) => {
      prevLocation = this.props.location
      nextLocation = location
      action = ac
      return ''
    })
  }
  render() {
    return null
  }
}

GuardHelper = withRouter(GuardHelper)
export default RouteGuard

首先, RouteGuard 返回的是一个 Router 组件, 依然只能是在 Router中处理这个事件, 但是我们将这个事件交给 RouteGuard 上传递进来的自定义事件 onUrlChange

这个自定义事件我们希望给他传递prevLocation(由那个页面跳转的页面信息), nextLocation(要去哪个页面的页面信息), action(以何种方式跳转), unBlock(取消阻塞), callback(允许跳转与否) 这五个参数。所以这里要考虑一个问题:RouteGuard 现在是根组件,路由信息从哪里来?

我们知道只有 Router 下的 Routecomponent 会默认得到路由信息。所以这里创建了一个 新的组件 : GuardHelper。将这个组件使用 withRouter 包装一下, 这样他就能够接收路由信息,但是这还不够, 还没有人传递给它,我们还需要把它放在 Router 下。

这样, 就可以顺利在 GuardHelper 中拿到路由信息。但是处理阻塞的 handleConfirm 函数在根组件 RouteGuard 里面,是这个函数需要路由信息。于是我们创建了几个全局变量, 不用担心,这个全局变量只在这个模块里面生效。GuardHelper 这个组件 componentDidMount 的时候,调用 history.listen 监听路由变化,history.block 设置阻塞,并且将路由信息保存在全局变量中, 这样RouteGuard 就能拿到了。

注意:(history.block 和 getUserConfirmation)必须是成对出现,依靠设置的阻塞, 才能通过 getUserConfirmation处理这个阻塞。

// 现在这样使用
function App() {
  return (
    <RouteGuard
      onUrlChange={(prevLocation, nextLocation, action, unBlock, commit) => {
        // do something
        console.log(prevLocation.pathname, nextLocation.pathname, action)
        commit(false)
      }}
    >
      <Route path='/home' component={Home}></Route>
      <Route path='/news' component={News}></Route>
    </RouteGuard>
  );
}

(不作为教程,前端知识冗杂,仅供自己备忘)

全部评论

相关推荐

头像
点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务