@reach/router源码分析

1,561 阅读2分钟

文章首发于语雀 @reach/router源码分析

欢迎来访,每周一篇原创文章,前端好多云这里不仅仅有前端,还有其他互联网上,随笔杂想等

未经允许严禁转载

@reach/router是一个react路由控件,是由前React-Router成员Ryan Florence开发的,对比一下react-router,就可得知:

  • 尺寸更小
  • 没有复杂的路由模式
  • 特别好用
  • 但不支持RN

接下来,就开始分析了。首页先讲述一下会有到的几个工具方法的作用。

resolve:合并目标url与基本url返回新url
pick:根据pathname找到匹配的路由组件
insertParams:将参数与当前路径匹配得到最后的路径
match:只匹配到uri的一条路径

有易到难,逐个分析各个API。首先讲述一下history。

API

history

顾名思义,这是用来保存浏览历史的。

@reach/router提供一个4个关于history的API,分别是globalHistory,navigate,createHistory,createMemorySource。

globalHistory是用来表示整个应用的全局history,是由函数createHistory执行生成的结果。navigate是它的一个方法,用来控制导航的。

let canUseDOM = !!(
  typeof window !== "undefined" &&
  window.document &&
  window.document.createElement
);
// 通过canuseDOM判断是否可以使用浏览器的window,否则创建history所需要的属性
let getSource = () => {
  return canUseDOM ? window : createMemorySource();
};
let globalHistory = createHistory(getSource());
let { navigate } = globalHistory;

从上面的代码中能看出,宿主环境如果是浏览器则用window.history,而如果不是,则自定义了hisotry。

createMemorySource

let createMemorySource = (initialPath = "/") => {
  // 得到基本路径和搜索路径
  let searchIndex = initialPath.indexOf("?");
  let initialLocation = {
    pathname:
      searchIndex > -1 ? initialPath.substr(0, searchIndex) : initialPath,
    search: searchIndex > -1 ? initialPath.substr(searchIndex) : ""
  };
  // 当前浏览记录在栈中的索引
  let index = 0;
  // 浏览历史栈 
  let stack = [initialLocation];
  let states = [null];
  // 模拟window关于history和location的一些方法
  return {
    get location() {
      return stack[index];
    },
    addEventListener(name, fn) {},
    removeEventListener(name, fn) {},
    history: {
      get entries() {
        return stack;
      },
      get index() {
        return index;
      },
      get state() {
        return states[index];
      },
      pushState(state, _, uri) {
        let [pathname, search = ""] = uri.split("?");
        index++;
        stack.push({ pathname, search: search.length ? `?${search}` : search });
        states.push(state);
      },
      replaceState(state, _, uri) {
        let [pathname, search = ""] = uri.split("?");
        stack[index] = { pathname, search };
        states[index] = state;
      }
    }
  };
};

createHistory

用来定义使用@reach/router的关于history方法。

React.Context
很重要,在@reach/router中大量使用到React.Context,具体的使用可以参考文档。下面介绍两个Context。

LocationContext

let LocationContext = createNamedContext("Location");

let Location = ({ children }) => (
  <LocationContext.Consumer>
    // 有contenxt则直接使用,否则使用组件LocationProvider
    {context =>
      context ? (
        children(context)
      ) : (
        <LocationProvider>{children}</LocationProvider>
      )
    }
  </LocationContext.Consumer>
);

BaseContext

// 为嵌套路由器和链接设置baseuri和basepath
let BaseContext = createNamedContext("Base", { baseuri: "/", basepath: "/" });
// 创建组件Router,包裹了Location组件
let Router = props => (
  <BaseContext.Consumer>
    {baseContext => (
      <Location>
        {locationContext => (
          <RouterImpl {...baseContext} {...locationContext} {...props} />
        )}
      </Location>
    )}
  </BaseContext.Consumer>
);

createNamedContext

const createNamedContext = (name, defaultValue) => {
  // createContext来源于create-react-context
  const Ctx = createContext(defaultValue);
  Ctx.Consumer.displayName = `${name}.Consumer`;
  Ctx.Provider.displayName = `${name}.Provider`;
  return Ctx;
};

Link

