图解 React-router 带你深入理解路由本质
> 阅读源码小 tips:从整体到细节,刚开始不要太拘泥于一行代码的实现,先从整体去理解,理解好各自的关系,再去读源码。
首先,我们先不纠结于源码细节。先用最简单的话来概括一下 React-router 到底做了什么?
本质上, React-Router 就是在页面 URL 发生变化的时候,通过我们写的 path 去匹配,然后渲染对应的组件。
那么,从这句话,我们想一下如何分步骤实现:
- 如何监听 url 的变化 ?
- 如何匹配 path,按什么规则 ?
- 渲染对应的组件
了解好需要实现的关键步骤,我们来将仓库源码下载下来。
接下来我们看一下 GitHub
, 它使用 lerna
管理同时管理多个包.也就是Multirepo
概念。
> react-router 使用 lerna 来同时管理多个包. ( lerna 的好处特别多,对于依赖关系大,同类型的包推荐使用 lerna 来统一管理。)
核心库是 react-router. react-router-dom 是在浏览器中使用的,react-router-native是在 rn 中使用的。
如果不理解,直接看一下源码就懂了。其实 react-router-dom 只是多了下面四个组件 BrowserRouter、 Link、NavLink、HashRouter, 其他其实都是直接引用 react-router 的。
了解完多包的组织关系之后,我们回到前面如何实现 react-router
的 3个关键步骤,如下:
- 如何监听
url
的变化 ? - 如何匹配
path
? - 渲染对应的组件
我们不自己来实现,直接看源码,站在巨人的肩膀上来学习😄。接下来我们来看一下 react-router-dom
官方文档 的基本使用。
export default function App() {
return (
<browserrouter>
<div>
<link to="/">Home
<link to="/about">About
<link to="/topics">Topics
<switch>
<route path="/about">
<about>
</about></route>
<route path="/">
<home>
</home></route>
</switch>
</div>
</browserrouter>
);
}
从代码中,我们可以观察到下面几点:
- 最外层包裹了
<browserrouter>
,它有什么意义? - 在
<route>
匹配的外层,包裹了<switch>
,作用是如果匹配了一个,则不会再继续渲染另外一个。如何实现? - Route 中有
path
匹配路径,包裹的则是渲染的组件。
整体设计
我们用一张图来理解一下整个 react-router 是怎么实现的:
接下来我们看看每一个步骤是怎么实现的。
一、监听 URL 的变化
正常情况下,当 URL 发生变化时,浏览器会像服务端发送请求,但使用以下2种办法不会向服务端发送请求:
- 基于 hash
- 基于 history
react-router 使用了 history 这个核心库。
1. 选择方式: history 或 hash
HashRouter 先是从 history 中引用 createBrowserHistory ,然后将 history 和 children 传入到 Router 。BrowseHistory同理。
BrowseHistory 必须依赖服务器让 url 都映射到 index.html ,否则会 404 。
2. 监听 URL 的变化,拿到对应的 history,location,match 等通过 Provider 注入到子组件中。
二、Route 中匹配渲染组件
这代码可以分两部分理解:
- 是否匹配
- 渲染组件
1. 是否匹配
computedMatch 是使用 Switch 包裹的子组件才有的值,Switch的作用是从上到下开始渲染,只要匹配到一个,其他的就不匹配。所以这里会先判断 computedMatch 。
匹配解析 path ,这里使用了第三方库 path-to-regexp
// Make sure you consistently `decode` segments.
const fn = match("/user/:id", { decode: decodeURIComponent });
fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
fn("/invalid"); //=> false
fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } }
2. 组件渲染方式
从文档来看,它支持三种方式的渲染,如下:
// children 方式
<route exact="" path="/">
<homepage>
</homepage></route>
// func 方式
<route path="/blog/:slug" render="{({" match="" })=""> {
// Do whatever you want with the match...
return <div>;
}}
/>
// component 方式
<route path="/user/:username" component="{User}">
源码部分如下:
吐槽一下,作者怎么就不能好好用 if else 来写,非要写这么多变态的 ?:,请不要学习,除非你的项目只有你一个前端😂。
一下子看不懂也没关系,我们来看下面的流程图。
从上面的代码我们可以看出:
- Router 渲染的优先级:children > component > render,三种方式互斥,只能使用一种。
- 不匹配的情况下,只要 children 是函数,也会渲染
- component 是使用 createComponent 来创建的, 这会导致不再更新现有组件,而是直接卸载再去挂载一个新的组件。如果是使用匿名函数来传入 component ,每次 render 的时候,这个 props 都不同,会导致重新渲染挂载组件,导致性能特别差。因此,当使用匿名函数的渲染时,请使用 render 或 children 。
// 不要这么使用
<route path="/user/:username" component="{()" ==""> <user> } />
结论
对于依赖关系大,同类型的包使用 lerna
来统一管理。尽量抽象出共用不可变的地方,比如 react-router
中的方法。
React-router
使用了Compound components
(复合组件模式),在这种模式中,组件将被一起使用,它们可以方便的共享一种隐式的状态,比如 Switch
, 可以在这里通过 React.children
来控制包裹组件的渲染优先级,而无须使用者去控制。再比如我们经常使用的 <select>
和 <option>
, 可以通过 React.children
和 React.cloneElement 来劫持修改子组件,让组件使用者通过更少的 api 来触发更强大的功能。
公众号:前端加加
更多精彩文章,尽在前端加加,欢迎关注讨论。
我组建了一个氛围特别好的阿里巴巴内推社群,如果你对加入阿里巴巴感兴趣的话(后续有计划也可以),我们可以一起进行面试相关的答疑、聊聊面试的故事、并且在你准备好的时候随时帮你内推。下方加 peen 好友回复「面试」即可。