React-Route的基本实现

1,064 阅读6分钟

前言

断断续续停更了好久,但是看着大家还是觉得我写的很不错,之前在简书平台的粉丝给我留言,问我为啥不写了,只想说,博主变懒了,现在再次恢复更新,重新换了一个平台,希望对大家理解和学习有帮助

React-Router 源码实现

由于最近刚刚上手React技术栈,在使用React-router 的时候经常忘记一些api,所以就有了这一篇,自己动手实现一些官方的api, 从根本上去了解底层的原理,本文使用的history模式,对于hash模式之后有时间可以补上一些实现,基本原理都差不多,实现以下api和一些hooks

  1. BrowserRouter
  2. Router
  3. Switch 独占路由
  4. Route 路由器
  5. Link 链接
  6. Redirect 重定向

以及下面一些hooks

  1. withRouter
  2. useHistory
  3. useLocation
  4. useRouteMatch
  5. useParams

例子代码

<div className="App">
  <Router>
    <Link to="/">首页</Link>
    <Link to="/user">用户中心</Link>
    <Link to="/login">登录</Link>
    <Link to="/product/123">商品</Link>
    <Switch>
      <Route
        exact
        path="/"
        // children={children}
        component={HomePage}
        render={render}
      >
      </Route>
      <Route path="/user" component={UserPage}/>
      <Route path="/login" component={LoginPage}/>
      <Route path="/product/:id" render={() => <Product/>}/>
      <Route component={_404Page}/>
    </Switch>
  </Router>
</div>

基本介绍

React-router 一切皆为组件的思想

  1. route渲染优先级 children > component > render , 三种情况都可以接受同样的[route props], 包括match, location 和 history, 如果不匹配的时候,match为null (可以做一些缓存机制)
  2. 如果一个route 没有path, 表示一定匹配
  3. Router 是所有组件的入口文件,提供了route 所需要的参数, 应用程序只会有一个高阶Router

实现基本的LinkRouteBrowserRouter

BrowserRouter

BrowserRouter 作为路由的实现的一种方式,提供操作history的一些功能,简单的包括一层参数给组件 Router

import React from 'react'
import { createBrowserHistory } from 'history'
import Router from './Router'
function BrowserRouter(props) {
  const history = createBrowserHistory()
  return (
      <Router history = {history} children={props.children} />
  )
}

export default BrowserRouter
  1. 这里可能会想为什么需要包裹一层Router组件,因为之前我们说过,history模式只是前端路由的一种实现方式还有hash模式等,但是对外提供的组件需要保持一致。
Router

Router 作为提供服务的最外层组件,首先我们需要明白它的作用是那些

  1. 对子组件route 提供match, location, history 等参数
  2. 全局监听路由的变化,给location重新赋值,然后让订阅了location的组件发生变化 (这里就说明了要使用Context)
import React, { useEffect, useState } from 'react'
import { RouterContext } from './Context'

function Router(props) {
  const computedMatch = (pathname) => {
    return { path: '/', url: '/', params: {}, isExact: pathname === '/' }
  }
  const { history, children } = props
  const [location, setLocation] = useState(history.location)
  useEffect(() => {
    let unListen = history.listen((location) => {
      setLocation(location)
    })
    return () => {
      unListen && unListen()
    }
  }, [history])
  return (
      <RouterContext.Provider value={{
        location,
        history,
        match: computedMatch(location.pathname)
      }}>
        {children}
      </RouterContext.Provider>
  )
}

Route

Route 直接决定了要渲染哪一个组件,以及渲染的先后顺序的判断

  1. 渲染规则:
  • 如果匹配 children > component > render > null
  • 如果不匹配, 存在children就渲染它 children(function) > null
  1. 如果指定了path, 匹配后渲染对应的组件,如果没有指定path,则需要默认渲染
import { RouterContext } from './Context'
import matchPath from '../hcc-react-router-dom/matchPath'

function Route(props) {
  const context = useContext(RouterContext)
  const { location } = context
  const { children, component, render, path } = props
  // 由于没有path的时候,要渲染,所以必须要给没有path的组件渲染的时候一个默认值
  const match = path ? matchPath(location.pathname, props) : context.match 
  // 更新渲染组件的props
  const newProps = {
      context,
      ...match
  }
  // match children > component > render > null
  // no match  children(func) > null
  return (
    match ? (children ? (typeof children === 'function' ? children(newProps) : children)
        : (component ? React.createElement(component, newProps)
            : render ? render(newProps) : null))
        : ((typeof children === 'function') ? children(newProps) : null)

  )
}

export default Route

到这里我们就基本上实现了路由之前的切换的时候渲染对应的组件, 基本原理就是根据通过history来监听url变化,如果匹配 成功就返回对应的组件,如果失败就返回null, 由于react对于null元素不进行渲染,所以就实现了只渲染匹配到的元素。

Switch

Switch 独占路由,它会渲染于地址匹配的第一个子节点<Route> 或者 <Redirect>

  1. Switch 的内部实现肯定会遍历子节点,然后根据location.pathname和子节点的props进行匹配, 会将多个Route子节点转换成 一个子节点,所以Route只会渲染被匹配的,其他的Router不会被渲染
