Source code series:

  • Redux-chunk, Redux-Promise source code analysis
  • Redux v4. X source code analysis

This article is mainly about the analysis of the React-Router source code, version is V5. x, and SPA routing implementation principle.

Single-page applications all use routing routers. Currently, there are two ways to implement routing: Hash routing and H5 History API implementation.

The React-router route uses the history library, which encapsulates hash, history, and memory routes (clients).

Let’s take a look at how hash and history implement routing.

hash router

Hash is an attribute of location, which is the part of the URL after the #. If no # is present, an empty string is returned.

Hash routing is implemented as follows: When the hash is changed, the page does not jump. That is, the Window object can listen for the hashchange (hashchange event) and trigger a callback whenever the HASH in the URL changes.

Using this feature, the following simulation implements a hash route.

Implementation steps:

  • Initialize a class
  • Record historical values of routes and history
  • Add a Hashchange event that triggers a callback when the hash changes
  • All routes are added initially
  • Simulate forward and backward functions

The code is as follows:

hash.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>hash router</title>
</head>
<body>
  <ul>
    <li><a href="# /">turn yellow</a></li>
    <li><a href="#/blue">turn blue</a></li>
    <li><a href="#/green">turn green</a></li>
  </ul>
  <button id='back'>back</button>
  <button id='forward'>forward</button>
  <script src="./hash.js"></script>
</body>
</html>
Copy the code

hash.js

class Routers {
  constructor() {
    this.routes = {}
    this.currentUrl = ' '
    this.history = []; // Record the hash historical value
    this.currentIndex = this.history.length - 1;  // The default points to the last one in history
    // Default forward and backward
    this.isBack = false;
    this.isForward = false;

    this.onHashChange = this.onHashChange.bind(this)
    this.backOff = this.backOff.bind(this)
    this.forward = this.forward.bind(this)

    window.addEventListener('load'.this.onHashChange, false);
    window.addEventListener('hashchange'.this.onHashChange, false);  // Hash change listening event, support >= IE8
  }

  route(path, callback) {
    this.routes[path] = callback || function () { }
  }

  onHashChange() {
    // Neither forward nor backward, triggered when clicking the A TAB
    if (!this.isBack && !this.isForward) {
      this.currentUrl = location.hash.slice(1) | |'/'
      this.history.push(this.currentUrl)
      this.currentIndex++
    }

    this.routes[this.currentUrl]()

    this.isBack = false
    this.isForward = false
  }
  // Back up
  backOff() {
    this.isBack = true
    this.currentIndex = this.currentIndex <= 0 ? 0 : this.currentIndex - 1

    this.currentUrl = this.history[this.currentIndex]
    location.hash = ` #The ${this.currentUrl}`
  }
  // Forward function
  forward() {
    this.isForward = true
    this.currentIndex = this.currentIndex >= this.history.length - 1 ? this.history.length - 1 : this.currentIndex + 1

    this.currentUrl = this.history[this.currentIndex]
    location.hash = ` #The ${this.currentUrl}`}}// Add all routes initially
window.Router = new Routers();
Router.route('/'.function () {
  changeBgColor('yellow');
});
Router.route('/blue'.function () {
  changeBgColor('blue');
});
Router.route('/green'.function () {
  changeBgColor('green');
});

const content = document.querySelector('body');
const buttonBack = document.querySelector('#back');
const buttonForward = document.querySelector('#forward')

function changeBgColor(color) {
  content.style.backgroundColor = color;
}
// Simulate forward and backward
buttonBack.addEventListener('click', Router.backOff, false)
buttonForward.addEventListener('click', Router.forward, false)
Copy the code

Hash routing is compatible, but it is complicated to implement a set of routes.

The online preview

history router

History is a new HTML5 API that allows you to manipulate a browser’s session history that has been accessed in a TAB or frame.

