一文带你拿捏ReactRouter原理

前言

之前写了一篇关于路由中困扰点的文章,相信大家看完后,对于前端路由、后端路由,以及history和hash两种模式,都有了自己的理解并且也知道该如何分辨了。那这篇文章,就是为大家带来React-Router的原理解析。用最简单的代码来阐述React-Router中最核心的几个功能,废话不多说,我们开始吧! ​

浏览器API与组件的数据交互

正如上一篇文章中分析的那样,前端路由利用了浏览器自带的一些能力,主要为window.historypopstate的结合以及location.hashhashchange的结合。这两种方式,一边处理的是浏览器的url,一边是和事件的回调函数对接,既然我们是想要实现React-Router,就得在事件的回调函数上和react联动起来。 ​

我们再来分析下React-Router这样的组件,该如何与事件的回调进行交互。首先,我们的目的是当url变化时,页面进行对应的跳转。而每次url变化时,浏览器的popstate或者hashchange回调函数都会重新执行一遍,回调函数需要告诉React-Router当前的url已经变了,而React-Router在拿到新的url数据后,需要进行正确的更新。梳理到这一步,其实就很容易发现,React-Router只要把url数据存放在state上,就能够做到url数据更新后,能根据新的url数据进行更新。也就是说,我们在浏览器事件回调函数中需要做的事,就是更新React-Router中的state。 ​

最直观的解决方案,就是在React-Router中,我们自行去注册popstate事件或者hashchange事件,然后回调函数里更新组件的state,但是显然这样不够优雅。并且我们知道,手动pushState是不会触发popstate事件的,所以我们需要在手动pushState之后,再手动触发一下更新state的操作,并且手动pushState的操作,都是在跟底层的一些业务组件上,比如菜单栏,或者某个跳转的按钮,即更底层的组件需要有调用顶层组件方法的能力,所以我们很自然地想到了用context进行值传递。 回到React-Router里是否该写事件绑定的问题,因为选用history模式还是hash模式完全取决于使用者,并且除了事件绑定外,还需要处理history模式下通过代码跳转的情况,所以为了把功能划分清晰,已经有一个history的npm包来处理history相关的内容,官方的React-Router也是采用了这个包。 ​

这个包所做的事,一方面会根据用户需要的前端路由类型(history模式/hash模式)来绑定事件,并且会用到发布订阅模式,来管理回调函数,通过createBrowserHistory或者createHashHistory新建一个history对象后,可以调用history.listen进行事件的注册。以history模式为例,大致的流程图如下 image.png

hash模式也大同小异,确认了大致的数据交互后,我们来实现一版简易的React-Router,同样也是已history模式为例。

常用的组件

首先,我们先看一下平时在项目里应用React-Router,会需要用到哪些组件,以及我们在使用这些组件时,需要传递哪些参数

  • BroswerRouter
    • 这个是组件是我们能够实现路由跳转的前提,再实际使用中,采用history模式的话,我们会选择BroswerRouter组件,其核心就是Router组件
    • 通过这个名字不难看出,hash模式下对应的就是HashRouter,而BroswerRouter和HashRouter所做的事,就是将不同类型的history传递给Router组件
  • Router
    • 路由的核心,包括和history之间的数据交互,以及相关数据往下层的透传。
    • url变化后需要自动更新,并通知到各子组件
  • Route
    • 需要配置path,component/children/render等参数
    • 如果path与当前url数据符合,则渲染对应的component/children/render
  • Switch
    • 从Route的定义中可以发现,url数据与path不一定是一对一的关系,有可能某一url,对应多个Route,那么多个Route组件都是渲染对应的组件
    • Switch的功能就是,确保其子组件中,只有一个组件被匹配,多余的会被废弃掉
  • Redirect
    • 需要配置to参数
    • 当所有Route的path都与url不匹配,则会跳转到to所指的地址
  • Link
    • 用在导航栏,或者跳转按钮上,其内部就是调用了history对象上的push方法,做到url的更新,以及对应回调函数的执行,最终做到页面跳转

BroswerRouter

这个组件就是在Router上再套一个history的壳,所以其实现比较简单,我们直接写:

import { createBrowserHistory } from 'history'

class BroswerRouter extends React.Component{
  history=createBrowserHistory()
  render(){
    return <Router history={this.history} />
  }
}
复制代码

Router

