react-router 源码学习笔记

269 阅读7分钟

学习react-router源码笔记。

案例

通过一个简单的案例,引出路由的使用及源代码的实现。

案例结果

  1. 通过点击不同的链接,实现不同内容的展示。

  2. 在用户中心页面采用路由守卫功能,如果没有登录,点击用户中心时会跳转登录页面,点击登录之后跳转到用户中心页面。

  1. '搜索'链采用动态路由匹配,同时路由嵌套显示‘查看详情’链接,点击查看详情显示‘详情页面’。

页面布局代码

首页 --- HomePage.jsx

import React, { Component } from 'react'

export default class HomePage extends Component {
    render() {
        return (
            <div>
                <h2>首页</h2>
            </div>
        )
    }
}

用户中心 --- UserPage.jsx

import React, { Component } from 'react'

export default class UserPage extends Component {
    render() {
        return (
            <div>
                <h2>用户中心</h2>
            </div>
        )
    }
}

登录 --- LoginPage.jsx

登录页面使用路由守卫判断用户是否登录(isLogin), 如果登录允许用户进入当前页面,如果没有登录,显示登录界面。 点击登录按钮,模拟登录操作,在localStorage中写入{ isLogin: true }

import React, { Component } from 'react'
// import { Redirect } from 'react-router-dom'
import { Redirect } from './react-router-dom'

export default class LoginPage extends Component {
    loginSubmit = (loginRedirect) => { 
        localStorage.setItem('isLogin', true) 
        window.location.replace(loginRedirect)
    }
    render() {
        const { isLogin, location } = this.props
        const { loginRedirect='/' } = location.state || {}
        if(isLogin) {
            // return <Redirect to={loginRedirect} />
            return <Redirect to={{pathname:loginRedirect}} />

        }else{
            return (
                <div>
                     <button onClick={() => this.loginSubmit(loginRedirect)}>登录</button>
                </div>
            )
        }
    }
}

路由守卫 --- Private.jsx

import React, { Component } from 'react'
// import { Redirect, Route } from 'react-router-dom'
import { Route, Redirect } from './react-router-dom'


export default class PrivateRoute extends Component {
    render() {
        const { isLogin, path, component} = this.props
        if(isLogin) {
            return <Route path={path} component={component} />
        }else {
            return <Redirect to={{pathname: '/login', state:{loginRedirect: path}}} />
        }
    }
}

路由配置 RoutePage.jsx

路由守卫组件判断 isLogin 如果为true允许用户进入当前页面,如果为 false 重定向到登录页面

import React, { Component } from 'react'
// import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom'
import { BrowserRouter as Router, Link, Route, Switch } from './react-router-dom'
import HomePage from './HomePage'
import LoginPage from './LoginPage'
import Private from './Private'
import UserPage from './UserPage'

export default class RouterPages extends Component {
   
    // 获取登录
    render() {
        return (
            <div>
                <Router>
                    <Link to='/'>首页  </Link>
                    <Link to='/user'>  用户中心  </Link>
                    <Link to='/login'>  登录  </Link>
                    <Link to='/children'>  children  </Link>
                    <Link to='/render'>  render  </Link>
                    <Link to='/search/123'>  搜索  </Link>


                    {/* <Switch location={{pathname: '/user'}}> */}
                    <Switch>
                    
                        <Route path='/' component={HomePage} exact/>
                        {/* <Route path='/user' component={UserPage} /> */}

                        <Route path='/children' children={() => (<div>children</div>)} />
                        {/* <Route path='/children' children={'children'} /> */}

                        <Route path='/render' render={() => <div>render</div>} />
                        <Route path='/search/:id' component={Search} />
                        
                       {/* 从本地存储中获取isLogin 测试路由守卫功能 */}
                        <Private path='/user' isLogin={localStorage.getItem('isLogin')} component={UserPage} />
                        <Route path='/login' component={LoginPage} />
                        <Route render={() => <div>404</div> } />
                    </Switch>
                    
                </Router>
            </div>
        )
    }
}

function Search(props) { 
    const { id } = props.match.params
    return (
        <div>
            搜索 动态路由--- {id} <br/>
            <Link to='/search/123/detail'>查看详情</Link>
            <Route path='/search/:id/detail' component={Detail} />
        </div>
    )
 }

 function Detail() {
     return (
         <h2>详情页面</h2>
     )
 }

react-router源代码实现

创建context

import React from 'react';

export const RouterContext = React.createContext()