History contains properties and methods:

  • history.stateReads the topmost value in the history stack
  • history.lengthThe number of elements in browsing history
  • window.history.replaceState(obj, title, url)Replaces the current record in the current browsing history
  • window.history.pushState(obj, title, url)To add history to browsing history, do not skip to the page
  • window.history.popstate(callback)Listen for changes in history and trigger a callback event when state changes
  • window.history.back()Back, equivalent to the browser back button
  • window.history.forward()Forward, equivalent to the browser forward button
  • window.history.go(num)Moving forward or back a few pages,numWhen it is negative, back up a few pages

Implementation steps:

  • Initialize a class
  • Record the routing
  • addpopstateEvent that triggers a callback when state changes
  • All routes are added initially

The code is as follows:

history.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>h5 router</title>
</head>
<body>
  <ul>
      <li><a href="/">turn yellow</a></li>
      <li><a href="/blue">turn blue</a></li>
      <li><a href="/green">turn green</a></li>
  </ul>
  <script src='./history.js'></script>
</body>
</html>
Copy the code

history.js

class Routers {
  constructor() {
    this.routes = {};
    this._bindPopState();
  }

  route(path, callback) {
    this.routes[path] = callback || function () {}; } go(path) { history.pushState({path: path }, null, path);
    this.routes[path] && this.routes[path]();
  }

  _bindPopState() {
    window.addEventListener('popstate', e => {  // Listen for history.state changes
      const path = e.state && e.state.path;
      this.routes[path] && this.routes[path](); }); }}// Add all routes initially
window.Router = new Routers();
Router.route('/'.function () {
  changeBgColor('yellow');
});
Router.route('/blue'.function () {
  changeBgColor('blue');
});
Router.route('/green'.function () {
  changeBgColor('green');
});

const content = document.querySelector('body');
const ul = document.querySelector('ul');

function changeBgColor(color) {
  content.style.backgroundColor = color;
}

ul.addEventListener('click', e => {
  if (e.target.tagName === 'A') {
    debugger
    e.preventDefault();
    Router.go(e.target.getAttribute('href')); }});Copy the code

Compatibility: Support >= IE10

The online preview

react-router

React-router consists of four packages, namely react-router, react-router-dom, react-router-config, and react-router-native. React-router-dom is the browser API, react-router-native is the react-native API, and react-router is the core and common part of the API. React-router-config is some configuration dependent.

The react router is a route specified by react. The react API inherits some properties and methods from the React router.

The React-router also uses the History library, which encapsulates hash, history, and memory routes.

The react router v5.x version uses context, which reduces explicit props for passing components to and from each other.

To create the context:

var createNamedContext = function createNamedContext(name) {
    var context = createContext();
    context.displayName = name;
    return context;
};

var context = createNamedContext("Router");
Copy the code

Router

Define a Router class that inherits the react.componentproperties and methods, so Router is also a React component.

_inheritsLoose(Router, _React$Component);
/ / equivalent to the
Router.prototype = Object.create(React.Component)  
Copy the code

Add the React life cycle componentDidMount, componentWillUnmount, and Render methods to the Router prototype.

Generally, a Router is an upper-layer Route that functions as a Route or other subroutes. It uses context.Provider, receives a value attribute, and passes the value to the consuming subcomponent.

var _proto = Router.prototype; 
_proto.componentDidMount = function componentDidMount() {
    // todo
};

_proto.componentWillUnmount = function componentWillUnmount(){
    // todo
};

_proto.render = function render() {
    return React.createElement(context.Provider, props);
};

Copy the code

Listen (callback(location)). If the location changes, setState will update the location. Consuming child components can also get the updated location and render the corresponding component.

The core source code is as follows:

