一文实现源码 React Router 源码

542 阅读5分钟

前言

本文介绍的是,关于讲解关于 React-Router 底层逻辑,模拟实现一下 React-Router 路由,实现过程中,帮助自己理解掌握路由的原理

1. 第一步

构建模拟项目的架构,使用 create-react-app 生成项目模版,这里就不详细叙述

create-react-app react-routet-project

2. 第二步

我们需要模拟实现通过切换路由,跳转到不同的页面(这里我们配置好 3 个页面),下面通过 react-router-dom 这个官方推荐的库实现路由跳转

平常我们大部分项目中也是这样配置处理,遇到路由通常会另外新建一个文件进行配置

export default Routes = [
    {
        path: '...',
        component: xxxx,
    },
    ....
]

在这个基础上我们将 react-router-dom 替换成我们自己模拟实现的 react-router

import Page1 from './pages/page1'
import Page2 from './pages/page2'
import Page3 from './pages/page3'
import React, { Component } from 'react'
import { Route, Switch, withRouter, BrowserRouter } from 'react-router-dom';

console.log(Route, BrowserRouter)

class Router extends Component {
  constructor(props) {
    super(props)
    this.state = {
    }
  }

  render() {
    return (
      <BrowserRouter>
         <Switch>
           <Route exact path="/" component={withRouter(Page1)} />
           <Route exact path="/page2" component={withRouter(Page2)} />
           <Route exact path="/page3" component={withRouter(Page3)} />
         </Switch>
      </BrowserRouter>
    )
  }
}
export default Router

这里用到了 withRouter 需要解释一下,使用它的时候,因为外层有 BrowserRouter 包装,通过 withRouter 可以拿到 路由相关的上下文内容

image.png

3. 第三步

下面我们需要实现 react-router-dom 中下面的组件

  • Route
  • Switch
  • withRouter
  • BrowserRouter

3.1 BrowserRouter

BrowserRouter 的实现 基于 React.createContext() 实现

核心原理

监听popstate事件,当浏览器路由地址history 发生变化的时候,那么在popstate回调中,拿到变化值,进行不同的渲染操作

这里就不具体实现

  • basename: string
  • getUserConfirmation: func
  • forceRefresh: bool
  • keyLength: number
  • children: node
import React, { Component } from "react";
import Context from './context';

export default class BrowserRouter extends Component {
    constructor(props) {
        super(props);
        this.state = {
            location: {
                pathname: window.location.pathname || "/",
                search: undefined,
            },
            match: {

            }
        }
    }
    componentWillMount() {
        // 监听 popstate 事件,如果我们路由发生变化,就会重新修改 state 值
        window.addEventListener("popstate", () => {
            // 保存当前的 pathname
            this.setState({
                location: {
                    pathname: window.location.pathname
                }
            })
        })
    }
    render() {
        const curRoute = {
            location: this.state.location,
            match: this.state.match,
            history: {
                // 模拟 页面上需要调用的函数 push, replace 等 浏览器 API
                push: (to) => {
                    // 根据当前 to 去匹配不同的路由 实现路由切换
                    // to 是对象的情况是 使用了 Link 的情况 
                    // <Link to="/today"/> // renders <a href="/calendar/today">
                    if (typeof to === 'object') {
                        let { pathname, query } = to;
                        // 只是改变当前state的数据, 不触发reRender
                        this.setState({
                            location: {
                                query: to.query,
                                pathname: to.pathname
                            }
                        });
                        // 实现路由跳转,前进,推入新的路由
                        window.history.pushState({}, {}, pathname)
                    } else {
                        // 如果是字符串
                        this.setState({
                            location: {
                                pathname: to
                            }
                        })
                        // 实现路由跳转
                        window.history.pushState({}, {}, to)
                    }

                }
            }
        }
        // Context 就是 React.createContext()
        return (
            <Context.Provider value={curRoute}>{this.props.children}</Context.Provider>
        )
    }
}

3.2 Route

Route官方文档地址

这里用到了 路由匹配的正则表达式,这里引入第三方组件库 path-to-regexp

原理

通过在上一步 Context.Provider 中注入的数据 curRoute 数据,将拿到的 curRoute 数据 和 组件中传入的 path 参数进行 Diff 匹配,如果匹配成功,那么就渲染传入的 Component 组件,如果没有匹配成功的话,那么渲染空组件

import React, { Component } from "react";
import Context from "./context";
import { pathToRegexp, match } from "path-to-regexp";