BrowserRouter

import React, { Component } from 'react'
import {createBrowserHistory} from 'history'
import { RouterContext } from './RouteContext'

class BrowserRouter extends Component {
    // 初始化一个match对象,路由匹配对象,当路由匹配(<Route />)成功之后返回一个match对象,匹配不成功返回null
    static computeRootMatch(pathname) {
        return {
            path: '/',
            url: '/',
            params: {},
            isExact: pathname === '/'
        }
    }
    constructor(props) {
        super(props)
        // 利用react-router提供的createBrowserHistory创建一个功能兼容性良好的history对象
        this.history = createBrowserHistory()
        this.state = {
            location: this.history.location
        }
        // 监听location的改变
        this.unListen = this.history.listen(location => {
            this.setState({location})
        })

    }
    componentWillUnmount() {
    	// 当组件卸载时,卸载路由监听
        console.log('this.unListen', this.unListen)
        if(this.unListen) this.unListen()
    }
    render() {
        console.log('当前路由', this.state.location)
        // 利用context 实现 history location match对象的共享
        return (
            <RouterContext.Provider value={{
                history: this.history, 
                location: this.state.location,
                match: BrowserRouter.computeRootMatch(this.state.location.pathname)
            }}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }
}

export {BrowserRouter}

Link

import React, { Component } from 'react'
import { RouterContext } from './RouteContext';

class Link extends Component {
    handleClick = (e, history) => {
        // 使用a标签实现Link标签,阻止a标签的默认行为 通过 history.push 方法实现跳转
        e.preventDefault();
        history.push(this.props.to)
         
    }
    render() {
        const { to, children} = this.props
        return (
            <RouterContext.Consumer>
                {
                    context => (<a href={to} onClick={(e) => this.handleClick(e, context.history)}>{children}</a>)
                }
            </RouterContext.Consumer>
        )
    }
}

export {Link}

Switch

import React, { Component } from 'react'
import matchPath from './matchPath'
import { RouterContext } from './RouteContext'

class Switch extends Component {
    render() {
        return (
            <RouterContext.Consumer>
                {
                    context => {
                        console.log('Switch', context, this.props)
                        /*
                      Switch 
                            {history: {…}, location: {…}, match: {…}}
                            history: {length: 50, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …}
                            location: {pathname: "/login", search: "", hash: "", state: undefined, key: "pxbkis"}
                            match: {path: "/", url: "/", params: {…}, isExact: false}
                            __proto__: Object
                        */
                        
                        // 找出
                        let element, match
                        // const {location} = context
                        // 优先使用props上的location:<Switch location={{pathname: '/user'}}>
                        
                        const location = this.props.location || context.location
                        const { children } = this.props
                        
                        // React提供的数组遍历的方式
                        React.Children.forEach(children, child => {
                            if(match == null && React.isValidElement(child)) {
                                // <Route path='/' component={HomePage} exact/>
                                element = child
                                const path = child.props.path
                                console.log('child', child, path)
                                
                                // location.pathname 用户点击的链接的pathname
                                // path : 遍历children时的每一个child的路径 比如:<Route path='/' component={HomePage} exact/> path: '/'
                                match = path ? matchPath(location.pathname, {...child.props, path}) : context.match
                            }
                        })
                        
                        // cloneElement(element, otherProps)
                        // createElement(element.type, props)
                        return match ? React.cloneElement(element, {location, computedMatch: match}) : null
                    }
                }
            </RouterContext.Consumer>
        )
    }
}

export {Switch}

matchPath

复制的源码中的代码

import pathToRegexp from "path-to-regexp";

const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];
  const regexp = pathToRegexp(path, keys, options);
  const result = { regexp, keys };

  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

/**
 * Public API for matching a URL pathname to a path.
 */
function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {
    options = { path: options };
  }

  const { path, exact = false, strict = false, sensitive = false } = options;

  const paths = [].concat(path);

  return paths.reduce((matched, path) => {
    if (!path && path !== "") return null;
    if (matched) return matched;

    const { regexp, keys } = compilePath(path, {
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);

    if (!match) return null;

    const [url, ...values] = match;
    const isExact = pathname === url;

    if (exact && !isExact) return null;

    return {
      path, // the path used to match
      url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
      isExact, // whether or not we matched exactly
      params: keys.reduce((memo, key, index) => {
        memo[key.name] = values[index];
        return memo;
      }, {})
    };
  }, null);
}

export default matchPath;

