-
Notifications
You must be signed in to change notification settings - Fork 51
前端路由实现及 react-router v4 源码分析 #21
Comments
很棒。 |
@peterlxb react 是框架哦,是视图框架,框架与库的区别在与框架有限制,你只能按框架要求的写法写,只给了你几个生命周期去用,react 就是如此。而像 jq 或者 lodash 就是库,可以说就是提供一些方便的函数而已,不会要求你怎么用。 |
虽然我英文不好,但是library 是库的意思吧
用来构建用户界面的库。。。应该这样翻译吧 |
好吧,查了一下,你说的没错,准确的说确实是库,感谢指正 |
后面可以加一些跟V3版本设计改动的地方。 |
翻react-router的源码发现入手开始看有些难度,硬着头皮看了history中的一些封装,感觉方向不对,果断搜索了一下react-router源码解读,看完大概了解到封装的思路。谢谢博主 |
写得太好了 感谢 |
写得很棒,但是
这个地方有问题哦!!! https://reactrouter.com/web/api/Route/render-func
|
前言
react-router 目前作为 react 最流行的路由管理库,已经成为了某种意义上的官方路由库(不过下一代的路由库 reach-router 已经蓄势待发了),并且更新到了 v4 版本,完成了一切皆组件的升级。本文将对 react-router v4(以下简称 rr4) 的源码进行分析,来理解 rr4 是如何帮助我们管理路由状态的。
路由
在分析源码之前,先来对路由有一个认识。在 SPA 盛行之前,还不存在前端层面的路由概念,每个 URL 对应一个页面,所有的跳转或者链接都通过
<a>
标签来完成,随着 SPA 的逐渐兴盛及 HTML5 的普及,hash 路由及基于 history 的路由库越来越多。路由库最大的作用就是同步 URL 与其对应的回调函数。对于基于 history 的路由,它通过
history.pushState
来修改 URL,通过window.addEventListener('popstate', callback)
来监听前进/后退事件;对于 hash 路由,通过操作window.location
的字符串来更改 hash,通过window.addEventListener('hashchange', callback)
来监听 URL 的变化。SPA 路由实现
hash 路由
完整实现 hash-router,参考 hash router 。
其实知道了路由的原理,想要实现一个 hash 路由并不困难,比较需要注意的是 backOff 的实现,包括 hash router 中对 backOff 的实现也是有 bug 的,浏览器的回退会触发
hashChange
所以会在history
中 push 一个新的路径,也就是每一步都将被记录。所以需要一个backIndex
来作为返回的 index 的标识,在点击新的 URL 的时候再将 backIndex 回归为this.currentIndex
。基于 history 的路由实现
参考 H5 Router
相比 hash 路由,h5 路由不再需要有些丑陋去的去修改
window.location
了,取而代之使用history.pushState
来完成对window.location
的操作,使用window.addEventListener('popstate', callback)
来对前进/后退进行监听,至于后退则可以直接使用window.history.back()
或者window.history.go(-1)
来直接实现,由于浏览器的 history 控制了前进/后退的逻辑,所以实现简单了很多。react 中的路由
react 作为一个前端视图框架,本身是不具有除了 view (数据与界面之间的抽象)之外的任何功能的,为 react 引入一个路由库的目的与上面的普通 SPA 目的一致,只不过上面路由更改触发的回调函数是我们自己写的操作 DOM 的函数;在 react 中我们不直接操作 DOM,而是管理抽象出来的 VDOM 或者说 JSX,对 react 的来说路由需要管理组件的生命周期,对不同的路由渲染不同的组件。
源码分析
预备知识
在前面我们了解了创建路由的目的,普通 SPA 路由的实现及 react 路由的目的,先来认识一下 rr4 的周边知识,然后就开始对 react-router 的源码分析。
history
history 库,是 rr4 依赖的一个对
window.history
加强版的 history 库。match
源自 history 库,表示当前的 URL 与 path 的匹配的结果
location
还是源自 history 库,是 history 库基于 window.location 的一个衍生。
我们带着问题去分析源码,先逐个分析每个组件的作用,在最后会有回答,在这里先举一个 rr4 的小 DEMO
packages
rr4 将路由拆成了几个包 —— react-router 负责通用的路由逻辑,react-router-dom 负责浏览器的路由管理,react-router-native 负责 react-native 的路由管理,通用的部分直接从 react-router 中导入,用户只需引入 react-router-dom 或 react-router-native 即可,react-router 作为依赖存在不再需要单独引入。
Router
这是我们调用 Router 的方式,这里拿 BrowserRouter 来举例。
BrowserRouter 的源码在 react-router-dom 中,它是一个高阶组件,在内部创建一个全局的 history 对象(可以监听整个路由的变化),并将 history 作为 props 传递给 react-router 的 Router 组件(Router 组件再会将这个 history 的属性作为 context 传递给子组件)
其实整个 Router 的核心是在 react-router 的 Router 组件中,如下,借助 context 向 Route 传递组件,这也解释了为什么 Router 要在所有 Route 的外面。
这是 Router 传递给子组件的 context,事实上 Route 也会将 router 作为 context 向下传递,如果我们在 Route 渲染的组件中加入
来通过 context 访问 router,不过 rr4 一般通过 props 传递,将 history, location, match 作为三个独立的 props 传递给要渲染的组件,这样访问起来方便一点(实际上已经完全将 router 对象的属性完全传递了)。
在 Router 的 componentWillMount 中, 添加了
history.listen
能够监听路由的变化并执行回调事件。在这里每次路由的变化执行的回调事件为
相比于在 setState 里做的操作,setState 本身的意义更大 —— 每次路由变化 -> 触发顶层 Router 的回调事件 -> Router 进行 setState -> 向下传递 nextContext(context 中含有最新的 location)-> 下面的 Route 获取新的 nextContext 判断是否进行渲染。
之所以把这个 subscribe 的函数写在 componentWillMount 里,就像源码中给出的注释:是为了 SSR 的时候,能够使用 Redirect。
Route
Route 的作用是匹配路由,并传递给要渲染的组件 props。
在 Route 的 componentWillReceiveProps 中
Route 接受上层的 Router 传入的 context,Router 中的 history 监听着整个页面的路由变化,当页面发生跳转时,history 触发监听事件,Router 向下传递 nextContext,就会更新 Route 的 props 和 context 来判断当前 Route 的 path 是否匹配 location,如果匹配则渲染,否则不渲染。
是否匹配的依据就是 computeMatch 这个函数,在下文会有分析,这里只需要知道匹配失败则 match 为
null
,如果匹配成功则将 match 的结果作为 props 的一部分,在 render 中传递给传进来的要渲染的组件。接下来看一下 Route 的 render 部分。
rr4 提供了三种渲染组件的方法:component props,render props 和 children props,渲染的优先级也是依次按照顺序,如果前面的已经渲染后了,将会直接 return。
这里解释一下官网的 tips,component 是使用 React.createElement 来创建新的元素,所以如果传入一个内联函数,比如
的话,由于每次的 props.component 都是新创建的,所以 React 在 diff 的时候会认为进来了一个全新的组件,所以会将旧的组件 unmount,再 re-mount。这时候就要使用 render,少了一层包裹的 component 元素,render 展开后的元素类型每次都是一样的,就不会发生 re-mount 了(children 也不会发生 re-mount)。
Switch
我们紧接着 Route 来看 Switch,Switch 是用来嵌套在 Route 的外面,当 Switch 中的第一个 Route 匹配之后就不会再渲染其他的 Route 了。
Switch 也是通过 matchPath 这个函数来判断是否匹配成功,一直按照 Switch 中 children 的顺序依次遍历子元素,如果匹配失败则 match 为 null,如果匹配成功则标记这个子元素和它对应的 location、computedMatch。在最后的时候使用 React.cloneElement 渲染,如果没有匹配到的子元素则返回
null
。接下来我们看下 matchPath 是如何判断 location 是否符合 path 的。
matchPath
matchPath 返回的是一个如下结构的对象
这些信息将作为匹配的参数传递给 Route 和 Switch(Switch 只是一个代理,它的作用还是渲染 Route,Switch 计算得到的 computedMatch 会传递给要渲染的 Route,此时 Route 将直接使用这个 computedMatch 而不需要再自己来计算)。
在 matchPath 内部 compilePath 时,有个
作为 pathToRegexp 的缓存,因为 ES6 的 import 模块导出的是值的引用,所以将 patternCache 可以理解为一个全局变量缓存,缓存以
{option:{pattern: }}
的形式存储,之后如果需要匹配相同 pattern 和 option 的 path,则可以直接从缓存中获得正则表达式和 keys。加缓存的原因是路由页面大部分情况下都是相似的,比如要访问
/user/123
或/users/234
,都会使用/user/:id
这个 path 去匹配,没有必要每次都生成一个新的正则表达式。SPA 在页面整个访问的过程中都维护着这份缓存。Link
实际上我们可能写的最多的就是 Link 这个标签了,我们从它的 render 函数开始看
可以看到 Link 最终还是创建一个 a 标签来包裹住要跳转的元素,但是如果只是一个普通的带 href 的 a 标签,那么就会直接跳转到一个新的页面而不是 SPA 了,所以在这个 a 标签的 handleClick 中会 preventDefault 禁止默认的跳转,所以这里的 href 并没有实际的作用,但仍然可以标示出要跳转到的页面的 URL 并且有更好的 html 语义。
在 handleClick 中,对没有被 “preventDefault的 && 鼠标左键点击的 && 非
_blank
跳转 的&& 没有按住其他功能键的“ 单击进行 preventDefault,然后 push 进 history 中,这也是前面讲过的 —— 路由的变化 与 页面的跳转 是不互相关联的,rr4 在 Link 中通过 history 库的 push 调用了 HTML5 history 的pushState
,但是这仅仅会让路由变化,其他什么都没有改变。还记不记得 Router 中的 listen,它会监听路由的变化,然后通过 context 更新 props 和 nextContext 让下层的 Route 去重新匹配,完成需要渲染部分的更新。withRouter
withRouter 的作用是让我们在普通的非直接嵌套在 Route 中的组件也能获得路由的信息,这时候我们就要
WithRouter(wrappedComponent)
来创建一个 HOC 传递 props,WithRouter 的其实就是用 Route 包裹了 SomeComponent 的一个 HOC。创建 Route 有三种方法,这里直接采用了传递
children
props 的方法,因为这个 HOC 要原封不动的渲染 wrappedComponent(children
props 比较少用得到,某种程度上是一个内部方法)。在最后返回 HOC 时,使用了 hoistStatics 这个方法,这个方法的作用是保留 SomeComponent 类的静态方法,因为 HOC 是在 wrappedComponent 的外层又包了一层 Route,所以要将 wrappedComponent 类的静态方法转移给新的 Route,具体参见 Static Methods Must Be Copied Over。
理解
现在回到一开始的问题,重新理解一下点击一个 Link 跳转的过程。
有两件事需要完成:
过程如下:
hitsory.push(to)
,这个函数实际上就是包装了一下window.history.pushState()
,是 HTML5 history 的 API,但是 pushState 之后除了地址栏有变化其他没有任何影响,到这一步已经完成了目标1:路由的改变。总结
看到这里相信你已经能够理解前端路由的实现及 react-router 的实现,但是 react-router 有很多的不足,这也是为什么 reach-router 的出现的原因。
在下篇文章,我会介绍如何做一个可以缓存的 Route —— 比如在列表页跳转到详情页再后退的时候,恢复列表页的模样,包括状态及滚动位置等。
先放上仓库的地址: react-live-route,喜欢可以 star,欢迎提出 issue。
The text was updated successfully, but these errors were encountered: