1. What is routing?

Routing is a very important part of Web application development. When the current URL of the browser changes, the routing system will make some responses to ensure that the user interface is synchronized with the URL. With the advent of single-page applications, third-party libraries have emerged to serve them. Mainstream frameworks have their own routes, such as React, Vue, Angular, etc. What are the optimisations of React – Router compared to other routing systems? How does he take advantage of React’s UI state mechanism? We’ll take a look at the React routing system by reading the source code.

2. The react – the router and the react – the router – dom

React-router-dom is a library that can be used to route routes to a router.

The React-Router is not only for React, but also for React-Native. If you open the Github repository of the React-Router, you can see many libraries

  • React-router provides some core ROUTER apis, including Router, Route, Switch, etc., but it does not provide AN API for dom operations to jump.
  • React-router-dom is used as the routing system of the Browser, and depends on the react-router
  • React-router-native is used in the routing system of RN and depends on the react-router
  • React-router-config is a plugin that allows you to manage routes using react-router-config

In fact, the react-router-DOM is specially used in Browser. It relies on the React-Router, inherits all components of the React-Router internally, and encapsulates several more components on the basis of the React-Router

3. Basic use

The first step is to define the routing table

import {Redirect} from '.. /node_modules_copy/react-router-dom/modules';// Import the redirection component
import RouterPage from '.. /pages/router/Router';
import RouterListPage from '.. /pages/router/pages/list/List';
import RouterDetailPage from '.. /pages/router/pages/detail/Detail';

// src/router/index.js
// Define the routing table
export default[{path: '/router'.component: RouterPage,
        title: 'Simulated root routing'.routes: [{path: '/router/list'.component: RouterListPage,
                exact: true.title: 'list'}, {path: '/router/detail'.component: RouterDetailPage,
                exact: true.title: 'details',}]}, {path: '/'.// Redirects to the '/router' route when the path matches '/'
        component: () = > <Redirect to='/router'/>},]Copy the code

The second step imports and builds the route in app.js

import {BrowserRouter,HashRouter} from './node_modules_copy/react-router-dom/modules';
import {renderRoutes} from './node_modules_copy/react-router-config/modules'; // Introduce the renderRoutes method
import routers from './router';  // Import the routing table we just defined

//BrowserRouter History Specifies the routing mode
//HashRouter Hash routing mode
//renderRoutes is used to render our routing table

function App() {
    return (
        <BrowserRouter>{/* Render the routing table, the second parameter can be added to the props*/} {renderRoutes(routers,{value:' from above '})}</BrowserRouter>
    );
}

export default App;
Copy the code

The third step is to define the page

/ / / SRC/pages/router, the router. The simulation with js routing page

import React, {Component} from 'react';
import {NavLink} from ".. /.. /node_modules_copy/react-router-dom/modules";
import {renderRoutes, matchRoutes} from ".. /.. /node_modules_copy/react-router-config/modules";
const navList = [
    {
        to: '/router/list'.title: 'Go to list'
    },
    {
        to: '/router/detail'.title: 'To details'}]const styles = {
      padding: "20px".margin: "20px".width: "300px".border: "solid 1px #ccc"
}
class RouterPage extends Component {
    render() {
        return (
            <div>
                <ul>
                    {
                        navList.map((item, index) => {
                            return <li key={index}>
                                <NavLink
                                    to={item.to}
                                    activeStyle={{color: 'red'}} >
                                    {item.title}
                                </NavLink>
                            </li>})}</ul>
                <div className={'container'} style={styles}>
                    {renderRoutes(this.props.route.routes)}
                </div>
            </div>); }}export default RouterPage;


/ / / SRC/pages/router/pages/list list page

const List = () = > {
    return (
        <div>
            <h1>list</h1>
        </div>
    );
};

export default List;

/ / / SRC/pages/router/pages/detail page for details


const Detail = () = > {
    return (
        <div>
            <h1>detail</h1>
        </div>
    );
};

export default Detail;


Copy the code

The final result

4. Analyze the react – the router – the dom