// 如果没有forwardRef,那么forwardRef的作用就是单纯的输入输出 
let { forwardRef } = React;
if (typeof forwardRef === "undefined") {
  forwardRef = C => C;
}

let Link = forwardRef(({ innerRef, ...props }, ref) => (
  <BaseContext.Consumer>
    // BaseContext的默认值
    {({ basepath, baseuri }) => (
      // 上文的Location组件,因为没有contenxt,所以还是走了LocationProvider的那条路径
      <Location>
        {({ location, navigate }) => {
          let { to, state, replace, getProps = k, ...anchorProps } = props;
  				// 得到最终的路径
          let href = resolve(to, baseuri);
          // 将字符串做为uri进行编码
          let encodedHref = encodeURI(href);
          let isCurrent = location.pathname === encodedHref;
  				// 是否部分相同
          let isPartiallyCurrent = startsWith(location.pathname, encodedHref);

          return (
            <a
              ref={ref || innerRef}
              aria-current={isCurrent ? "page" : undefined}
              {...anchorProps}
              {...getProps({ isCurrent, isPartiallyCurrent, href, location })}
              href={href}
              onClick={event => {
                // 有onClick,则执行onClick,否则通过navigate导航
                if (anchorProps.onClick) anchorProps.onClick(event);
                if (shouldNavigate(event)) {
                  event.preventDefault();
                  navigate(href, { state, replace });
                }
              }}
            />
          );
        }}
      </Location>
    )}
  </BaseContext.Consumer>
));

Redirect

let Redirect = props => (
  <BaseContext.Consumer>
    {({ baseuri }) => (
      <Location>
        {locationContext => (
          <RedirectImpl {...locationContext} baseuri={baseuri} {...props} />
        )}
      </Location>
    )}
  </BaseContext.Consumer>
);

function RedirectRequest(uri) {
  this.uri = uri;
}

let redirectTo = to => {
  throw new RedirectRequest(to);
};

class RedirectImpl extends React.Component {
  // Support React < 16 with this hook
  componentDidMount() {
    let {
      props: {
        navigate,
        to,
        from,
        replace = true,
        state,
        noThrow,
        baseuri,
        ...props
      }
    } = this;
    Promise.resolve().then(() => {
      let resolvedTo = resolve(to, baseuri);
      // 重定向到目标路径
      navigate(insertParams(resolvedTo, props), { replace, state });
    });
  }

  render() {
    let {
      props: { navigate, to, from, replace, state, noThrow, baseuri, ...props }
    } = this;
    let resolvedTo = resolve(to, baseuri);
    // noThrow为false,可以用componentDidCatch捕获error,放置渲染失败
    if (!noThrow) redirectTo(insertParams(resolvedTo, props));
    return null;
  }
}

Match

let Match = ({ path, children }) => (
  <BaseContext.Consumer>
    {({ baseuri }) => (
      <Location>
        {({ navigate, location }) => {
          let resolvedPath = resolve(path, baseuri);
  				// 是否匹配根据当前路径与定义的match路径
          let result = match(resolvedPath, location.pathname);
  				// 渲染
          return children({
            navigate,
            location,
            match: result
              ? {
                  ...result.params,
                  uri: result.uri,
                  path
                }
              : null
          });
        }}
      </Location>
    )}
  </BaseContext.Consumer>
);

下面说说最重要的两个组件,一个是Location,另一个则是Router。

Location

Typically you only have access to the location in Route Components, Location provides the location anywhere in your app with a child render prop.
翻译一下:
通常你只能访问路由组件中的位置,Location可以在你的应用程序的任何地方提供定位并渲染子组件。

let Location = ({ children }) => (
  <LocationContext.Consumer>
    {context =>
     // 有contenxt则直接使用,否则使用组件LocationProvider渲染
     // 只有在服务端渲染的情况下,会使用到第一种,直接渲染
      context ? (
        children(context)
      ) : (
        <LocationProvider>{children}</LocationProvider>
      )
    }
  </LocationContext.Consumer>
);

class LocationProvider extends React.Component {
  static propTypes = {
    history: PropTypes.object.isRequired
  };
	// 默认props有一个history属性
  static defaultProps = {
    history: globalHistory
  };

