前言
之前写了一篇关于路由中困扰点的文章,相信大家看完后,对于前端路由、后端路由,以及history和hash两种模式,都有了自己的理解并且也知道该如何分辨了。那这篇文章,就是为大家带来React-Router的原理解析。用最简单的代码来阐述React-Router中最核心的几个功能,废话不多说,我们开始吧!
浏览器API与组件的数据交互
正如上一篇文章中分析的那样,前端路由利用了浏览器自带的一些能力,主要为window.history与popstate的结合以及location.hash与hashchange的结合。这两种方式,一边处理的是浏览器的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模式为例,大致的流程图如下
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中几个核心的组件,最后再用一张图来梳理下整个的架构 最后,再补上当用户点击某个导航栏后发生的事