用 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,更有助于我们融会贯通)
( 各位见笑了)