  state = {
    context: this.getContext(),
    refs: { unlisten: null }
  };
	// 自定义context
  getContext() {
    let {
      props: {
        history: { navigate, location }
      }
    } = this;
    return { navigate, location };
  }
	// 捕获重定向的error,然后重定向到该error发生的uri
  componentDidCatch(error, info) {
    if (isRedirect(error)) {
      let {
        props: {
          history: { navigate }
        }
      } = this;
      navigate(error.uri, { replace: true });
    } else {
      throw error;
    }
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.context.location !== this.state.context.location) {
      this.props.history._onTransitionComplete();
    }
  }

  componentDidMount() {
    let {
      state: { refs },
      props: { history }
    } = this;
    history._onTransitionComplete();
    // 监听history的popstate事件,并返回一个函数用来取消popstate事件
    refs.unlisten = history.listen(() => {
      Promise.resolve().then(() => {
        // 重绘之前调用该回调,并且只有在未卸载的情况下,得到当前的Context
        requestAnimationFrame(() => {
          if (!this.unmounted) {
            this.setState(() => ({ context: this.getContext() }));
          }
        });
      });
    });
  }

  componentWillUnmount() {
    let {
      state: { refs }
    } = this;
    // 已卸载
    this.unmounted = true;
    // 清除监听器
    refs.unlisten();
  }

  render() {
    let {
      state: { context },
      props: { children }
    } = this;
    return (
      <LocationContext.Provider value={context}>
        {typeof children === "function" ? children(context) : children || null}
      </LocationContext.Provider>
    );
  }
}

Router

路由组件,不需要再来个Route,仅仅把需要渲染的组件放入其中,再搭配pathname属性就可以了。

// 创建组件Router,包裹了Location组件
let Router = props => (
  <BaseContext.Consumer>
    {baseContext => (
      <Location>
        {locationContext => (
          <RouterImpl {...baseContext} {...locationContext} {...props} />
        )}
      </Location>
    )}
  </BaseContext.Consumer>
);

class RouterImpl extends React.PureComponent {
  static defaultProps = {
    primary: true
  };

  render() {
    let {
      location,
      navigate,
      basepath,
      primary,
      children,
      baseuri,
      component = "div",
      ...domProps
    } = this.props;
    let routes = React.Children.toArray(children).reduce((array, child) => {
      // 判断并创建route
      const routes = createRoute(basepath)(child);
      if (routes instanceof Array) {
        return array.concat(routes);
      } else {
        array.push(routes);
        return array;
      }
    }, []);
    let { pathname } = location;
		// 根据pathname匹配route
    let match = pick(routes, pathname);

    if (match) {
      let {
        params,
        uri,
        route,
        route: { value: element }
      } = match;

      // remove the /* from the end for child routes relative paths
      basepath = route.default ? basepath : route.path.replace(/\*$/, "");
			
      let props = {
        ...params,
        uri,
        location,
        navigate: (to, options) => navigate(resolve(to, uri), options)
      };
			// 得到路由组件
      let clone = React.cloneElement(
        element,
        props,
        element.props.children ? (
          <Router location={location} primary={primary}>
            {element.props.children}
          </Router>
        ) : (
          undefined
        )
      );

      // using 'div' for < 16.3 support
      // FocusWrapper的作用的,是使用div还是FocusHandler包裹渲染
      let FocusWrapper = primary ? FocusHandler : component;
      // don't pass any props to 'div'
      let wrapperProps = primary
        ? { uri, location, component, ...domProps }
        : domProps;

      return (
        <BaseContext.Provider value={{ baseuri: uri, basepath }}>
          <FocusWrapper {...wrapperProps}>{clone}</FocusWrapper>
        </BaseContext.Provider>
      );
    } else {
      return null;
    }
  }
}

至此,一些重要API已经分析好了,但还是有部分是遗漏的,有兴趣的还可以自行去看源码。

总结

大致来说,整个源码并不是很复杂,有耐心的小伙伴能看完。
说一下我的感受,在结合使用与源码解读的基础上,确实简单,使用简单,阅读简单。
但要去理解作者为什么这么去设计,为什么这样去写,这些还是需要我们去学习的。