React-Router Note

history

react router 基于 history 库实现,history 有统一的 api 兼容不同浏览器、不同环境下的历史记录管理,history 包含三种模式:

  • hash,低版本浏览器
    • 搜索引擎不友好,难以监控用户行为
  • browser,高版本浏览器,利用 html5 的 history
    • pushhistory 和 replacehistory 不会触发 popstate,back 和 forword 会触发
    • 不兼容低版本浏览器,需要后端匹配url,否则世界访问会 404
  • memory,node环境下,存储在 memory中

对应三大方法的实现:
createHashHistory

  • url 前进

    • window.location.hash = path;
    • window.location.replace(window.location.pathname + window.location.search + '#' + path);
  • url 后退

    • addEventListener(window, ‘hashchange’, hashChangeListener);

    createBrowserHistory

  • url 前进

    • window.history.pushState
    • window.history.replaceState
  • url 后退

    • addEventListener(window, ‘popstate’, popStateListener);

 createMemoryHistory 

  • url 前进
Javascript
switch (location.action) {
  case 'PUSH':
    entries.push(location);
    break;
  case 'REPLACE':
    entries[current] = location;
    break;
}
  • url 后退
Javascript
current += n
entries.pop()
Javascript
function createKey() {
  return Math.random()
    .toString(36)
    .substr(2, 8);
}

react-router 原理梳理

react-router 的核心组件包括 Route 、MemoryRouter (不涉及 dom 在 react-router 中定义),Router、Link、BrowserRouter、HistoryRouter(涉及到 dom 在 react-dom 中定义)

还有两个 context 组件 RouterContext 、HistoryContext 用于在组件中传递 router 和 history 相关的 props。

还有个 createNameContext 用于创建有displayName的context    
react-

router 基本原理如下:

react-router-basic

Route、Router、Link 三个组件即可构成一个基本 react-router应用

Router 组件

使用 route 时,在 route 之外会用 browserRouter HistoryRouter 包起来,他们是基于 router 组件实现的。

BrowserRouter、HistoryRouter、MemoryRouter 本质都是调用 Router 组件。

router 组件 调用 history 的 listen 监听 url 变化, 比较 url 是否 rootmatch 从而更新下级组件的 match 状态 props:

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

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

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

类似 a 标签,用于主动触发改变 url 的方法 。从 context 中拿到 history ,再根据 Props 有没有 replace 去执行 history.replace 和 history.push

Javascript
const Link = forwardRef(
  (
    {
      component = LinkAnchor,
      replace,
      to,
      innerRef, // TODO: deprecate
      ...rest
    },
    forwardedRef
  ) => {
    return (
      <RouterContext.Consumer>
        {context => {
          const { history } = context;

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

          const href = location ? history.createHref(location) : "";
          const props = {
            ...rest,
            href,
            navigate() {
              const location = resolveToLocation(to, context.location);
              const method = replace ? history.replace : history.push;

              method(location);
            }
          };

					...
          return React.createElement(component, props);
        }}
      </RouterContext.Consumer>
    );
  }
);

Route 组件

用于在 url 改变时判断如何更新视图:自身有props path,通过比较 path 和 url 。

比较的时候会做缓存,缓存上限 10000。

匹配成功则渲染 component props 、render props、 children props,排序即优先级:

Javascript
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
         	...
          const location = this.props.location || context.location;
          const match = this.props.computedMatch
            ? this.props.computedMatch // <Switch> already computed the match for us
            : this.props.path
            ? matchPath(location.pathname, this.props)
            : context.match;

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

          let { children, component, render } = this.props;

          // Preact uses an empty array as children by
          // default, so use null if that's the case.
          if (Array.isArray(children) && isEmptyChildren(children)) {
            children = null;
          }

          return (
            <RouterContext.Provider value={props}>
              {props.match
                ? children
                  ? typeof children === "function"
                    ? __DEV__
                      ? evalChildrenDev(children, props, this.props.path)
                      : children(props)
                    : children
                  : component
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? __DEV__
                  ? evalChildrenDev(children, props, this.props.path)
                  : children(props)
                : null}
            </RouterContext.Provider>
          );
        }}
      </RouterContext.Consumer>
    );
  }
}