Route

import React, { Component } from 'react'
import { RouterContext } from './RouteContext';
import mathPath from './matchPath'
class Route extends Component {
    render() {
      
        return (
            <RouterContext.Consumer>
                {
                    context => {
                        console.log('Route', context)
                        const { path, computedMatch, children, component, render } = this.props
                        
                        //  覆盖组件调用时用户传递的location
                        const location = this.props.location || context.location

                        const match = computedMatch ? computedMatch : path ? mathPath(location.pathname, this.props) : context.match
                        console.log('Route match', match)
                        /*
                        match: 
                            {path: "/", url: "/", isExact: true, params: {…}}
                            isExact: true
                            params: {}
                            path: "/"
                            url: "/"
                            __proto__: Object}
                        如果是动态路由:
                        Route match 
                            {path: "/search/:id/detail", url: "/search/123/detail", isExact: true, params: {…}}
                            isExact: true
                            params: {id: "123"}
                            path: "/search/:id/detail"
                            url: "/search/123/detail"
                            __proto__: Object}
                        */
                        
                        // children, component, render 能接收到 history、 location、match
                        // 所以我们定义props,传下去
                         const props = {
                             ...context, 
                             location,
                             match
                         }
                         
                         // match 渲染 children component render 或者 null
                         // match的时候如果children 存在:function 或者 children 本身
                         // 不match 渲染 children() 或者 null
                         // children 是和match无关的
                         
                         return (
                            //  对于嵌套路由用里层的参数覆盖掉上一层的参数,使内层路由拿到最新的值。
                            // 在使用context时,拿到的是最近的一层Provider,如果不写,当前<Route />的内层路由就只能获取到<Route />外层Provider的数据
                             <RouterContext.Provider value={props}>
                                  {
                                    match ? 
                                    (children 
                                        ? 
                                        (typeof children === 'function' ? children(props) : children) 
                                        : 
                                        (component ? (React.createElement(component, props)) : (render ? render(props) : (null)))
                                    ) 
                                    : 
                                    (typeof children === 'function' ? children(props) : null)
                                  }
                               </RouterContext.Provider>
                         )
                    }
                }
            </RouterContext.Consumer>
        )
    }
}

export {Route}

Redirect

import React, { Component } from 'react'
import { RouterContext } from './RouteContext'

class Redirect extends Component {
    render() {

        return (
            <RouterContext.Consumer>
                {
                    context => {
                        const { history } = context
                        const { to } = this.props
                         return <LifeCycle onMount={() => history.push(to)}></LifeCycle>
                    }
                }
            </RouterContext.Consumer>
        )
    }
}

class LifeCycle extends Component {
    componentDidMount() {
        if(this.props.onMount) {
            this.props.onMount()
        }
    }
    render() {
        return null
    }
}



export {Redirect} 

导出react-router方法

export {BrowserRouter} from './BrowserRouter'
export {Link} from './Link'
export {Route} from './Route'
export {Switch} from './Switch'
export {Redirect} from './Redirect'

总结:

  1. 页面刚加载时,遇到第一个组件 BrowserRoute:
  • BrowserRouter 中初始化一个match对象
 static computeRootMatch(pathname) {
        return {
            path: '/',
            url: '/',
            params: {},
            isExact: pathname === '/'
        }
    }
  • 通过 createBrowserHistory() 创建一个 history 对象
this.history = createBrowserHistory()
  • 在state中创建一个{ location: this.history.location }(从history对象中单独拿出location)
  • 监听 history 的变化,同时修改 state中的location
