出于对react-router一些参数的好奇,对react-router的源码简单的看了一下,这里记录下一些理解和备忘,用于以后复查,以及希望对正在看这篇文章的你,也有所帮助。

目录结构

react-router下分了五个包:

其中,react-router是核心包,react-router-dom是对react-router的简单封装,其余的三个没有了解。

对于浏览器端的页面,直接引用react-router-dom就可以。(这里和react相反,需要引入react和react-dom)。

五个包的源码,都在对应的modules目录下。

react-router-dom/index

export BrowserRouter from "./BrowserRouter";
export HashRouter from "./HashRouter";
export Link from "./Link";
export MemoryRouter from "./MemoryRouter";
export NavLink from "./NavLink";
export Prompt from "./Prompt";
export Redirect from "./Redirect";
export Route from "./Route";
export Router from "./Router";
export StaticRouter from "./StaticRouter";
export Switch from "./Switch";
export generatePath from "./generatePath";
export matchPath from "./matchPath";
export withRouter from "./withRouter";

以上为react-router-dom暴露的所有对象。

其中只有BrowserRouter、HashRouter、Link、NavLink进行了简单的再封装。

其他都是直接引用的react-router中的模块。

react-router-dom/XxxRouter

对于BrowserRouter,HashRouter,MemoryRouter主要靠传入不同的history实例实现不同的功能。

这里以HashRouter为例:

import Router from "./Router";
import { createHashHistory as createHistory } from "history";

class HashRouter extends React.Component {
  static propTypes = {
    basename: PropTypes.string,
    getUserConfirmation: PropTypes.func,
    hashType: PropTypes.oneOf(["hashbang", "noslash", "slash"]),
    children: PropTypes.node
  };

  history = createHashHistory(this.props);

  componentWillMount() {
    //warning(...);
  }

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

BrowserRouter,HashRouter,MemoryRouter 分别对应historycreateBrowserHistory,createHashHistory,createMemoryHistory

并且router的所有props都是直接传给history的create方法的。

关于history的详细介绍,可以查看该链接

react-router/Router

看下Router源码:

class Router extends React.Component {
    static propTypes = {
        history: PropTypes.object.isRequired,
        children: PropTypes.node
    };

    static contextTypes = { router: PropTypes.object };

    static childContextTypes = { router: PropTypes.object.isRequired };

    getChildContext() {
        return {
            router: {
                ...this.context.router,
                history: this.props.history,
                route: {
                    location: this.props.history.location,
                    match: this.state.match
                }
            }
        };
    }

    state = { match: this.computeMatch(this.props.history.location.pathname) };

    computeMatch(pathname) {
        return {
            path: "/",
            url: "/",
            params: {},
            isExact: pathname === "/"
        };
    }

    componentWillMount() {
        const {children, history} = this.props;

        invariant(children == null || React.Children.count(children) === 1,
           "A <Router> may have only one child element");

        this.unlisten = history.listen(() => {
            this.setState({
                match: this.computeMatch(history.location.pathname)
            });
        });
    }

    componentWillReceiveProps(nextProps) {
        warning(this.props.history === nextProps.history,
            "You cannot change <Router history>");
    }

    componentWillUnmount() { this.unlisten(); }

    render() {
        const {children} = this.props;
        return children ? React.Children.only(children) : null;
    }
}

首先,我们能看到router的几个特性:

  1. 具有两个props参数: historychidlren
  2. router下只能有一个子元素(通过React.Children.only进行限制)
  3. props.history初始化后,不能再次更改

render

先看render():

    render() {
        const {children} = this.props;
        return children ? React.Children.only(children) : null;
    }

render()很简单,直接输出children。

context

Router组件与子组件通信,主要是靠context,而且是旧版的context API.

旧版context语法中,设置在父模块的属性有: getChildContext,childContextTypes,设置在子模块的属性有: contextTypes

Router同时存在这三个属性,在于对嵌套路由的支持。

这里主要是对子组件暴露一个context.router对象,格式为: { history, route:{ location, match } }。包含当前history实例(history),以及当前路由匹配信息(route)。
context.router中使用了扩展运算符展开了父Router(this.context.router),但是我觉得后面的historyroute应该覆盖了展开的父Router的属性,所以我还不清楚这里展开父Router有什么意义。

context.router.route.location为当前history实例的location。
context.router.route.match指向this.state.match

router逻辑总结

到这里,应该大致能猜一下react-router的代码逻辑了。

对于url主要有以下5中操作:

这些操作,全部都封装在history中。然后将对应的history实例传入给Router组件。

Router只是通过history实例,监听路由变化,每次变化后都执行setState更新state.match

由于context中的match指向的是state.match,从而造成context发生变化,触发子组件的componentWillReceiveProps回调。

react-router/Route

Route源码:

class Route extends React.Component {
  static propTypes = {
    computedMatch: PropTypes.object, // private, from <Switch>
    path: PropTypes.string,
    exact: PropTypes.bool,
    strict: PropTypes.bool,
    sensitive: PropTypes.bool,
    component: PropTypes.func,
    render: PropTypes.func,
    children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
    location: PropTypes.object
  };