Entrance to the file

export {
  MemoryRouter,
  Prompt,
  Redirect,
  Route,
  Router,
  StaticRouter,
  Switch,
  generatePath,
  matchPath,
  withRouter,
  useHistory,
  useLocation,
  useParams,
  useRouteMatch
} from ".. /.. /react-router/modules";  // All these components are taken from the React-router without any processing

export { default as BrowserRouter } from "./BrowserRouter.js";  // Use this component if you want to use hash routing mode
export { default as HashRouter } from "./HashRouter.js";  // Use this component if you want to use the history routing pattern
export { default as Link } from "./Link.js"; // The Link component, which is actually the a tag, is used to jump to the page
export { default as NavLink } from "./NavLink.js";  // The NavLink component is used for navigation
Copy the code

React-route-dom has all of the react-router methods built in

  • BrowserRouter history routing
  • HashRouter hash routing
  • Link Redirects the components of the page
  • NavLink Navigation component of the jump page

4.1 BrowserRouter and HashRouter

//react-router-dom/modules/utils/BrowserRouter.js

import React from "react";
import { Router } from ".. /.. /react-router/modules";
import { createBrowserHistory as createHistory } from ".. /.. /history";
/** * The public API for a 
      
        that uses HTML5 history. */
      
// Introduce the createBrowserHistory method from history to generate the history object
// Import the Router and route component of the React-Router and pass it the history object
class BrowserRouter extends React.Component {
  history = createHistory(this.props); // Generate the history object

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


//react-router-dom/modules/utils/HashRouter.js

import React from "react";
import { Router } from ".. /.. /react-router/modules";
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

The code for BrowserRouter and HashRouter is exactly the same, except that the methods introduced are createHashHistory and createBrowserHistory, So what’s the difference between these two methods? That’s not what we’re talking about here, so I’m not going to go into it, but if you’re interested, take a look at the history library

4.2 the Link

// react-router-dom/modules/Link.js

import React from "react";
import {__RouterContext as RouterContext} from ".. /.. /react-router/modules"; // Import Context from the react-router
import {
    resolveToLocation,
    normalizeToLocation
} from "./utils/locationUtils.js";

// Compatible with React 15
const forwardRefShim = C= > C;
let {forwardRef} = React;
if (typeof forwardRef === "undefined") {
    forwardRef = forwardRefShim;
}


function isModifiedEvent(event) {
    return!!!!! (event.metaKey || event.altKey || event.ctrlKey || event.shiftKey); }const LinkAnchor = forwardRef(
    (
        {
            innerRef, //TODO: deprecate navigate, onClick, ... rest }, forwardedRef) = > {
        const {target} = rest;
        letprops = { ... rest,onClick: event= > {
                try {
                    //try catch to catch the click error
                    if (onClick) onClick(event);
                } catch (ex) {
                    event.preventDefault(); // Block the default event
                    throw ex;
                }
                if (
                    !event.defaultPrevented && // Determine whether the default event is blocked
                    event.button === 0 && // The judgment must be left click(! target || target ==="_self") && // Let the browser handle "target=_blank"! isModifiedEvent(event)// Ignore the click with the modify key
                ) {
                    event.preventDefault();  // Block the default event
                    navigate();  / / jump}}};// Compatible with React 15
        if(forwardRefShim ! == forwardRef) { props.ref = forwardedRef || innerRef; }else {
            props.ref = innerRef;
        }

        return <a {. props} / >; });/** * The public API for rendering a history-aware . */
const Link = forwardRef(
    (
        {
            component = LinkAnchor,
            replace,
            to,
            innerRef, //TODO: deprecate ... rest }, forwardedRef) = > {
        return (
            <RouterContext.Consumer>{context => {// Deconstruct the history object const {history} = context; // Still create location object, Const location = normalizeToLocation(resolveToLocation(to, context.location), context.location ); // Const href = location? history.createHref(location) : ""; const props = { ... Rest, href, // Since the default events for the a tag are blocked, the default href jump for the A tag will not take effect. Const location = resolveToLocation(to, context.location); const method = replace ? history.replace : history.push; // Jump to the method method(location); }}; React 15 if (forwardRefShim! == forwardRef) { props.ref = forwardedRef || innerRef; } else { props.innerRef = innerRef; } // Create the React component and return React. CreateElement (Component, props); }}</RouterContext.Consumer>); });export default Link;


// react-router-dom/modules/utils/locationUtils.js

import { createLocation } from "history";

export const resolveToLocation = (to, currentLocation) = >
  typeof to === "function" ? to(currentLocation) : to;

export const normalizeToLocation = (to, currentLocation) = > {
  return typeof to === "string"
    ? createLocation(to, null.null, currentLocation)
    : to;
};
Copy the code

4.3 NavLink

// react-router-dom/modules/NavLink.js

import React from "react";
import {__RouterContext as RouterContext, matchPath} from ".. /.. /react-router/modules";
import Link from "./Link.js"; // Import the Link component
import {
    resolveToLocation,
    normalizeToLocation
} from "./utils/locationUtils.js";

// React 15 compatible
const forwardRefShim = C= > C;
let {forwardRef} = React;
if (typeof forwardRef === "undefined") {
    forwardRef = forwardRefShim;
}

//合并class
function joinClassnames(. classnames) {
    return classnames.filter(i= > i).join("");
}

/** * A 
       wrapper that knows if it's "active" or not. */
const NavLink = forwardRef(
    (
        {
            "aria-current": ariaCurrent = "page",
            activeClassName = "active".//When the route matchesclassActiveStyle,//Style className: classNameProp when the route matches,//class
            exact,
            isActive: isActiveProp, //Does location: locationProp match,//Location object sensitive, strict, style: styleProp,//style
            to,
            innerRef, //TODO: deprecate ... rest }, forwardedRef) = > {
        return (
            <RouterContext.Consumer>{the context = > {/ / is still get location object const currentLocation = locationProp | | context. The location; // Location object to jump to, Hops use it const toLocation = normalizeToLocation(resolveToLocation(to, currentLocation), currentLocation); const {pathname: path} = toLocation; / / translating special symbol path const escapedPath = path && path. The replace (/ ([. + *? = ^! : ${} () [] | / \])/g, "\ $1"); Const match = escapedPath? matchPath(currentLocation.pathname, { path: escapedPath, exact, sensitive, strict }) : null; Const isActive =!! (isActiveProp ? isActiveProp(match, currentLocation) : match); // If they match, merge the two classes. If they do not match, only classNameProp const className = isActive? joinClassnames(classNameProp, activeClassName) : classNameProp; StyleProp const style = isActive? StyleProp const style = isActive? {... styleProp, ... activeStyle} : styleProp; Const props = {// Use the "aria-current" attribute to check whether the current route matches: (isActive && ariaCurrent) | | null, the className, / / a class style, / / style: toLocation, / / jump to the location of the object... rest }; React 15 if (forwardRefShim! == forwardRef) { props.ref = forwardedRef || innerRef; } else { props.innerRef = innerRef; } return<Link {. props} / >;
                }}
            </RouterContext.Consumer>); });export default NavLink;
Copy the code

5. Analyze the react – the router

Entrance to the file

// react-router/modules/index.js

export { default as MemoryRouter } from "./MemoryRouter.js";
export { default as Prompt } from "./Prompt.js";
export { default as Redirect } from "./Redirect.js";
export { default as Route } from "./Route.js";
export { default as Router } from "./Router.js";
export { default as StaticRouter } from "./StaticRouter.js";
export { default as Switch } from "./Switch.js";
export { default as generatePath } from "./generatePath.js";
export { default as matchPath } from "./matchPath.js";
export { default as withRouter } from "./withRouter.js";
import { useHistory, useLocation, useParams, useRouteMatch } from "./hooks.js";
export { useHistory, useLocation, useParams, useRouteMatch };
export { default as __HistoryContext } from "./HistoryContext.js";
export { default as __RouterContext } from "./RouterContext.js";
Copy the code

5.1 the Router

// react-router/modules/Router.js

import React from "react";
import HistoryContext from "./HistoryContext.js";  //HistoryContext
import RouterContext from "./RouterContext.js"; //RouterContext

/** * The public API for putting history on context. */
class Router extends React.Component {
    //computeRootMatch is the default value for the match object, which is used by other components as well. If nothing matches, the default value will be used
    static computeRootMatch(pathname) {
        return {path: "/".url: "/".params: {}, isExact: pathname === "/"};
    }

    constructor(props) {
        super(props);

        this.state = {
            location: props.history.location // Define the accident location object
        };

        // This is a trick. We need to start monitoring the location
        // If there is any 
      
       , change here in the constructor
      
        // In the initial render. If so, they will replace/push
        // Since cDM occurs in children before parents, we may
        // Get a new location before 
      
        is mounted.
      
        this._isMounted = false; // It is used to determine whether the first rendering is complete
        this._pendingLocation = null; // Hold the location object temporarily
        if(! props.staticContext) {// staticContext is undefined by default and will only have a value if rendered on the server
            // When the route changes, update it with setState. All components of the context package are updated
            this.unlisten = props.history.listen(location= > {
                if (this._isMounted) { // If the first rendering is complete, use setState
                    this.setState({location});
                } else { // If the rendering is not complete for the first time, cache the location first, in order to reduce unnecessary updates
                    this._pendingLocation = location; }}); }}componentDidMount() {
        this._isMounted = true; // The flag is not initialized
        if (this._pendingLocation) { // After initialization, determine if there is a cached location, and update immediately if there is
            this.setState({location: this._pendingLocation}); }}componentWillUnmount() {
        // Destroy the listener here
        if (this.unlisten) this.unlisten();
    }

    render() {
        return (
            // Use context to pass attributes down
            <RouterContext.Provider
                value={{
                    history: this.props.history.location: this.state.location.match: Router.computeRootMatch(this.state.location.pathname),
                    staticContext: this.props.staticContext
                }}
            >{/* Use context to cache the history object, because useHistory is useful */}<HistoryContext.Provider
                    children={this.props.children || null}
                    value={this.props.history}
                />
            </RouterContext.Provider>); }}export default Router;
Copy the code

The Router is also simple to do

The first isto use The RouterContext to pass down properties like the History object and the Location object, and use the HistoryContext to save the history object, because other components will use it and it’s not wrapped by the RouterContext, so you need to cache it

The second is to listen for route changes to get the new location and use setState to trigger updates, so that all components in the RouterContext will also be updated, and the components will be matched according to the route, so that the page will respond to the route changes

5.2 the Switch

// react-router/modules/Switch.js

import React from "react";
RouterContext caches the history, location and other properties of the RouterContext. RouterContext caches the history, location and other properties of the RouterContext
import RouterContext from "./RouterContext.js";
import matchPath from "./matchPath.js";

/** * The public API for rendering the first 
      
        that matches. */
      
class Switch extends React.Component {
  render() {
    return (
      <RouterContext.Consumer>{the context = > {/ / location object, there are pick up props in the props, no is in the context of the const location = this. Props. The location | | context. The location; let element, match; ForEach (this.props. Children, props. Children, props. Child => {// Check that match is null and child is a React component. Note that if the match object has a value, it will not be matched. If (match == null && React.isValidElement(child)) {element = child; / / element with the value of the cache child outside loop will use const path = child. Props. The path | | child. Props. The from; Match = path; // Match = path; // Match = path; matchPath(location.pathname, { ... Child-.props, path}) // Use the default value of match cached in context, equivalent to router.puterootMatch (location.pathName) : context.match; }}); // To ensure that the original component is not affected, clone a new component using cloneElement. React.cloneElement(element, { location, computedMatch: match }) : null; }}</RouterContext.Consumer>); }}export default Switch;



// react-router/modules/matchPath.js

import pathToRegexp from "path-to-regexp"; // A library used to generate regeds
const cache = {};  // The object used to cache compilePath results
const cacheLimit = 10000; // Maximum number of caches
let cacheCount = 0; // The number of times cached

function compilePath(path, options) {
  // Use end, strict, and sensitive as the cache key
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  // cache[cacheKey] takes its value if it has a value, and assigns {} if it doesn't
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});
  // If pathCache[path] has a value, return it directly
  if (pathCache[path]) return pathCache[path];
  // Match the array of keywords, such as params parameters, it can help us match
  const keys = [];
  // The return value of pathToRegexp is a re
  const regexp = pathToRegexp(path, keys, options);
  / / the result
  const result = { regexp, keys };
  // Check that the number of caches does not exceed the maximum value
  if (cacheCount < cacheLimit) {
    // Continue caching results
    pathCache[path] = result;
    // The counter increases
    cacheCount++;
  }
  / / return the result
  return result;
}

/** * Public API for matching a URL pathname to a path. */
function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) { // Switch matchPath does not enter if
    options = { path: options };
  }
  const { path, exact = false, strict = false, sensitive = false } = options;  // Retrieve path, exact, and other related attributes from options
  const paths = [].concat(path); // Make a deep copy
  return paths.reduce((matched, path) = > { // Return the final match result with an initial value of null
    if(! path && path ! = ="") return null; // Path does not exist or path! =="" returns null

    if (matched) return matched; //matched the initial value is null, and the return value will be assigned to it if it has a value

    const { regexp, keys } = compilePath(path, {
      end: exact, // Whether the re matches to the end of the string
      strict, // Whether the match is strictly matched
      sensitive // Is case sensitive
    });
    const match = regexp.exec(pathname); Exec matches the current pathname with an array
    if(! match)return null; // If the match fails, null is returned

    const [url, ...values] = match; // Structure the first value of the array
    const isExact = pathname === url; // Check whether the URL and the current route match exactly

    if(exact && ! isExact)return null; // If isExact is not false, return null

    return {
      path, // The path to match
      url: path === "/" && url === "" ? "/" : url, // The matching part of the URL
      isExact, // Whether it is an exact match
      //keys is the params parameter, but it is an Array. It is treated as an Object for convenience
      params: keys.reduce((memo, key, index) = > {
        memo[key.name] = values[index];
        returnmemo; }, {}}; },null);
}

export default matchPath;
Copy the code

After looking at the code above, the essence of the Switch code is react.children. ForEach, which is very flexible to match the route we should render and does a lot of good optimizations inside the loop

5.3 Redirect

// react-router/modules/Redirect.js

import React from "react";
// createLocation creates a location object, which can only take a string or an object, which it handles differently
// locationsAreEqual checks whether two Location objects are the same
import { createLocation, locationsAreEqual } from ".. /.. /history/esm/history";
import Lifecycle from "./Lifecycle.js"; // This is a component, as described below
import RouterContext from "./RouterContext.js";  // Use the history, location, and other attributes cached by the RouterContext
import generatePath from "./generatePath.js"; // The method used to merge pathName and params together

/** * The public API for navigating programmatically with a component. */
/** * computedMatch is a match object that is passed when the Switch wraps it. * to route to redirect * push jump */
function Redirect({ computedMatch, to, push = false }) {
  return (
    <RouterContext.Consumer>{context => {const {staticContext} = context; Const method = push; // Redirect Redirect () const method = push history.push : history.replace; Const location = createLocation(computedMatch? typeof to === "string" ? generatePath(to, computedMatch.params) : { ... to, pathname: generatePath(to.pathname, computedMatch.params) } : to ); // Immediately set the new position when rendering in a static context. if (staticContext) { method(location); return null; } return (<Lifecycle
              //onMountIs actuallycomponentDidMountThe life cycleonMount={()= >{ method(location); OnUpdate ={(self, prevProps) => {/* * If (props) => {/* * If (props) => {/* * If (props) => { * */ const prevLocation = createLocation(prevProps. To); if ( ! locationsAreEqual(prevLocation, { ... location, key: prevLocation.key }) ) { method(location); }}} to={to}; }}</RouterContext.Consumer>
  );
}

export default Redirect;


// react-router/modules/Lifecycle.js

import React from "react";

/** * This is just a very different class component. It returns NULL. The React-router makes proper use of the class lifecycle */
class Lifecycle extends React.Component {
  componentDidMount() {
    if (this.props.onMount) this.props.onMount.call(this.this);
  }

  componentDidUpdate(prevProps) {
    if (this.props.onUpdate) this.props.onUpdate.call(this.this, prevProps);
  }

  componentWillUnmount() {
    if (this.props.onUnmount) this.props.onUnmount.call(this.this);
  }

  render() {
    return null; }}export default Lifecycle;
Copy the code

5.4 withRouter

// react-router/modules/withRouter.js

import React from "react";
import hoistStatics from "hoist-non-react-statics";  // is a library that inherits the static properties of the constructor
import RouterContext from "./RouterContext.js"; // We still introduce the RouterContext that caches history, Location, and other objects

/** * A public higher-order component to access the imperative API */
function withRouter(Component) {
    const displayName = `withRouter(${Component.displayName || Component.name}) `;
    const C = (props) = > {
        const{wrappedComponentRef, ... remainingProps} = props;return (
            <RouterContext.Consumer>
                {context => {
                    return (
                        <Component
                            {. remainingProps} / /propsAttributes are returned unchanged in {. context} / /contextThere arehistory,locationEqual-correlation attributeref={wrappedComponentRef}/ / to preventrefLost / >
                    );
                }}
            </RouterContext.Consumer>
        );
    };

    C.displayName = displayName;
    C.WrappedComponent = Component;


    return hoistStatics(C, Component);  // Returns a C function that inherits static properties from Component to prevent static property loss
}

export default withRouter;
Copy the code

The withRouter is essentially a high-level component that blends parameters like history and location into props and returns the new component

6. Analyze the react – the router – config

Entrance to the file

// react-router-config/modules/index.js

export { default as matchRoutes } from "./matchRoutes";
export { default as renderRoutes } from "./renderRoutes";
Copy the code

He only has two apis

  • RenderRoutes is used to render our routing table and is handy to use
  • MatchRoutes gets the entries that the current route matches in the routing table (not just the routing table). It is very convenient to use it to eliminate crumbs

6.1 renderRoutes

// react-router-config/modules/renderRoutes.js
// renderRoutes does a very simple thing: use the Switch wrap to render the Route

import React from "react";
import {Switch, Route} from ".. /.. /react-router/[modules](url)"; // Introduce Switch and Route components

function renderRoutes(routes, extraProps = {}, switchProps = {}) {
    return routes ? (
        <Switch {. switchProps} >
            {
                routes.map((route, i) => {
                        return <Route
                            key={route.key || i}
                            path={route.path}
                            exact={route.exact}
                            strict={route.strict}
                            render={props= >// Which method to use to render route.render? ( route.render({... props, ... extraProps, route: route}) ) : (<route.component {. props} {. extraProps} route={route}/>} />})}</Switch>
    ) : null;
}

export default renderRoutes;
Copy the code

6.2 matchRoutes

// react-router-config/modules/matchRoutes.js
// matchRoutes can help me match the entries in the routing table with the current route

import {matchPath, Router} from ".. /.. /react-router/modules";


/** * routes Routing table * pathName Route to be queried * Default branch value ** /
function matchRoutes(routes, pathname, branch = []) {
    /*** * loop through the routing table to match the match and obtain the match in several ways. If not, use Router.computeRootMatch (initial value) */
    routes.some(route= > {
        const match = route.path
            ? matchPath(pathname, route)
            : branch.length
                ? branch[branch.length - 1].match // use parent match
                : Router.computeRootMatch(pathname); // use default "root" match
        if (match) {
            / / push in
            branch.push({route, match});
            // Determine if there is a next level to recurse the query
            if(route.routes) { matchRoutes(route.routes, pathname, branch); }}return match;
    });

    return branch;
}

export default matchRoutes;
Copy the code