The front-end routing

  • The concept of routing began in the back end, navigating to a specific HTML page from a URL requested by the user.
  • Today’s front-end routing is different from traditional routing in that it does not require server parsing and can be implemented using hash functions or the History API.
  • In front-end development, routes can be used to set up access paths and switch the display of components based on the mapping between paths and components
  • This whole process is implemented within the same page, and does not involve jumping between pages, which is often referred to as single-page application (SPA).

1, view the source code posture

1.1 Code Warehouse

Github.com/ReactTraini…

2.2 package

  • React-router Public base package
  • React-router-dom is used in browsers and depends on react-router
  • React-router-native is used in react-native and depends on the react-router

2.3 Source Location

  • React-router source code locationreact-router/packages/react-router/modules/
  • React-router-dom source locationreact-router/packages/react-router-dom/modules/

2.4 File Structure

  • HooksNo attention
  • <Link>
    • <NavLink>
  • <Prompt> No attention
  • <Redirect>
  • <Route>
  • <Router>
    • <HashRouter>
    • <BrowserRouter>
    • <MemoryRouter> No attention
    • <StaticRouter> No attention
  • <Switch>
  • generatePath No attention
  • history
  • location No attention
  • match No attention
  • matchPath No attention
  • withRouter

React-router source code analysis

3.1 Comparison of four Router source codes

<! --HashRouter-->// Delete some code
import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";
/** * The public API for a 
      
        that uses window.location.hash. */
      
class HashRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />; }}export default HashRouter;

Copy the code
<! --BrowserRouter-->// Delete some code
import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";
/** * The public API for a 
      
        that uses HTML5 history. */
      
class BrowserRouter extends React.Component {
  history = createHistory(this.props);
  render() {
    return <Router history={this.history} children={this.props.children} />; }}export default BrowserRouter;

Copy the code
<! --MemoryRouter-->// Delete some code
import React from "react";
import { createMemoryHistory as createHistory } from "history";
import Router from "./Router.js";
/** * The public API for a 
      
        that stores location in memory. */
      
class MemoryRouter extends React.Component {
  history = createHistory(this.props);

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

Copy the code
<! --StaticRouter-->// Delete some code
import React from "react";
import { createLocation, createPath } from "history";
import Router from "./Router.js";
/** * The public top-level API for a "static" 
      
       , so-called because it * can't actually change the current location. Instead, it just records * location changes in a context object. Useful mainly in testing and * server-rendering scenarios. */
      
class StaticRouter extends React.Component {
  render() {
    const { basename = "", context = {}, location = "/". rest } =this.props;
    const history = {
      createHref: path= > addLeadingSlash(basename + createURL(path)),
      action: "POP".location: stripBasename(basename, createLocation(location)),
      push: this.handlePush,
      replace: this.handleReplace,
      go: staticHandler("go"),
      goBack: staticHandler("goBack"),
      goForward: staticHandler("goForward"),
      listen: this.handleListen,
      block: this.handleBlock
    };
    return <Router {. rest} history={history} staticContext={context} />; }}export default StaticRouter;

Copy the code

Both are component-based implementations, the difference being passing different parameters. Focus on and

3.2 Source code Analysis

<! --Router-->// Delete some code
class Router extends React.Component {
  render() {
    return (
      <RouterContext.Provider
        value={{
          history: this.props.history.location: this.state.location.match: Router.computeRootMatch(this.state.location.pathname), / / onlyStaticRouterIn use, temporarily not consideredstaticContext: this.props.staticContext
        }}
      >{/*HistoryContext is used in hooks. */}<HistoryContext.Provider
          children={this.props.children || null}
          value={this.props.history}
        />
      </RouterContext.Provider>); }}Copy the code

The core function of the Router is to provide the following data

  • History, passed in by the parent component and generated by the history library
  • Location, calculated within the component
  • Match, generated by static method calculation within the component

3.3 Source Code Analysis

<! --Route-->// Delete some code
class Route extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>
        {context => {
          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>); }}Copy the code

