React-router
本质上, React-Router 就是在页面 URL 发生变化的时候,通过写的 path 去匹配,然后渲染对应的组件。
那么,从这句话,想一下如何分步骤实现:

  1. 如何监听 url 的变化 ?
  2. 如何匹配 path,按什么规则 ?
  3. 渲染对应的组件

了解好需要实现的关键步骤,将仓库源码下载下来。
接下来看一下 GitHub, 它使用 lerna 管理同时管理多个包.也就是 Multirepo 概念。

react-router 使用 lerna 来同时管理多个包. ( lerna 的好处特别多,对于依赖关系大,同类型的包推荐使用 lerna 来统一管理。)

React-router 路由本质 - 图2
React-router 路由本质 - 图3
核心库是 react-router, react-router-dom 是在浏览器中使用的,react-router-native是在 rn 中使用的。
如果不理解,直接看一下源码就懂了。其实 react-router-dom 只是多了下面四个组件 BrowserRouter、 Link、NavLink、HashRouter, 其他其实都是直接引用 react-router 的。
React-router 路由本质 - 图4
了解完多包的组织关系之后,回到前面如何实现 react-router 的 3个关键步骤,如下:

  1. 如何监听 url 的变化 ?
  2. 如何匹配 path ?
  3. 渲染对应的组件

这里不自己来实现,直接看源码,站在巨人的肩膀上来学习。接下来来看一下 react-router-dom 官方文档 的基本使用。

  1. export default function App() {
  2. return (
  3. <BrowserRouter>
  4. <div>
  5. <Link to="/">Home</Link>
  6. <Link to="/about">About</Link>
  7. <Link to="/topics">Topics</Link>
  8. <Switch>
  9. <Route path="/about">
  10. <About />
  11. </Route>
  12. <Route path="/">
  13. <Home />
  14. </Route>
  15. </Switch>
  16. </div>
  17. </BrowserRouter>
  18. );
  19. }

从代码中,可以观察到下面几点:

  1. 最外层包裹了<BrowserRouter> ,它有什么意义?
  2. <Route />匹配的外层,包裹了<Switch>,作用是如果匹配了一个,则不会再继续渲染另外一个。如何实现?
  3. Route 中有 path 匹配路径,包裹的则是渲染的组件。

    整体设计

    用一张图来理解一下整个 react-router 是怎么实现的:
    接下来看看每一个步骤是怎么实现的。
    React-router 路由本质 - 图5

    一、监听 URL 的变化

    正常情况下,当 URL 发生变化时,浏览器会像服务端发送请求,但使用以下2种办法不会向服务端发送请求:
  • 基于 hash
  • 基于 history

react-router 使用了 history 这个核心库。

1. 选择方式: history 或 hash

HashRouter 先是从 history 中引用 createBrowserHistory ,然后将 history 和 children 传入到 Router 。BrowseHistory同理。
React-router 路由本质 - 图6
BrowseHistory 必须依赖服务器让 url 都映射到 index.html ,否则会 404 。

2. 监听 URL 的变化,拿到对应的 historylocationmatch 等通过 Provider 注入到子组件中。

React-router 路由本质 - 图7

二、Route 中匹配渲染组件

React-router 路由本质 - 图8
这代码可以分两部分理解:

  1. 是否匹配
  2. 渲染组件

    1. 是否匹配

    computedMatch 是使用 Switch 包裹的子组件才有的值,Switch的作用是从上到下开始渲染,只要匹配到一个,其他的就不匹配。所以这里会先判断 computedMatch 。
    React-router 路由本质 - 图9

    匹配解析 path ,这里使用了第三方库 path-to-regexpReact-router 路由本质 - 图10
    1. // Make sure you consistently `decode` segments.
    2. const fn = match("/user/:id", { decode: decodeURIComponent });
    3. fn("/user/123"); //=> { path: '/user/123', index: 0, params: { id: '123' } }
    4. fn("/invalid"); //=> false
    5. fn("/user/caf%C3%A9"); //=> { path: '/user/caf%C3%A9', index: 0, params: { id: 'café' } }

    2. 组件渲染方式

    从文档来看,它支持三种方式的渲染,如下:

    1. // children 方式
    2. <Route exact path="/">
    3. <HomePage />
    4. </Route>
    5. // func 方式
    6. <Route
    7. path="/blog/:slug"
    8. render={({ match }) => {
    9. // Do whatever you want with the match...
    10. return <div />;
    11. }}
    12. />
    13. // component 方式
    14. <Route path="/user/:username" component={User} />

    源码部分如下:
    React-router 路由本质 - 图11
    一下子看不懂也没关系,来看下面的流程图。
    React-router 路由本质 - 图12
    从上面的代码可以看出:

  3. Router 渲染的优先级:children > component > render,三种方式互斥,只能使用一种。

  4. 不匹配的情况下,只要 children 是函数,也会渲染
  5. component 是使用 createComponent 来创建的, 这会导致不再更新现有组件,而是直接卸载再去挂载一个新的组件。如果是使用匿名函数来传入 component ,每次 render 的时候,这个 props 都不同,会导致重新渲染挂载组件,导致性能特别差。因此,当使用匿名函数的渲染时,请使用 render 或 children 。

    1. // 不要这么使用
    2. <Route path="/user/:username" component={() => <User/> } />

    结论

  6. 对于依赖关系大,同类型的包使用 lerna 来统一管理。尽量抽象出共用不可变的地方,比如 react-router 中的方法。

  7. React-router 使用了Compound components(复合组件模式),在这种模式中,组件将被一起使用,它们可以方便的共享一种隐式的状态,比如 Switch , 可以在这里通过 React.children 来控制包裹组件的渲染优先级,而无须使用者去控制。再比如经常使用的 <select /><option>, 可以通过 React.childrenReact.cloneElement 来劫持修改子组件,让组件使用者通过更少的 api 来触发更强大的功能。