react-router和react-router-dom的实现

React Router 是完整的 React 路由解决方案
React Router 保持 UI 与 URL 同步。它拥有简单的 API 与强大的功能例如代码缓冲加载、动态路由匹配、以及建立正确的位置过渡处理。今天我们就来大概的看一下这个强大的组件是如何实现的。
react-router为了能实现跨平台的路由解决方案,将react-router分成了三个模块:react-routerreact-router-nativereact-router-dom三个包,react-router是另外两个模块的基础。这里我们主要分析react-router-dom的基本实现。
react-router源码地址: https://github.com/ReactTraining/react-router

RouterContext

react-router使用context实现跨组件间数据传递,所以react-router定义了一个routerContext作为数据源,

// packages/react-router/modules/RouterContext.js
import createContext from "mini-create-react-context";

const createNamedContext = name => {
  const context = createContext();
  context.displayName = name;

  return context;
};

const context = createNamedContext("Router");
export default context;

代码非常好理解,定义createNamedContext函数,然后调用它来创建一个context。

Router

我们在使用react-router-dom的使用,经常会用到BroswerRouter和HashRouter两个组件,这两个组件都使用了Router组件,所以所Router组件是基础,我们先来看看Router组件定义

// packages/react-router/modules/Router.js
import RouterContext from "./RouterContext";

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

  constructor(props) {
    super(props);

    this.state = {
      location: props.history.location
    };

    this._isMounted = false;
    this._pendingLocation = null;

    if (!props.staticContext) {
      this.unlisten = props.history.listen(location => {
        if (this._isMounted) {
          this.setState({ location });
        } else {
          this._pendingLocation = location;
        }
      });
    }
  }

  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation });
    }
  }

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

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

Router组件引入了刚刚提到的RouterContext,将其作为Provider为子组件提提供数据,主要的数据有两个historylocation,这两个对象主要封装了路由跳转的相关功能和路由的相关信息

BrowserRouter和HashRouter

看完了Router的基本实现后,我们可以来看看BrowserRouter和HashRouter的实现了,

  • BrowserRouter

// BrowserRouter packages/react-router-dom/modules/BrowserRouter.js
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
import PropTypes from "prop-types";
import warning from "tiny-warning";

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

BrowserRouter.propTypes = {
  basename: PropTypes.string,
  children: PropTypes.node,
  forceRefresh: PropTypes.bool,
  getUserConfirmation: PropTypes.func,
  keyLength: PropTypes.number
}

// HashRouter packages/react-router-dom/modules/HashRouter.js
class HashRouter extends React.Component {
  history = createHistory(this.props);

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

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

两个Router的实现非常简单,而且非常相似:创建一个history对象,然后渲染Router组件,但这里history对象是怎么创建出来的呢? 感兴趣的朋友可以看看这篇文章:https://blog.csdn.net/tangzhl/article/details/79696055。
在这里我大致说一下他们的作用
createBrowserHistory是通过使用HTML5 history的API来实现路由的跳转(pushState和replaceState)并通过监听popstate事件路由变换,进而更新location对象(Router中提到,location对象封装了路由信息),location对象的改变出发了组件的重新渲染,从而根据路由来渲染不同的组件
createHashHistory通过浏览器的hash来改变路由,然后监听hashChange事件监听hash的改版,然后出发location对象的改变,进而更新试图

Route组件

我们常用route组件来进行路径和组件的匹配,所以我们来看看Route组件的实现
我们先看看Route的不同使用方式

<Route path="xxx" component={Xxx} />
<Route path="xxx" render={() =>(</Xxx>)} />
<Route path="xxx"><Xxx /></Route>

在来看源码,方便理解,都打上了注释,为了代码更新其,也删除了一些判断

// /packages/react-router/modules/Switch.js
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
		  // 获取location对象 Router组件上面的
          const location = this.props.location || context.location;
          // 判断path和location是否对应
          const match = this.props.computedMatch
            ? this.props.computedMatch //  already computed the match for us
            : this.props.path
              ? matchPath(location.pathname, this.props)  // 在这里判断path和locations是否对象
              : context.match;

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

	      // 获取Route上的对应的渲染组件,
          let { children, component, render } = this.props;
          // 判断是children长度是否为0
          if (Array.isArray(children) && children.length === 0) {
            children = null;
          }

		  // 获取children
          if (typeof children === "function") {
            children = children(props);

          }

          return (
            <RouterContext.Provider value={props}>
              {children && !isEmptyChildren(children)
                ? children
                : props.match // 判断path与locaiton是否匹配,如果匹配则渲染component,否则返回null
                  ? component
                    ? React.createElement(component, props)
                    : render
                      ? render(props)
                      : null
                  : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}

Route工作核心步骤只有两三步,获取RouterContext的信息(location对象),获取pathcomponent属性,判断path和当前的location是否匹配,如果匹配,则渲染component,否则返回null,不渲染任何内容

Switch

// /packages/react-router/modules/Route.js
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {

          const location = this.props.location || context.location;

          let element, match;

          React.Children.forEach(this.props.children, child => {
            if (match == null && React.isValidElement(child)) {
              element = child;

              const path = child.props.path || child.props.from;
			  // 判断Route的path和当前location是否匹配,如果匹配则渲染,否则不渲染
              match = path
                ? matchPath(location.pathname, { ...child.props, path })
                : context.match;
            }
          });

          return match
            ? React.cloneElement(element, { location, computedMatch: match })
            : null;
        }}
      </RouterContext.Consumer>
    );
  }
}

Switch组件实现很简单,遍历所有子元素(Route),判断Routepath和location是否匹配,如果匹配,则渲染,否则不渲染。从代码可以看出,貌似Switch会渲染最后一个匹配的Route

Link组件

// /packages/react-router-dom/modules/Link.js
function LinkAnchor({ innerRef, navigate, onClick, ...rest }) {
  const { target } = rest;

  return (
    <a
      {...rest}
      ref={innerRef} // TODO: Use forwardRef instead
      onClick={event => {
        try {
          if (onClick) onClick(event);
        } catch (ex) {
          event.preventDefault();
          throw ex;
        }

        if (
          !event.defaultPrevented && // onClick prevented default
          event.button === 0 && // ignore everything but left clicks
          (!target || target === "_self") && // let browser handle "target=_blank" etc.
          !isModifiedEvent(event) // ignore clicks with modifier keys
        ) {
          event.preventDefault();
          navigate();
        }
      }}
    />
  );
}

function Link({ component = LinkAnchor, replace, to, ...rest }) {
  return (
    <RouterContext.Consumer>
      {context => {
        invariant(context, "You should not use  outside a ");

        const { history } = context;

        const location = normalizeToLocation(
          resolveToLocation(to, context.location),
          context.location
        );

        const href = location ? history.createHref(location) : "";

        return React.createElement(component, {
          ...rest,
          href,
          navigate() {
            const location = resolveToLocation(to, context.location);
            const method = replace ? history.replace : history.push;

            method(location);
          }
        });
      }}
    </RouterContext.Consumer>
  );
}

从代码意义看出,Link组件本质就是a标签,它修改了a标签的默认行为,当点击Link时,会导航到对应的路由,导致locaiton对象的改变,出发组件的更新

withRouter

withRouter是要高阶函数,对传入的组件进行加强,功能就是获取routerContext上面的信息,然后作为props传给需要加强的组件

// /packages/react-router/modules/withRouter.js 
function withRouter(Component) {
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    return (
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {...remainingProps}
              {...context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };
  }
 
 return hoistStatics(C, Component)

总结

React-Router博大精深,同志仍需努力

你可能感兴趣的:(React,前端,React,React-Router)