react-router 源码浅析

3,240 阅读5分钟

用 react-router 也用了比较久了,对他的内部工作方式却只是了解皮毛,而且大部分还是通过别人的博客。最近两周打算自己探究一下他的实现。 注意!因为我只使用过 v3 版本的 react-router,因为对他的使用方式比较熟悉,所以这次解析也是基于这个版本。

文章目录:

  • react-router 工作模式简化流程
  • 内部具体实现
  • Link 组件的实现方式 以及 *History.push 的实现方式
  • 为什么要这样实现?有什么别的方式?做个比较?

react-router 工作模式简化流程

聊到这个话题就离不开前端路由。关于前端路由的一些演变过程和现有的方式可以看这篇文章。前端路由的重点就是不刷新页面,现有的解决方案有 hashChange 和 popState 两种。 React 提供API也是围绕这两种方式。 共同点都是发布订阅的模式,让浏览器事件触发的时候自己添加的 listener 被调用。Router 组件包裹着 Route 组件,Route 组件负责描述每条路由的展示组件和匹配的路径。这样 Router 组件实际上会格式化出一个映射的路由表
然而这是在页面路由更新的时候,最开始进入页面的时候怎么办呢?其实刚进入页面的时候也会进行一次匹配,详细分析见下一部分。

内部具体实现

首先解答上面的遗留问题,刚进入页面的状态如何带入?这个问题我们可以和 "Router 组件是在什么时候添加的事件监听"放在一起解答。

  componentWillMount() {//来源:modules/Router.js
    this.transitionManager = this.createTransitionManager()
    this.router = this.createRouterObject(this.state)

    this._unlisten = this.transitionManager.listen((error, state) => {
      if (error) {
        this.handleError(error)
      } else {
        // Keep the identity of this.router because of a caveat in ContextUtils:
        // they only work if the object identity is preserved.
        assignRouterState(this.router, state)
        this.setState(state, this.props.onUpdate)
      }
    })
  },

Router 组件在 willMount 生命周期添加了 listener,而添加 listener 本身就会触发一次匹配路由展示的过程。匹配的过程有 match 方法,用于各种嵌套路由的匹配。
但是注意,如果使用的是 browserHistory,这种路由方式一般是/a/b 这种方式,可能需要后端同学的配合。

封装 history ——transitionManager

在上面的代码中,我们会注意到添加监听器的 listen方法来自于 transitionManager 这个生成之后被赋值到 this.router 实例的属性。实际上 react-router 的事件监听过程是用 transitionManager 套了 history 这个库,抹平各种前端路由方式的调用差异。history库本身暴露了一些API 比如监听、取消监听、跳转等一系列方法。有基于咱们刚才提到的 hash 和 state 两种方式。我们传给 Router 组件 history 属性的值其实就是他的实例。(拿hashHistory 举栗,下面的文件是 reate-router export 的 hashHistory 的来源,也就是我们用的 hashHistory 的来源)。

//来源:modules/hashHistory.js
import createHashHistory from 'history/lib/createHashHistory'
import createRouterHistory from './createRouterHistory'
export default createRouterHistory(createHashHistory)

而 transitionManager 做的事情是针对当前的 router 实例和开发者指定的 history 对象,对 history 库给的 API 做一次二次封装,加上修改路由状态等等操作。然后开发者拿着 transitionManager 封装之后暴露出的 listen 等方法操作路由。

  createTransitionManager() {//来源:modules/Router.js
    const { matchContext } = this.props
    if (matchContext) {
      return matchContext.transitionManager
    }

    const { history } = this.props
    const { routes, children } = this.props

    invariant(
      history.getCurrentLocation,
      'You have provided a history object created with history v4.x or v2.x ' +
        'and earlier. This version of React Router is only compatible with v3 ' +
        'history objects. Please change to history v3.x.'
    )

    return createTransitionManager(//注意这个createTransitionManager才是
      history,
      createRoutes(routes || children)
    )
  },
渲染部分

渲染过程不是放在 Route 组件中负责渲染,而是把状态都放在 Router 中保存,详细可见第一部分的代码添加 listener 的部分。

     assignRouterState(this.router, state)
     this.setState(state, this.props.onUpdate)

而 Router 组件的 render 是这样写的:

    const { location, routes, params, components } = this.state
    const { createElement, render, ...props } = this.props

    return render({
      ...props,
      router: this.router,
      location,
      routes,
      params,
      components,
      createElement
    })

而 props 的值是当前 Router 组件的状态,他现在要展示的组件,对应的地址,当前跳转携带的参数 params 等等。下面是调用 render 的部分。

  return <RouterContext {...props} />

RouterContext 包装组件的主要作用就是把 props参数中存有当前路由状态的对象router存到全局。类似于 Redux 的 Provider 组件。

Link 组件的实现方式

这里小伙伴们可以猜测一下,Link是怎么做的呢?
我们知道 Link最后渲染完是个 a 标签,我们通常会给 Link 组件几个参数,最常用的是跳转的路由地址和携带的参数。通过上面的讲解不难猜出,Link 在点击的时候应该是调用了一个跳转的操作(八成也是 history 库里给的),然后禁止掉默认跳转就行了。
事实也是如此,history 暴露了一个 push方法,来 push 进浏览器的历史访问栈中。 这里再提一句另外一种用法:*history.push() 的方式。这种其实就相当于直接点击了 a 标签一样的道理,只不过用 js 的方式实现了。

为什么要这样实现?有什么别的方式?做个比较?

我们可不可以尝试把展示交给 route 组件管理?router 只控制激活当前的 route?但是这样就不能支持通过 props 传给 router 路由配置的方式使用了,这是其一;
其二,这样其实 route 其实负责了组件的渲染工作,而不是把所有的状态和路由信息全部放在 router 中管理了,不方便集中维护和扩展。 不知道小伙伴们还有别的看法吗?

(本来想写个浅析的……噼里啪啦写了一大堆……还捎带点语无伦次……)
(但是虽然说了一堆……不过确实挺浅的……各位有兴趣可以尝试自己扒一下源码。建议 react-router 和 history 库一起 debug,更有助于我们融会贯通)
( 各位见笑了)