export default class Route extends Component {
    static contextType = context;
    render() {
        const currenRoutePath = this.context.location.pathname; // 从上下文context中获取到当前路由
        const { path, component: Component, exact = false } = this.props; // 获取Route组件props的路由
        const paramsRegexp = match(path, { end: exact }); // 生成获取params的表达式
        const matchResult = paramsRegexp(currenRoutePath); // 通过正则获取得到 匹配的路由结果
        console.log("路由匹配结果", matchResult);
        
        
        this.context.match.params = matchResult.params; // 获取到请求参数
        
        // 将上下文全部的内容 赋值到 props
        const props = {
            ...this.context
        }
        
        const pathRegexp = pathToRegexp(path, [], { end: exact }); // 生成路径匹配表达式
        // 核心关键步骤,如果匹配到浏览器的路由路径和传入路径匹配,那么渲染传入的组件
        if (pathRegexp.test(currenRoutePath)) {
            return (<Component {...props}></Component>) // 将概念上下文路由信息当作传递给传入的组件
        }
        return null; // 返回空组件 可以在
    }
}

3.3 Switch

Switch 核心是在组件内接收 多个 Route 组件作为 Children, 遍历 Children 然后再做路由的匹配,到底是渲染哪一个组件,这里直接贴源码,实现原理也很简单

var Switch = /*#__PURE__*/function (_React$Component) {
  _inheritsLoose(Switch, _React$Component); // 这个可以不管

  function Switch() {
    // 上下文 this 指向绑定
    return _React$Component.apply(this, arguments) || this;
  }

  var _proto = Switch.prototype;

  // 组件函数的 render 方法,写法其实一样
  _proto.render = function render() {
    var _this = this;
    
    // 只返回一个 React Element
    return /*#__PURE__*/React.createElement(context.Consumer, null, function (context) {
      !context ?  invariant(false, "You should not use <Switch> outside a <Router>")  : void 0;
      var location = _this.props.location || context.location;
      var element, match;
      // toArray 会将每个 Child 加 Key
      // component at different URLs.
      
      // 遍历 传入的多个 Route 子组件
      React.Children.forEach(_this.props.children, function (child) {
        if (match == null && /*#__PURE__*/React.isValidElement(child)) {
          element = child;
          var path = child.props.path || child.props.from;
          // 判断是否匹配成功,如果成功就将成功的 path location 等参数传入
          match = path ? matchPath(location.pathname, _extends({}, child.props, {
            path: path
          })) : context.match;
        }
      });
      // 匹配失败 那么就是 null
      return match ? /*#__PURE__*/React.cloneElement(element, {
        location: location,
        computedMatch: match
      }) : null;
    });
  };

  return Switch;
}(React.Component);

3.4 withRouter

withRouter 核心其实是用 React.Consumer 包裹一层,将上一层 BrowserRouter 或者 HashRouter 上的 Provider 数值传入到 withRouter 组件中调用继承的作用

import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import invariant from "tiny-invariant";

import RouterContext from "./RouterContext.js";

function withRouter(Component) {
  const displayName = `withRouter(${Component.displayName || Component.name})`;
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      // 核心逻辑在这里
      // context 拿到从父组件获取的上下文
      <RouterContext.Consumer>
        {context => {
          invariant(
            context,
            `You should not use <${displayName} /> outside a <Router>`
          );
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };

  C.displayName = displayName;
  C.WrappedComponent = Component;

  return hoistStatics(C, Component);
}

export default withRouter;

3.5 Link

import React, { Component } from "react";
import context from "./context";

export default class Link extends Component {
    static contextType = context;
    render() {
        const { to } = this.props;
        return (
            <a onClick={() => {this.context.history.push(to)}}>
            {
                this.props.children
            }
            </a>
        )
    }
}

小结

致此,这里就基本实现了 React 路由的实现了,简单版本的 React-Router,在参照官方版本的源码的基础上,其实不难理解它的原理,但面试的时候很多情况下都会被问到他们是如何实现的。如果自己亲手实现一遍,基本上就了解它是如何实现并且展开来使用的,这对以后工作帮助也很大。这里 BrowserRoute 实现方式取巧用到的是 React.createContext ,但是我们抛出这个 API,很大概率在面试上会被追问下去,如何实现 createContext的,这里就不详细讲述了

后记

面试的时候,经常会被问到关于,history 路由和 Hash 路由之间的区别。这里也总结一下

  • 由于历史的原因,浏览器以前是没有提供 API 去操作触发浏览器重新请求的,所以才有了hash 路由,监听 hash 值改变去渲染不同的页面,HTML5 以后多了很多 API 例如 onpopstate 去做监听
  • SPA 单页应用路由的方式,慢慢由 hash 变成 history 出于美观的话,少了一个 #
  • hash 路由通过监听 onhashchange 事件,在回调里面获取 hash 值的变化
  • history 路由就是 通过 pushState 和 replaceState 来检测地址的改变,切换不同的页面。

image.png