Because three nested expressions are not easy to read, the code can be converted to

/* * 1, check if children(props) is the same as match (props), or if children(props) is the same as match (props), If children is a function, return children(props), or children * 2.2 does not exist. CreateElement (Component, props) * 3.2 does not exist, render(props) * 4.2 does not exist, null * */
getComponent(props, children, component, render) {
  if (props.match) {
    if (children) {
      if (typeof children === "function") {
        return children(props);
      } else {
        returnchildren; }}else {
      if (component) {
        return React.createElement(component, props);
      } else {
        if (render) {
          return render(props);
        } else {
          return null; }}}}else {
    if (typeof children === "function") {
      return children(props);
    } else {
      return null; }}}Copy the code

It can be seen from the source code that the core function of Reoute is to render components, and has the following features

  • Children > component > render
  • Children is a function that can display whether the match is matched or not
  • Children for components can only be displayed in matches
  • Component can only be a component
  • Render can only be a function

3.4 Source Code Analysis

<! --Switch-->// Delete some code
function Redirect({ computedMatch, to, push = false }) {
  const method = push ? history.push : history.replace;
  return (
    <RouterContext.Consumer>
      {context => {
        return (
          <Lifecycle
            onMount={()= > {
              method(location);
            }}
            onUpdate={(self, prevProps) => {
              method(location);
            }}
            to={to}
          />
        );
      }}
    </RouterContext.Consumer>
  );
}
Copy the code

Redirect is a Redirect to the page that is directed to.

3.5 Source Code Analysis