这个组件是我们的核心,首先从BroswerRouter的实现可以看出,history会以props的形式传递给Router组件。再从我们上文分析到的,history的变化,需要改动到Router组件的state,从而触发react的rerender,所以我们需要建立一个state,上面存放的就是当前的url location,其初始值就是props.history传递进来的值(直接取用window上的url也可以,但因为已经用了history这个库,所以所有url、history相关数据及API,都用history抛出来的)。 另外就是为了做到state变化后,子组件都能感知到,且为了避免后续的重复调用,所以我们采用Context来做数据透传。 其简化版代码如下:

class Router extends React.Component{
  constructor(props){
    super(props)
    this.state={
      location:props.history.location
    }
  }
  
  componentDidMount() {
    const {history}=this.props
    history.listen(({location})=>{
      this.setState({
        location
      })
    })
  }
  
  render(){
    const {location}=this.state
    const {children,history}=this.props
    return <RouterContext.Provider value={
      {
        location,
        history
      }
    } children={children} />
  }
}
复制代码

Route

Route组价是路由模块中偏底层的组件,它的功能就是以path与url做对比,如果符合,则渲染对应组件。组件可以用component/children/render等方式表示,由于本文主要是介绍React-Router中数据流相关,所以统一用component来代表组件。如果大家感兴趣,其实源码中针对三种表达方式,有内置的优先级,children>component>render。源码中组件渲染相关代码如下:

children
? typeof children === "function"
  ? children(props)
  : children
: component
  ? React.createElement(component, props)
  : render
    ? render(props)
    : null
复制代码

Switch

在我个人的理解中,大多数情况下,我们都是希望一个url对应一个Route组件的,如果出现多个的情况,肯定会出现后续不好维护,以及排查问题容易出现遗漏等问题。而想要实现一对一的关系,如果想要在Route组件内部去实现,那么其势必需要获取到同级Route的信息,这显然是不合理的。 那直接集成在Router中呢?如果在Router中实现的话,万一真的需要一对多的场景,就无法满足了,所以React-Router就设计出了Switch组件,将一对一或者一对多的选择权,交给开发者。 在使用时,BroswerRouter、Switch以及Route的关系如下

<BroswerRouter>
  <Switch>
    <Route />
    <Route />
  </Switch>
</BroswerRouter>
复制代码

我们可以发现,在Switch组件中,可以通过props.children获取到所有的Route实例,我们就可以按顺序,当某个Route的path与url配对时,阻止后续其他Route的比较及渲染。 由上面的分析,不难得出如下的简易版代码:

class Switch extends React.Component{
  render(){
    return <RouterContext.Consumer>
      {context=>{
        const {children}=this.props
        let result=null
        result=children.find(child=>{
         return child.props.path===context.location.pathname
        })
        return result ? React.cloneElement(result) : null
      }}
    </RouterContext.Consumer>
    
  }
}
复制代码

Redirect

一般放在Route组件的最后面,表示当前面的Route组件都未配对时,跳转到Redirect中to所指的位置,结合上面对Switch的分析,如果Redirect被实例化,则说明上方所有的Route都未匹配到,所以其实现大致如下:

class LifeCycleHoc extends React.Component{
  
  componentDidMount() {
    this.props.onMount && this.props.onMount() 
  }
}
class Redirect extends React.Component{
  

  render(){
    const {to}=this.props
    return <RouterContext.Consumer>
      {
        context=>{
          return <LifeCycleHoc onMount={()=>{
            console.log(111)
              context.history.push(to)
            }} 
          />
        }
      }
    </RouterContext.Consumer>
  }
  
}
复制代码

Link

之前讲到的组件,都是路由架构层面的,而Link组件则是在调用层面,供开发者频繁调用的,结合上面分析的history和Router之间的数据交互,我们只需要从Router中拿到history对象,然后再调用history.push方法,即可完成页面的跳转,其实现如下

class Link extends React.Component{
  render(){
    const {to,children}=this.props
    return (
      <RouterContext.Consumer>
        {context=>{
          return <div onClick={()=>context.history.push(to)}>{children}</div>
        }}
      </RouterContext.Consumer>
    )
  }
}
复制代码

总结

至此,我们实现了React-Router中几个核心的组件,最后再用一张图来梳理下整个的架构 image.png 最后,再补上当用户点击某个导航栏后发生的事

image.png

完整实例代码

codesandbox.io/s/quirky-ch…

参考文章

juejin.cn/post/688629…

猜你喜欢

转载自juejin.im/post/7037067599120711687