// 如果添加了Switch, Route子节点只会渲染匹配的一次,如果没有的话,Route会渲染5次,然后根据规则渲染对应的Route
<Switch>
  <Route
    exact
    path="/"
    // children={children}
    component={HomePage}
    render={render}
  >
  </Route>
  <Route path="/user" component={UserPage}/>
  <Route path="/login" component={LoginPage}/>
  <Route path="/product/:id" render={() => <Product/>}/>
  <Route component={_404Page}/>
</Switch>
  1. 如果都没有匹配,渲染没有path的route子节点
  2. 由于Switch的匹配规则不一样,所以对于Route的渲染需要增加一个参数,来判断是否引入了Switch, 如果引入了就需要按照Switch 的规则 (computedMatch)
import React, { useContext } from 'react'
import matchPath from '../hcc-react-router-dom/matchPath'
import { RouterContext } from './Context'

function Switch(props) {
  // 这里可以使用React.Children.forEach
  const context = useContext(RouterContext)
  let match, element
  React.Children.forEach(props.children, (child) => {
    // 如果没有匹配,并且child是合法的react元素
    if (!match && React.isValidElement(child)) {
      const { path } = child.props
      element = child
      // 如果存在path, 看看是否匹配path,如果全部都不存在的话,给默认值,用于渲染没有path的子节点
      match = path ? matchPath(context.location.pathname, child.props) : context.match
    }
  })
  return (
    match ? React.cloneElement(element, {
      computedMatch: match
    }) : null
  )
}

export default Switch

// route需要这样修改
- const match = path ? matchPath(location.pathname, props) : context.match
+ const match = computedMatch ? computedMatch : path ? matchPath(location.pathname, props) : context.match

Redirect

Redirect 使导航到一个新的地址,通过push选项来确定是否替换到路由栈中的地址

import React, { useContext, useEffect } from 'react'
import { RouterContext } from './Context'

/**
 * @return {null}
 */
function Redirect(props) {
  console.log('-redirect-a')
  const {history} = useContext(RouterContext)
  const {to, push = false} = props
  useEffect(() => {
    push ? history.push(to) : history.replace(to)
  }, [])

  return null
}

export default Redirect

Hook部分和withRouter

在我们开发应用程序的过程中,所有的组件不可能都在最外层使用,如果我们使用了嵌套组件,我们就无法获取match, location 和 history等参数,所有需要提供一些额外的hook和一些高阶组件,来方便嵌套组件来使用参数

高阶组件withRouter

我们通过高阶组件来进行包裹,提供嵌套组件所需要的api

import React from 'react'
import { RouterContext } from './Context'

function withRouter(Component) {
  return (props) => {
    return (
        <RouterContext.Consumer>
          {
            context => {
              return <Component {...props} {...context} />
            }
          }
        </RouterContext.Consumer>
    )
  }
}

export default withRouter

一些Hook

  1. useHistory
  2. useLocation
  3. useRouterMatch
  4. useParams
export function useHistory() {
  return  useContext(RouterContext).history
}

export function useLocation() {
  return useContext(RouterContext).location
}

export function useRouteMatch() {
  return useContext(RouterContext).match
}

export function useParams() {
  const match = useContext(RouterContext).match
  return match ? match.params : {}
}

问题

这样实现了一些Hook方法后,我们发现一个问题,就是嵌套组件获取params, 出现获取不到params的参数值

<Route path="/product/:id" component={Product} />
function Product(props) {
  console.log('Product', props.match)
  return (
      <div>
        <h1>Product</h1>
        <Detail />
      </div>
  )
}

function Detail() {
  const match = useRouteMatch()
  console.log('detail',match)
  return (
      <div>
        <h1>detail</h1>
      </div>
  )
}

在这个例子中,Detail组件获取不到路由为product/123的params的值,这个是什么原因呢?这个问题要回归到Context的本质,它提供一个全局的存放 数据的环境,我们useRouteMatch的实现,它会一直向上寻找到我们最外层的RouterContext.Provider的对象, 这个对象和Route里面合并后的对象少了一个合并matchPath解析过后的步骤,所以我们需要做一些处理,让hooks的 Context的值来至于Route中合并后的值

// .... 修改return中的值,使得在嵌套组件中获取的history,location 等参数来自于最新的合并后的值
  return <RouterContext.Provider value={newProps}>
    {
      match ? (children ? (typeof children === 'function' ? children(newProps) : children)
          : (component ? React.createElement(component, newProps)
              : render ? render(newProps) : null))
          : ((typeof children === 'function') ? children(newProps) : null)

    }
  </RouterContext.Provider>
//....

结语

这里我们就基本实现了React-router的基本api了。

  1. 下一篇介绍实战,通过这些api对于动态路由进行封装,实现类似于Vue中的keep-alive
  2. 下下篇会讲React-redux的实现,
  3. 下下下篇会将前端错误监控系统搭建和定位 计划都是美好的,时间就说不定了