react-router-dom源码解析系列第五篇 - Route, 一起来学习Route
组件的思想和继续完善我们自己的react-router
库.
一、更新
[2019-4-21]
Changed
- 改进文章排版
二、前言
上一篇主要学习了react-router-dom
中Router
组件的基本思想和简单实现, 我们可以发现, Router
作为整个体系的Powered by
-动力来源, 起着至关重要的作用. 而这一篇将要学习的Route
组件, 是整个体系中的执行者
:
PS: 给你一个
path
, 你拿去, 把对应的component
render出来.
由此可见, Route
同样是多么的重要.
三、细说
3.1 前置知识
花了几分钟时间, 画了一张脑图, 大概是这一篇文章的大致脉络😰…
3.2 path-to-regexp
学习Route
组件源码之前, 先来看一下这个库:
react-router-dom
官方再源码中引用了这个库, 库的地址也可以在这里找到. 见名闻其意 —— path-to-regexp, 顾名思义, 就是将path, 例如url
转化为不同模式的RegExp
, 同时也提供了一些配置项供我们使用.
来看一下path-to-regexp库提供的这个方法:
1 | const path = '/user/:id/profile/:secret'; |
这是官方提供的例子, 我们在控制台打印可以看到如下结果:
我们可以看到, path-to-regexp
根据我们输入的path
路径, 以及keys
空数组, 生成了对应的regexp
对象, 以及保存着url参数的数组. 那么我们可以思考一下, 我们能否将这个小例子, 引申到react-router-dom
中呢? 答案是肯定的.
先来捋一下Route
组件的设计思路:
- 接收到带有{ path, sensitive, exact, … }的props
- 判断props是否具有
Switch
组件已经计算好的的match
对象- 有的话, 执行下一步
render
- 没有的话, 计算
match
- 有的话, 执行下一步
- 根据props的{ children, component, render }执行
render
再将目光转向上面提到的例子, 我们可否将例子里的path
当作Route
的path
props? 接着第一个例子, 抛砖引玉, 我们再来看一个:
1 | const path = '/user/:id/profile/:secret'; |
可以看到打印出了如下结果:
这里, 我增加了pathname
常量, 根据例子一中生成的regexp
的match
方法, 来生成一个捕获之后的对象. 生成的数组的前三项分别是精确匹配后的url
, :id
参数, :secret
参数. 得出这个结果, 我们可以整合一下, 能否例子二中的pathname
当作props.location.pathname
?
Route
的path
props根据path-to-reg
生成一个regexp
对象, 该regexp
根据porps.location.pathname
生成捕获到的对象, 该数组对象中具有匹配之后的pathname
, 以及path
中携带的参数.
3.3 源码分析
根据前置知识的分析, 了解到其实Route
组件是引用了path-to-regexp
这个库, 可以说一切的核心都是围绕这个库展开的, 下面来看一看具体的源码…
首先来看一下:
1 | // src/yyg-react-router-dom/components/Route.js |
这一段代码刚好印证了之前提到了Route
组件设计思路的第二步, 当然目前Switch
组件的源码还没看… 其中可以看到, 当props.computedMatch
不存在的话, 就会执行matchPath
这个method. 接着, 我们进入到matchPath
这个函数内部, 也就是Route
同目录下的matchPath.js
看一看…
1 | // src/yyg-react-router-dom/components/matchPath.js |
可以看到, matchPath函数接受两个参数:
- pathname
- 也就是对应的
context.location.pathname
- 也就是对应的
- options
- Route接收到的
props
—— { path, sensitive, exact, strict }
- Route接收到的
可以说, 余下的一切都是围绕这两个参数展开的…
接着再往下看:
1 | // src/yyg-react-router-dom/components/matchPath.js |
由于path
可以将单个string, 也可以接收string[]数组作为值, 所以这里将path
统一转化为数组进行操作.
1 | // src/yyg-react-router-dom/components/matchPath.js |
使用了reduce
方法, 通过遍历path
数组, 如果迭代的match
对象存在, 也就是说匹配到了一个path
, 则直接返回.
紧接着来看:
1 | // src/yyg-react-router-dom/components/matchPath.js |
这里, 也是重点: 通过执行一个名叫compilePath
方法, 传递了相关的props参数, 接收到了{ regexp, keys }两个返回值, 是不是很熟悉? 没错, 和我们的例子一
中的是一样的, 将path
转化成了对应的regexp
, 将path
中携带的参数提取到了keys
数组中.
顺藤摸瓜, 来到compilePath()
函数:
1 | // src/yyg-react-router-dom/components/matchPatch.js |
该函数作为连接path-to-regexp
的桥梁, 主要任务是通过传入的options
配置项 ,将path
转化为regexp
,该函数同样接收两个参数:
- path 需要转化的路径
- options 配置项
- sensitive 是否区分大小写
- strict 是否忽略路径后的斜杠
- end 是否精确匹配, 在做嵌套
Route
的时候非常有用, 本人被坑过🙂🙂🙂…
接着下一步:
1 | // src/yyg-react-router-dom/components/Route.js |
其中, 用到了几个全局常量对象, 这里就不做标注了.
如上所示, 其实是对path-to-regexp
处理的结果进行了缓存, 由于options
的参数是可选的, 所以这时对其进行缓存处理, 每一次计算regexp
都会查询cache
中有无缓存, 有利于优化性能. 这也是看源码值得学习的地方, 理解作者思路, 学习大牛写法🙂…
最后, 和我们之前写的例子一样:
1 | const keys = []; |
将处理后的regexp
返回给调用者, 供调用者使用.
分析完compilePath
方法之后, 回到matchPath
:
1 | // src/yyg-react-router-dom/components/matchPath.js |
上面的源码中, 接收到了compilePath
的返回值, 通过调用regexp
正则对象的exec
方法获取匹配值, 然后通过解构赋值, 将match匹配到的url, 也就是第一个参数赋值给了url
, 该url
日后将为Link
组件享用. 余下的参数统一赋值给了rest
参数, 方便接下来的params
赋值操作…
最后一步, 返回整个match
对象, 包括:
1 | // src/yyg-react-router-dom/components/matchPath.js |
keys
数组中保存着path
中的参数, values
中保存着对应的参数值, 通过reduce
迭代, 最终返回match
对象…
终于分析完了matchPath
这个方法, 我们回到Route.js
文件, 继续之前的进度, 接着往下看:
1 | // src/yyg-react-router-dom/components/Route.js |
上述代码中, 通过es6的解构赋值, 将原context
中的location
和match
对象替换成处理过的location和match. 接着, 由于Route
组件可能接收到{ children, render, component }这三个props, 所以这里分别做了处理… 当然, 上面有一段兼容preact
框架的代码, 这里就不管他了😀…
最后, 到了render
环节, 继续来看源码:
1 | // src/yyg-react-router-dom/components/Route.js |
在render
中, 将更新之后的context
提供给Provider
, 然后再根据children
-> component
-> render
的顺序依次判断是否据此渲染.
其中有使用到了一个名为isEmptyChildren
的方法, 在vscode
内追踪到该函数:
1 | // 判断props.children是否为空? |
我们发现, 该函数内部其实是调用了React
的静态方法, 所以, 不是那么神秘.
到了这里, 基本上Route
组件的源码已经看完了, 剩下了一些Error
或者Warning
处理, 可以当作参考. 下面该继续来完善自己的react-router-dom
库…
四、实践
PS: 隔了一天写的, 接着上面的源码简单分析, 今天主要是完善一下自己的
yyg-react-router-dom库
话不多说, 顺着昨天的思路: Route
通过传入的props
计算match
, 在计算match
的过程中引用了path-to-regexp
npm包, 用来转化path
为RegExp
, 从而与location.pathname
作匹配, 得出计算之后的match
对象.
紧接着, 由于Route
的props有三种渲染方式-children
| render
| component
, 所以这里做了一些判断, 同时, 加入了对preact
的兼容处理以及错误处理
.
那么, 回顾了一下昨天的思路, 就开始动手coding了…
4.1 定义interface
PS: 由于之前已经写入了基本结构, 所以首先要约束所需的
props
1 | // src/yyg-react-router-dom/components/route.tsx |
4.2 拆分处理函数
本来, 这里是不想写的. 但是, 由于比较重要, 也当作是加深记忆把, 所以还是记录下来…
PS: 在
render
中, 尽量减少逻辑代码
也就是说, 尽量将render
中的处理逻辑拆分成单个函数来处理, 这个, 我觉得对于整个组件的整洁是非常重要的…
了解了这点, 我们可以这样 —— 将render
中的处理逻辑提取至handleProcess
这个处理函数:
1 | return ( |
我们在组件中生成一个名为handleProcess
的主进程处理函数, 该函数接收一个名为context
的参数.
1 | function handleProcess( |
这样一来, 整个代码的可读性就好了很多, 同时美观程度也大有改观.
4.3 计算location
location
的来源有两个方面:
- context.location
- props.location
当然, props
的优先级肯定是要比context
的高了, 所以对其作一下简单的处理:
1 | // src/yyg-react-router-dom/components/route.tsx |
4.4 计算match
主函数接收到计算后的location
之后, 将其传递给matchProcess
, 也就是这一步 —— 计算match
:
1 | // src/yyg-react-router-dom/components/route.tsx |
对应的match
处理主函数接收到context
和location
, 进行一系列判断, 如果父组件不是Switch
并且props.path
存在, 则进入computeMatchProcess
处理函数:
1 | // src/yyg-react-router-dom/components/route.tsx |
在computeMatchProcess中, 我们要做的就是对path
进行迭代
处理, 将处理后的match
对象返回, 该match
可能为null
或者键值对
. 这样, 就可以在render
阶段根据match
的值存在与否来决定是否渲染对应的components
.
注意, computedMatchProcess
中引入了computePath
辅助函数, 主要是为了代码分割, 避免一个函数内部的代码过于繁杂…
4.5 融合
计算完相应的match
对象, 在我们自己的主进程处理函数中, 现在已经获取到了计算完毕的location
和match
对象. 这一步, 就是将原来的context
中的对应的location和match替换为计算之后的, 也就是融合.
1 | const composedContext = {...context, match, location}; |
4.6 重置Provider
将融合后的context, 作为新的value传递给Provider
1 | return ( |
4.7 render
根据不同的途径 —— render
| component
| children
, 来进行渲染:
PS:
react-router-dom
官方采用的渲染权重是: children > component > render
在这里, 我们自己玩, 所以不必在意这个东西
1 | return ( |
五、测试
经过上面的实践环节, Route
组件基本完成了, 现在应该做个测试…
在src/test
下新建一个测试组件, 内容随意
在src/App.tsx
中引入我们自己的Route
和测试组件:
1 | // src/App.tsx |
在App
中写入我们的测试代码:
1 | // src/App.tsx |
打开浏览器, 可以看到, 我们自己的组件可以正常渲染, 并且测试组件Test.tsx
中也正常打印出了context
对象, 像这样:
测试成功exact
和component
配置项之后, 再来试一下render
.
在src/test/
下新建Three.tsx
, 作为我们的第三个测试组件, 当然, 内容随意, 能展示出具体内容就ok:
1 | // src/test/Three.tsx |
然后, 在src/App.tsx
中引入该组件, 并且更改测试代码:
1 | // src/App.tsx |
可以看到, 这个开发中经常用到的结构, 完成之后, 重新编译, 在CentBrowser
中作测试:
首先, 当url为/user/:name/profile/:secret
时, 也就是匹配到了component为One
的Route
, 理所当然, 渲染这个组件:
可以看到, 结果是预期所示的, 没有问题, 那么, 接着将url
改为Three
组件对应的path
, 再来看:
结果是Three
组件的内容被完全渲染了出来, 这也证实, 我封装的组件系没有啥大问题的📍
同理, 测试其他配置项都是没有问题的, 这里由于文章篇幅太长, 就不一一展示了…
六、源码
源码地址: 点我
七、总结
写了两天, 终于搞定了这篇文章, 还是收获颇丰的: 通过Route
组件, 掌握了内部的运作机理, 比如match
计算, location计算等, 又通过计算match, 初步了解了path-to-regexp
这个库.
PS: 不静下心看看源码, 永远不知道你和别人查了多远
学无止境, 下一篇文章继续阅读学习Switch
组件的源码, 再会!