<! --Switch-->// Delete some code
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>{context => { const location = this.props.location || context.location; let element, match; // match the first Route React.children. ForEach (this.props. Children, child => { if (match == null && React.isValidElement(child)) { element = child; const path = child.props.path || child.props.from; match = path ? matchPath(location.pathname, { ... child.props, path }) : context.match; }}); // React elements are returned if matched, otherwise null return match? React.cloneElement(element, { location, computedMatch: match }) : null; }}</RouterContext.Consumer>); }}Copy the code

It can be seen from the source code that the core function of Switch is to render matching to the first Route component.

3.6 withRouter source code analysis

<! --withRouter-->// Delete some code
function withRouter(Component) {
  const C = props= > {
    const{ wrappedComponentRef, ... remainingProps } = props;return (
      // Pass the Router property to the Component via context. Consumer for enhancement purposes
      <RouterContext.Consumer>
        {context => {
          return (
            <Component
              {. remainingProps}
              {. context}
              ref={wrappedComponentRef}
            />
          );
        }}
      </RouterContext.Consumer>
    );
  };
  // Static property copy
  return hoistStatics(C, Component);
}
Copy the code

The withRouter’s core function is to pass Router attributes to the Component.

3.7 Source Code Analysis

<! --Link-->// Delete some code
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 location = normalizeToLocation(resolveToLocation(to, context.location), context.location); const href = location ? history.createHref(location) : ""; // Props data const props = {... rest, href, navigate() { const location = resolveToLocation(to, context.location); const method = replace ? history.replace : history.push; method(location); }}; // set forwardedRef props. Ref = callback // create and return the component. CreateElement (Component, props); }}</RouterContext.Consumer>); });Copy the code
<! --LinkAnchor-->// Delete some code
const LinkAnchor = forwardRef(
  (
    {
      innerRef, //TODO: deprecate navigate, onClick, ... rest }, forwardedRef) = > {
    const { target } = rest;

    // Assembly props data
    letprops = { ... rest,onClick: event= > {
        // Handle the click event
        try {
          if (onClick) onClick(event);
        } catch (ex) {
          event.preventDefault();
          throw ex;
        }
        // Execute the route jump event
        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(); }}};/ / set forwardedRef
    props.ref = forwardedRef;

    /* eslint-disable-next-line jsx-a11y/anchor-has-content */
    return <a {. props} / >; });Copy the code

Through the source code can be seen Link’s core functions are as follows

  • Custom with Component, otherwise default
  • The main function of the to attribute is to convert the href attribute
  • The main function isto mask the default click event, using history for route jump

3.8 Source Code Analysis

<! --NavLink-->// Delete some code
const NavLink = forwardRef(
  (
    {
      "aria-current": ariaCurrent = "page",
      activeClassName = "active",
      activeStyle,
      className: classNameProp,
      exact,
      isActive: isActiveProp,
      location: locationProp,
      sensitive,
      strict,
      style: styleProp,
      to,
      innerRef, //TODO: deprecate ... rest }, forwardedRef) = > {
    return (
      <RouterContext.Consumer>{context => {// Active according to isActive const isActive =!! (isActiveProp ? isActiveProp(match, currentLocation) : match); ClassName const className = isActive? joinClassnames(classNameProp, activeClassName) : classNameProp; // Set the inline style according to the isActive attribute const style = isActive? {... styleProp, ... activeStyle } : styleProp; / / data organization props const props = {" aria - the current ": (isActive && ariaCurrent) | | null, the className, style, to: toLocation,... rest }; // set the props of forwardedRef. return<Link {. props} / >;
        }}
      </RouterContext.Consumer>); });Copy the code

The core functions that can be seen through the source code are as follows

  • Allows you to customize the default and active state styles
  • IsActive has a higher priority than the default route matching

4. History source code analysis

4.1 createBrowserHistory

<! --createBrowserHistory-->// Delete some code
export function createBrowserHistory( options: BrowserHistoryOptions = {} ) :BrowserHistory {
  / / get the history
  let { window = document.defaultView! } = options;
  let globalHistory = window.history;


  window.addEventListener(PopStateEventType, handlePop);

  let action = Action.Pop;
  let [index, location] = getIndexAndLocation();
  let listeners = createEvents<Listener>();

  //pathname + search + hash
  function createHref(to: To) {
    return typeof to === 'string' ? to : createPath(to);
  }

  // To handle event subscriptions
  function applyTx(nextAction: Action) {
    action = nextAction;
    [index, location] = getIndexAndLocation();
    listeners.call({ action, location });
  }

  // Implement based on pushState method
  function push(to: To, state? : State) {
    let nextAction = Action.Push;
    let nextLocation = getNextLocation(to, state);
    function retry() {
      push(to, state);
    }
    if (allowTx(nextAction, nextLocation, retry)) {
      let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);

      // TODO: Support forced reloading
      // try... catch because iOS limits us to 100 pushState calls :/
      try {
        globalHistory.pushState(historyState, ' ', url);
      } catch (error) {
        // They are going to lose state here, but there is no real
        // way to warn them about it since the page will refresh...
        window.location.assign(url);
      }
      // To handle event subscriptionsapplyTx(nextAction); }}// Based on the replaceState method
  function replace(to: To, state? : State) {
    let nextAction = Action.Replace;
    let nextLocation = getNextLocation(to, state);
    function retry() {
      replace(to, state);
    }

    if (allowTx(nextAction, nextLocation, retry)) {
      let [historyState, url] = getHistoryStateAndUrl(nextLocation, index);

      // TODO: Support forced reloading
      globalHistory.replaceState(historyState, ' ', url);

      // To handle event subscriptionsapplyTx(nextAction); }}// Based on the go method
  function go(delta: number) {
    globalHistory.go(delta);
  }

  let history: BrowserHistory = {
    get action() {
      return action;
    },
    get location() {
      return location;
    },
    createHref,
    push,
    replace,
    go,
    back() {
      go(-1);
    },
    forward() {
      go(1);
    },
    listen(listener) {
      return listeners.push(listener);
    },
    block(blocker) {...}
  };

  return history;
}
Copy the code
  • Back and forward are implemented based on the GO method
  • Push is implemented based on the pushState method
  • Replace is implemented based on the replaceState method
  • Browser-specific (URL entry, forward, backward) events are handled by listening for popState events and then passed to the Route component as event subscriptions

4.2

  • CreateHashHistory with createBrowserHistory
  • CreateMemoryHistory with createBrowserHistory

Refer to the link