  static contextTypes = {
    router: PropTypes.shape({
      history: PropTypes.object.isRequired,
      route: PropTypes.object.isRequired,
      staticContext: PropTypes.object
    })
  };

  static childContextTypes = {
    router: PropTypes.object.isRequired
  };

  getChildContext() {
    return {
      router: {
        ...this.context.router,
        route: {
          location: this.props.location || this.context.router.route.location,
          match: this.state.match
        }
      }
    };
  }

  state = {
    match: this.computeMatch(this.props, this.context.router)
  };

  computeMatch(
    { computedMatch, location, path, strict, exact, sensitive },
    router
  ) {
    if (computedMatch) return computedMatch; // <Switch> already computed the match for us

    invariant(
      router,
      "You should not use <Route> or withRouter() outside a <Router>"
    );

    const { route } = router;
    const pathname = (location || route.location).pathname;

    return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
  }

  componentWillMount() {
    warning(
      !(this.props.component && this.props.render),
      "You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
    );

    warning(
      !(
        this.props.component &&
        this.props.children &&
        !isEmptyChildren(this.props.children)
      ),
      "You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
    );

    warning(
      !(
        this.props.render &&
        this.props.children &&
        !isEmptyChildren(this.props.children)
      ),
      "You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
    );
  }

  componentWillReceiveProps(nextProps, nextContext) {
    warning(
      !(nextProps.location && !this.props.location),
      '<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
    );

    warning(
      !(!nextProps.location && this.props.location),
      '<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
    );

    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
  }

  render() {
    const { match } = this.state;
    const { children, component, render } = this.props;
    const { history, route, staticContext } = this.context.router;
    const location = this.props.location || route.location;
    const props = { match, location, history, staticContext };

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

    if (typeof children === "function") return children(props);

    if (children && !isEmptyChildren(children))
      return React.Children.only(children);

    return null;
  }
}

props

首先可以看到参数格式,对外暴露总共7个参数,以及props.children。

其中,路径匹配相关的props参数:

渲染相关:

context

可以看到Route也实现了context的接口。向子元素暴露context.router对象。格式为:{ history, route:{ location,match } }
其中,

state.match

当前的路由信息保存在state.match中,并且在componentWillReceiveProps回调中通过setState()进行更新。
state.match的格式为: { path, url, isExact, params },如果未匹配当前路径,则为null

state.match更新逻辑

  1. 如果未指定props.path,则返回父组件的context.match
  2. 根据location.pathname,进行匹配,如果匹配失败,则返回null。(详细匹配规则参照该文档path-to-regexp)
  3. 匹配成功,返回{ path, url, isExact, params }

componentWillReceiveProps(nextProps, nextContext)

在该回调中执行setState(),每当父组件context变化时,会触发该回调,并传入新的context对象作为nextContext参数

props限制

props.location在初始化后,不允许再更改,其他props属性可以随意更改,每次更改都会更新state.match

render()伪代码:

    var childProps= { match, location, history };

    if (props.component) return match ? React.createElement(props.component, childProps) : null;

    else if (props.render) return match ? props.render(props) : null;

    else if (typeof props.children === "function") return props.children(props);

    else if (children) return React.Children.only(children);

    else return null;

注意:

匹配规则总结

到这里,react-router最核心的匹配规则已经很清楚了。

实现功能总结

在回头看下index.js中的暴露对象,可以总结下react-router所有实现的功能:

  1. 路由类型: StaticRouter, BrowserRouter, HashRouter, MemoryRouter, Router
  2. 路由导航: Link, NavLink
  3. 路由守卫: Prompt
  4. 路由匹配: Route, Redirect, Switch
  5. 帮助方法-获取路由相关对象: withRouter
  6. 帮助方法-path匹配规则: matchPath
  7. 帮助方法-生成path: generatePath

仅我个人而言,react-router已经满足绝大部分需求了,如果再有路由相关的需求,则可以对照该列表比较,如果不满足就需要额外开发,目前我只遇到了关于权限控制的需求,需要额外开发。