React-Router源码解析

507 阅读3分钟

React-Router的简单使用

官方文档

手写mini React-Router

Link

  React-RouterVue-Router 中的 Link 实质上就是a标签。

import { Component } from "react";

export default class Link extends Component {
  render() {
    const { to, children, ...restProps } = this.props;
    return <a href={to} {...restProps}>{children}</a>;
  }
}


BrowserRouter

   根据React-Router库中,Routerreact-router-domreact-router-native的公共组件,跟着库的文件目录:BrowserRouter引用Router并传入一个history参数

import { Component } from "react";
import {createBrowserHistory} from "history";
import Router from "./Router";

export default class BrowserRouter extends Component {
  constructor(props) {
    super(props);
    this.history = createBrowserHistory();
  }

  render() {
    const { children } = this.props;
    return (
      <Router history={this.history}>{ children }</Router>
    );
  }
}

Context

  创建一个全局上下文,方便记录数据,跨层级传递

import React from 'react';

const RouterContext = React.createContext();

export default RouterContext;

Router

  Router下的子组件需要根据BrowserRouter传入的history中的参数自动进行渲染,在Router中需要监听location的变化。且传入Provider的参数需要是对象,子组件才会根据其上下文进行重新渲染

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

export default class Router extends Component {
  static computeRootMatch(pathname) {
    return {
      path: "/",
      url: "/",
      params: {},
      isExact: pathname === "/"
    };
  }

  constructor(props) {
    super(props);
    this.state = {
      location: props.history.location
    }

    // 监听location变化
    this.unListen = props.history.listen(({ location }) => {
      this.setState({location});
    });
  }

  componentWillUnmount() {
    if (this.unListen) {
      this.unListen();
    }
  }

  render() {
    const { children, history } = this.props;
    return <RouterContext.Provider value={{
        history,
        location: this.state.location,
        match: Router.computeRootMatch(this.state.location.pathname)
      }}>
      {children}
      </RouterContext.Provider>;
  }
}

Route

import React, { Component } from "react";
import matchPath from "./matchPath";
import RouterContext from "./Context";

// 展示对应的组件,其中children > component > render
export default class Route extends Component {
  render() {
    return (
      <RouterContext.Consumer>
        {(context) => {
          const { location } = context;
          // computedMatch由Switch组件传入,若存在则直接使用即可 单一选择
          const { path, children, component, render, computedMatch } =
            this.props;
          // 根据location判断当前的界面参数
          const match = computedMatch
            ? computedMatch
            : path
            ? matchPath(location.pathname, this.props)
            : context.match;

          const props = {
            ...context,
            match,
          };

          return (
            // 先判断是否匹配,若匹配则判断是否存在children  再判断是否存在component 最后判断是否存在render
            // 若不匹配: 则判断是否存在children(404),若存在则使用children,若不存在则返回null
            <div>
              {/* 添加Provider的原因: 组件会从组件树离自身最近匹配的Provider中读取当前的context值,hook中需要使用最近的props */}
              <RouterContext.Provider value={props}>
                {match
                  ? children
                    ? typeof children === "function"
                      ? children(props)
                      : children
                    : component
                    ? React.createElement(component, props)
                    : render
                    ? render(props)
                    : null
                  : children
                  ? typeof children === "function"
                    ? children(props)
                    : children
                  : null}
              </RouterContext.Provider>
            </div>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

Switch

import React, { Component } from "react";
import RouterContext from './Context';
import matchPath from "./matchPath";

export default class Switch extends Component {
  render() {
    const { children } = this.props;
    return (
      <RouterContext.Consumer>
        {
          context => {
            let match, element;
            const { location } = context;
            // 只取一个match的组件进行渲染
            // 独占路由,若其中一个Route组件已命中(match不再为null),则不再向下匹配 直接渲染命中组件
            React.Children.forEach(children, (child) => {
            // 精确匹配当前Route组件的path与location.pathname  精确匹配才算匹配
            // 若没精确匹配中 则将match设置为null 继续遍历向下匹配
              if (match == null && React.isValidElement(child)) {
                element = child;
                const { path } = child.props;
                match = path ? matchPath(location.pathname, path) : context.match;
              }
            });
            return match ? React.cloneElement(element, { computedMatch: match }) : null;
          }
        }
        </RouterContext.Consumer>
    );
  }
}

Redirect

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

export default class Redirect extends Component {
  render () {
    return (<RouterContext.Consumer>
       {context => {
          const {history, push = false} = context;
          const {to} = this.props;
          return (
            <LifeCycle
              onMount={() => {
                push ? history.push(to) : history.replace(to);
              }}
            />
          );
       }}
      </RouterContext.Consumer>);
  }
}

// 创建一个返回空的组件,执行其中的函数进行跳转
class LifeCycle extends Component() {
  componentDidMount() {
    if(this.onMount) {
      this.onMount();
    }
  }

  render() {
    return null;
  }
}

withRouter

  withRouter本质上就是一个高阶组件,接收一个组件,将 RouteContext 中的context上下文当做 props 一并传入接收的组件,并返回当前接收组件即可

import React from 'react';
import RouterContext from './Context';

const withRouter = Component => props => {
  return <RouterContext.Consumer>
    {context => {
      // 将context上下文作为参数传给Component
      return <Component {...props} {...context} />;
    }}
  </RouterContext.Consumer>
}

export default withRouter;

hooks

import { useContext }  from 'react';
import RouterContext from './Context';

export function useHistory() {
  return useContext(RouterContext).history;
}

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

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

export function useParams(params) {
  // RouterContext取最近的Provider  需要在Route中包裹一层RouterContext.Provider
  const match = useContext(RouterContext).match;
  return match ? match.params : null
}

matchPath

  直接使用React-Router中的匹配路由文件

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;