var Router =
    function (_React$Component) {
        // The Router inherits properties and methods from the react.componentprototype
        _inheritsLoose(Router, _React$Component);  

        Router.computeRootMatch = function computeRootMatch(pathname) {
            return {
                path: "/".url: "/".params: {},
                isExact: pathname === "/"
            };
        };

        function Router(props) { // First define a Router class, which is also the React component
            var _this;

            _this = _React$Component.call(this, props) || this; // Inherit its own properties and methods
            _this.state = {
                location: props.history.location
            };

            _this._isMounted = false;
            _this._pendingLocation = null;

            if(! props.staticContext) {// If it is not staticRouter, the location is listened on
                _this.unlisten = props.history.listen((location) = > { // Listen for history.location changes and update locaiton if there are changes
                    if (_this._isMounted) {
                        _this.setState({
                            location: location
                        });
                    } else{ _this._pendingLocation = location; }}); }return _this;
        }

        var _proto = Router.prototype;  ComponentDidMount, componentWillUnmount, and Render methods are added to the prototype object

        _proto.componentDidMount = function componentDidMount() {
            this._isMounted = true;

            if (this._pendingLocation) {
                this.setState({
                    location: this._pendingLocation }); }}; _proto.componentWillUnmount =function componentWillUnmount() {
            if (this.unlisten) this.unlisten();  // Stop listening to location
        };

        _proto.render = function render() {
            // React Context is used to pass history, location, match, and staticContext, making these properties and methods available to all child components
            // const value = {
            // history: this.props.history,
            // location: this.state.location,
            // match: Router.computeRootMatch(this.state.location.pathname),
            // staticContext: this.props.staticContext
            // }

            // return (
            // 
      
            // {this.props.children}
            // 
            // )
            return React.createElement(context.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 // staticContext is an API in staticRouter, not a public API}}); };return Router;
    }(React.Component);
Copy the code

Route

A Route is usually a subcomponent of a Router. It matches the path and renders the corresponding components.

The Route uses context.Consumer, subscribes to the context provided by the Router, and when the location changes, the context changes. Determines whether the current location. pathName matches the path of the child component, if so, render the corresponding component, otherwise not.

Route because some libraries pass components in different ways, so there are multiple renders, part of the code is as follows:

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

// Render children if there are children
if(children && ! isEmptyChildren(children)) renderEle = children// Render if the component passes render and match matches
if (render && match) renderEle = render(props)

// Render component if the component passes Component and match matches
if (component && match) renderEle = React.createElement(component, props)

return (
    <context.Provider value={obj}>
        {renderEle}
    </context.Provider>
)
Copy the code

The core source code is as follows:

var Route =
    function (_React$Component) {
        _inheritsLoose(Route, _React$Component);

        function Route() {
            return _React$Component.apply(this.arguments) | |this;
        }

        var _proto = Route.prototype;

        _proto.render = function render() {
            var _this = this;
            // context.consumer Each Route component can consume the context provided by the Provider in the Router
            return React.createElement(context.Consumer, null.function (context?1) {
                var location = _this.props.location || context?1.location;
                var match = _this.props.computedMatch ? _this.props.computedMatch
                    : _this.props.path ? matchPath(location.pathname, _this.props) : context?1.match;  // Whether to match the current path

                var props = _extends({}, context?1, { // Handle location and match passed by context
                    location: location,
                    match: match
                });

                var _this$props = _this.props,
                    children = _this$props.children,
                    component = _this$props.component,  
                    render = _this$props.render;

                if (Array.isArray(children) && children.length === 0) {
                    children = null;
                }

                // let renderEle = null

                // Render children if there are children
                // if (children &&  !isEmptyChildren(children)) renderEle = children

                // Render if the component passes render and match matches
                // if (render && props.match) renderEle = render(props)

                // Render component if the component passes Component and match matches
                // if (component && props.match) renderEle = React.createElement(component, props)

                // return (
                // 
      
                // {renderEle}
                // 
                // )

                return React.createElement(context.Provider, { // define a Provider in Route to pass props to childrenvalue: props }, children && ! isEmptyChildren(children) ? children : props.match ? component ? React.createElement(component, props) : render ? render(props) :null : null);
            });
        };

        return Route;
    }(React.Component);
Copy the code

Redirect

Redirect routing: From which component is from, to where to be directed.

 <Redirect from="home" to="dashboard" />
Copy the code

Push adds (history.push) to the state stack, or replaces (history.replace) the current state, depending on whether or not push is passed.

Redirect uses context.Consumer, subscribes to the context provided by the Router, and when the location changes, the context changes and the Redirect is triggered.

The source code is as follows:

function Redirect(_ref) {
    var computedMatch = _ref.computedMatch,
        to = _ref.to,
        _ref$push = _ref.push,
        push = _ref$push === void 0 ? false : _ref$push;
    return React.createElement(context.Consumer, null, (context?1) = > {// context.consumer The third argument is a function
        var history = context?1.history,
            staticContext = context?1.staticContext;

        // method: determine whether to replace the current state or add a new state to the state stack
        var method = push ? history.push : history.replace;
        // Generate a new location
        var location = createLocation(computedMatch ? typeof to === "string" ? generatePath(to, computedMatch.params) : _extends({}, to, {
            pathname: generatePath(to.pathname, computedMatch.params)
        }) : to); 

        if (staticContext) { // When rendering a static context (staticRouter), set the new location immediately
            method(location);
            return null;
        }
        // Lifecycle is an empty component that returns null but defines the componentDidMount, componentDidUpdate, componentWillUnmount Lifecycle
        return React.createElement(Lifecycle, {
            onMount: function onMount() {
                method(location);
            },
            onUpdate: function onUpdate(self, prevProps) {
                var prevLocation = createLocation(prevProps.to);
                // If the location is equal before and after the update is triggered, then the location is updated
                if(! locationsAreEqual(prevLocation, _extends({}, location, {key: prevLocation.key }))) { method(location); }},to: to
        });
    });
}
Copy the code

Switch

The child component of the Switch component must be a Route or Redirect component.

The Switch uses context.Consumer, subscribes to the context provided by the Router, and when the location changes, the context changes, Determines whether the current location. pathName matches the path of the child component. If so, render the corresponding child component and leave the rest unrendered.

The source code is as follows:

var Switch =
    function (_React$Component) {
        _inheritsLoose(Switch, _React$Component);

        function Switch() {
            return _React$Component.apply(this.arguments) | |this;
        }

        var _proto = Switch.prototype;

        _proto.render = function render() {
            var _this = this;

            return React.createElement(context.Consumer, null.function (context?1) {
                var location = _this.props.location || context?1.location;
                var element, match; 
               
                React.Children.forEach(_this.props.children, function (child) {
                    if (match == null && React.isValidElement(child)) {
                        element = child;
                        var path = child.props.path || child.props.from;
                        match = path ? matchPath(location.pathname, _extends({}, child.props, {
                            path: path
                        })) : context?1.match; }});return match ? React.cloneElement(element, {
                    location: location,
                    computedMatch: match  // Enhanced match
                }) : null;
            });
        };

        return Switch;
    }(React.Component);
Copy the code

Link

The Link component is used to jump to a specified route. Link actually encapsulates the tag.

Click to trigger the following:

  • Changed url, but usede.preventDefault(), so the page does not jump.
  • Depending on whether or not the attribute replace is passed, passing is replacing the current state (history.replace), otherwise add (history.push), which changes the route.
  • The routing changes because the Router listens for location, which is passed through the context to the consuming subcomponent to match if the path is the same and render the corresponding component.

The core source code is as follows:

function LinkAnchor(_ref) {
    var innerRef = _ref.innerRef,
        navigate = _ref.navigate,
        _onClick = _ref.onClick,
        rest = _objectWithoutPropertiesLoose(_ref, ["innerRef"."navigate"."onClick"]); // Residual attributes

    var target = rest.target;
    return React.createElement("a", _extends({}, rest, {  / / a label
        ref: innerRef ,
        onClick: function onClick(event) {
            try {
                if (_onClick) _onClick(event);
            } catch (ex) {
                event.preventDefault(); // use e.preventdefault () to prevent jumps
                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(); / / change the location}}})); }function Link(_ref2) {
    var _ref2$component = _ref2.component,
        component = _ref2$component === void 0 ? LinkAnchor : _ref2$component,
        replace = _ref2.replace,
        to = _ref2.to, // to jumps to the link path
        rest = _objectWithoutPropertiesLoose(_ref2, ["component"."replace"."to"]);

    return React.createElement(__RouterContext.Consumer, null.function (context) {
        var history = context.history;
        // Generate a new location based on to
        var location = normalizeToLocation(resolveToLocation(to, context.location), context.location);
        var href = location ? history.createHref(location) : "";

        return React.createElement(component, _extends({}, rest, {
            href: href,
            navigate: function navigate() {
                var location = resolveToLocation(to, context.location);
                var method = replace ? history.replace : history.push;
                method(location); // If replace is passed, the current location is replaced, otherwise a location is added to the history stack}})); }); }Copy the code

BrowserRouter

BrowserRouter uses the H5 History API, so you can use methods like pushState and replaceState.

Source code is mainly used in the history library createBrowserHistory method to create an encapsulated history object.

Pass the encapsulated history object to the Props of the Router.

var BrowserRouter =
    function (_React$Component) {
        _inheritsLoose(BrowserRouter, _React$Component); // BrowserRouter inherits the react.componentproperties and methods

        function BrowserRouter() {
            var _this;

            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }

            _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this; // Inherit its own properties and methods
            _this.history = createBrowserHistory(_this.props); // Create the Browser History object, which is the HTML 5 history API
            return _this;
        }

        var _proto = BrowserRouter.prototype;

        _proto.render = function render() {
            return React.createElement(Router, { // Use Router as element, and history and children as props for the Router
                history: this.history,
                children: this.props.children
            });
        };

        return BrowserRouter;
    }(React.Component);
Copy the code

HashRouter

HashRouter differs from BrowserRouter in that it is created with window.location.hash as an object and returns a history, mainly for compatibility reasons.

var HashRouter =
    function (_React$Component) {
        _inheritsLoose(HashRouter, _React$Component);

        function HashRouter() {
            var _this;

            for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
                args[_key] = arguments[_key];
            }

            _this = _React$Component.call.apply(_React$Component, [this].concat(args)) || this;
            _this.history = createHashHistory(_this.props); // Create a Hash history object that is compatible with old browser hashes and is otherwise similar to BrowserRouter
            return _this;
        }

        var _proto = HashRouter.prototype;

        _proto.render = function render() {
            return React.createElement(Router, {
                history: this.history,
                children: this.props.children
            });
        };

        return HashRouter;
    }(React.Component);

Copy the code

conclusion

  • React-router consists of four packages, namely react-router, react-router-dom, react-router-config, and react-router-native. React-router-dom is the browser API, react-router-native is the react-native API, and react-router is the core and common part of the API. React-router-config is some configuration dependent.

  • The react router is a react route. The react router API inherits react attributes and methods.

  • The React-router also uses the History library, which encapsulates hash, history, and memory routes.

  • A Router is an upper-layer Route that functions as a Route or other subroutes. It uses context.Provider, receives a value attribute, and passes the value to the consuming subcomponent.

  • Listen (callback(location)). Click on a Link component and change the location. Whenever the location changes, The context passes the changed location, and the consuming child gets the updated location to render the corresponding component.

reference

  • react-router github
  • history github
  • Looking for Sealand 96 [Do you know front-end routing?]
  • History MDN

communication

Here is the blog address, feel good to click a Star, thank you ~

Github.com/hankzhuo/Bl…