constructor(props) {
        super(props)
        this.history = createBrowserHistory()
        this.state = {
            location: this.history.location
        }
        this.unListen = this.history.listen(location => {
            this.setState({location})
        })
    }
    componentWillUnmount() {
        console.log('this.unListen', this.unListen)
        if(this.unListen) this.unListen()
    }
  • 使用 context ,将 history location match 传递下去
  render() {
        return (
            <RouterContext.Provider value={{
                history: this.history, 
                location: this.state.location,
                match: BrowserRouter.computeRootMatch(this.state.location.pathname)
            }}>
                {this.props.children}
            </RouterContext.Provider>
        )
    }
  1. 第二个组件 Switch 组件:
  • 获取到 Switch 组件标签中间所有的 Route 组件
   <Switch>
                    
      <Route path='/' component={HomePage} exact/>
      {/* <Route path='/user' component={UserPage} /> */}

      <Route path='/children' children={() => (<div>children</div>)} />
      {/* <Route path='/children' children={'children'} /> */}

      <Route path='/render' render={() => <div>render</div>} />
      <Route path='/search/:id' component={Search} />


      <Private path='/user' isLogin={localStorage.getItem('isLogin')} component={UserPage} />
      <Route path='/login' component={LoginPage} />
      <Route render={() => <div>404</div> } />
  </Switch>
  • 使用 React.Children.forEach() 遍历每个 Route 组件,得到匹配成功的 Route 赋值 element

  • 同时利用 matchPath() 方法得到匹配成功的 match 对象。如果没有匹配成功 element 就是 <Route render={() => <div>404</div> } />, match 取 context.match 就是自定义的 match 对象

  • 最后利用 React.cloneElement() 返回 匹配成功的 Route 组件 或者 <Route render={() => <div>404</div> } />

        context => {
            console.log('Switch', context, this.props)
            /*
          Switch 
                {history: {…}, location: {…}, match: {…}}
                history: {length: 50, action: "PUSH", location: {…}, createHref: ƒ, push: ƒ, …}
                location: {pathname: "/login", search: "", hash: "", state: undefined, key: "pxbkis"}
                match: {path: "/", url: "/", params: {…}, isExact: false}
                __proto__: Object
            */
            // 找出
            let element, match
            // const {location} = context
            // 优先使用props上的location:<Switch location={{pathname: '/user'}}>
            const location = this.props.location || context.location
            const { children } = this.props
            React.Children.forEach(children, child => {
                if(match == null && React.isValidElement(child)) {
                    // <Route path='/' component={HomePage} exact/>
                    element = child
                    const path = child.props.path
                    console.log('child', child, path)
                    // location.pathname 用户点击的链接的pathname
                    // path : 遍历children时的每一个child的路径 比如:<Route path='/' component={HomePage} exact/> path: '/'
                    match = path ? matchPath(location.pathname, {...child.props, path}) : context.match
                }
            })
            // cloneElement(element, otherProps)
            // createElement(element.type, props)
            return match ? React.cloneElement(element, {location, computedMatch: match}) : null
        }
  1. 第三个组件 Route 组件
  • 根据点击不同的链接(地址栏中最新的信息),覆盖掉原来的 location match 信息
   context => {
        console.log('Route', context)
        const { path, computedMatch, children, component, render } = this.props
        // const match = context.location.pathname === path
        // return match ? React.createElement(component, this.props) : null
        //  覆盖组件调用时用户传递的location
        const location = this.props.location || context.location

        const match = computedMatch ? computedMatch : path ? mathPath(location.pathname, this.props) : context.match
        console.log('Route match', match)
        /*
        match: 
            {path: "/", url: "/", isExact: true, params: {…}}
            isExact: true
            params: {}
            path: "/"
            url: "/"
            __proto__: Object}
        如果是动态路由:
        Route match 
            {path: "/search/:id/detail", url: "/search/123/detail", isExact: true, params: {…}}
            isExact: true
            params: {id: "123"}
            path: "/search/:id/detail"
            url: "/search/123/detail"
            __proto__: Object}
        */
        // children, component, render 能接收到 history、 location、match
        // 所以我们定义props,传下去
         const props = {
             ...context, 
             location,
             match
         }
  • 处理 Route 中使用 children component render 三种不同方式加载组件的情况
  • 同时将最新的 location history match 传递下去
<RouterContext.Provider value={props}>
    {
      match ? 
      (
          children 
          ? 
          (typeof children === 'function' ? children(props) : children) 
          : 
          (component ? (React.createElement(component, props)) : (render ? render(props) : (null)))
      ) 
      : 
      (typeof children === 'function' ? children(props) : null)
    }
 </RouterContext.Provider>
  1. Redirect 组件
  • 从上下文对象中获取 history 对象,同时获取到 to 属性值
class Redirect extends Component {
    render() {

        return (
            <RouterContext.Consumer>
                {
                    context => {
                        const { history } = context
                        const { to } = this.props
                         return <LifeCycle onMount={() => history.push(to)}></LifeCycle>
                    }
                }
            </RouterContext.Consumer>
        )
    }
}
  • 利用 history.push 方法,在组件componentDidMount生命周期中将路径重定向
class LifeCycle extends Component {
    componentDidMount() {
        if(this.props.onMount) {
            this.props.onMount()
        }
    }
    render() {
        return null
    }
}