The React Router source code is concise and easy to read. It is a good Angle to get into the principle of front-end routing. In the process of sharing and learning, I also have some thoughts and opinions on front-end routing, so I write this article to share my understanding of front-end routing with you.

In this paper, we will first implement a basic front-end routing using native JS, and then introduce the source code implementation of React Router. By comparing the implementation methods of React Router and React Router, we will analyze the motivation and advantages of the implementation. By the end of this article, readers should know:

  1. Basic principles of front-end routing
  2. The React Router implementation principle
  3. React Router

I. How should we implement a front-end routing

First of all, let’s think about how to implement a front-end routing with native JavaScript. Front-end routing is basically two functions: monitoring and recording route changes, matching route changes, and rendering content. With these two requirements as the basic framework, we can outline the shape of the front-end routing.

Routing examples:

1. The Hash

As we all know, front-end routing generally provides two matching modes, hash mode and history mode. The main difference between the two modes is the difference between the listening part of the URL. Hash mode listens for the change of the hash part of the URL, that is, the part after the #. The browser provides the onHashChange event to help us directly listen for changes to the hash:

<body>
    <a href="#/home">Home</a>
    <a href="#/user">User</a>
    <a href="#/about">About</a>
    <div id="view"></div>
</body>

<script>
    // the onHashChange event callback matches the route change and renders the corresponding content
    function onHashChange() {
        const view = document.getElementById('view')
        switch (location.hash) {
          case '#/home':
              view.innerHTML = 'Home';
              break;
          case '#/user':
              view.innerHTML = 'User';
              break;
          case '#/about':
              view.innerHTML = 'About';
              break; }}// Bind hash change events to listen for route changes
    window.addEventListener('hashchange', onHashChange);
</script>
Copy the code

Hash mode implementation is relatively simple, we can use the hashChange event to directly listen for changes in the routing hash, and render different content based on the matched hash.

2. The History

Compared with the simple and straightforward hash implementation, the implementation of history mode requires us to write a few more lines of code. Let’s first modify the jump link of tag A. After all, the most direct difference between history mode and hash is that the jump route does not carry the # sign, so we try to remove the # sign directly:

<body>
    <a href="/home">Home</a>
    <a href="/user">User</a>
    <a href="/about">About</a>
    <div id="view"></div>
</body>
Copy the code

Click a label, the page will jump, and the message that the jump page can not be found, which is also expected behavior, because the default behavior of a label is the jump page, we do not have the corresponding page file in the jump path, the error will be prompted. So what should we do about this non-hash route change? In general, we can implement routing in History mode through the following three steps:

2. Update URL 3 with H5's history API. Listen for and match route changes to update the pageCopy the code

Before we start writing code, it’s worth looking at a few basic uses of the H5 History API. In fact, the global object window.history existed in the era of HTML4, but at that time we could only call back(), go() and other methods to manipulate the basic behavior of the browser forward and backward. The pushState(), replaceState() and popState events introduced in H5 allow us to modify urls without refreshing the page and listen for CHANGES in urls, providing basic capabilities for the implementation of history routing.

// Several uses of the H5 history API

History.pushState(state, title [, url])
// To add a state to the top of the history stack, the method takes three parameters: a state object, a title, and (optionally) a URL
// Simply put, pushState updates the current URL without causing a page refresh

History.replaceState(stateObj, title[, url]);
// Modify the current history entity
PushState is similar to pushState, except that pushState adds a record to the top of the page stack, whereas replaceState modifies the current record

window.onpopstate
// When the active history entry changes, the popState event is emitted
// Note that onPopState is not triggered by pushState or replaceState changes to the URL. It is only triggered by browser actions such as clicking the back button, the forward button, the A TAB click, etc
Copy the code

For details on the parameters and usage of MDN, please refer to MDN. This section only introduces the key points related to routing implementation and basic usage. With these apis in mind, we can implement our history route in three steps:

<body>
    <a href="/home">Home</a>
    <a href="/user">User</a>
    <a href="/about">About</a>
    <div id="view"></div>
</body>

<script>
    // Overwrite all a tag events
    const elements = document.querySelectorAll('a[href]')
    elements.forEach(el= > el.addEventListener('click'.(e) = > {
      e.preventDefault()    // Block the default click event
      const test = el.getAttribute('href')
      history.pushState(null.null, el.getAttribute('href'))     
      // Modify the current URL (the first two parameters are state and title, which are not needed here
      onPopState()          
      PushState does not trigger the onPopState event, so we need to trigger the event manually
    }))
    
    // The onPopState event callback matches the route change and renders the corresponding content, basically the same as the hash mode
    function onPopState() {
        const view = document.querySelector('#view')
        switch (location.pathname) {
          case '/home':
              view.innerHTML = 'Home';
              break;
          case '/user':
              view.innerHTML = 'User';
              break;
          case '/about':
              view.innerHTML = 'About';
              break; }}// Bind the onPopState event to trigger the popState event when the page routing changes (such as forward or back)
    window.addEventListener('popstate', onPopState);
</script>
Copy the code

The history mode code does not run locally by opening an HTML file directly. When switching routes, you will be prompted:

Uncaught SecurityError: A history state object with URL file://xxx.html cannot be created in a document with origin ‘null’.

This is because the url for pushState must be of the same origin as the current url, and the page opened in file:// does not have origin, causing an error. If you want to run the experience properly, you can start a local service for the files using http-server.

The implementation code of the History mode is also relatively simple. We prevent the default page jump behavior by rewriting the click event of a label, change the URL without refreshing through the History API, and finally render the content of the corresponding route. So far, we have basically understood the differences and implementation principles of hash and History front-end routing modes. In general, although the principles of hash and History are different, their objectives are basically the same, which isto monitor and match the changes of routes and render page content according to route matching without refreshing the page. Since we can implement front-end routing so easily, what are the advantages of the React Router and what can be learned from its implementation?

React Router

Before analyzing the source code, let’s review the basic usage of the React Router and analyze the basic design and requirements of a front-end routing library. Only by first understanding the requirements and design as upstream can we clearly and comprehensively parse the source code as downstream.

React Router components are divided into three types:

Router components: and, router components as root container components, such as routing components must be wrapped before they can be used. Route matching component: and, the route matching component renders the corresponding component by matching the path. Navigation component: and, the navigation component plays a similar role as a TAB to jump to the page. We will also use the code parsing of these six components as a clue to get a glimpse of the React Router’s overall implementation. React Router {React Router} React Router {React Router}

import { BrowserRouter, Switch, Route, Link } from "react-router-dom";
// There is little difference between using HashRouter and BrowserRouter

const App = () = > {
  return (
    <BrowserRouter>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/user">User</Link>

        <Switch>
            <Route path="/about"><About /></Route>
            <Route path="/user"> <User /></Route>
            <Route path="/"><Home /></Route>
        </Switch>
    </BrowserRouter>
  );
}

const Home = () = > (<h2>Home</h2>);
const About = () = > (<h2>About</h2>);
const User = () = > (<h2>User</h2>);

export default App;
Copy the code

We re-implemented the function of the initial native route using the React Router. There are similarities and differences between the two. Corresponding to a label, realize the function of jump route; The render logic in onPopState() matches the route and renders the corresponding component; The addEventListener listens for route changes.

Let’s get into the React Router source code to see how these components are implemented.

React Router

1. Catalog overview

The React Router code is stored in the Packages folder. After version v4, the React Router is distributed in four packages. This article mainly analyzes the react-Router and react-router-dom folders.

├ ─ ─ packages ├ ─ ─ the react to the router / / core and common code ├ ─ ─ the react - the router config / / routing configuration ├ ─ ─ the react - the router - dom / / browser environment routing └ ─ ─ React -router-native // react native routesCopy the code

2. BrowserRouter and HashRouter

Both and are routing container components. All routing components must be wrapped in these two components to use:

const App = () = > {
  return (
    <BrowserRouter>
        <Route path="/" component={Home}></Route>
    </BrowserRouter>); }}}}}}}}/ / < BrowserRouter > source

import React from "react";
import { Router } from "react-router";
import { createBrowserHistory as createHistory } from "history";

class BrowserRouter extends React.Component {
  history = createHistory(this.props);

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

/ / < HashRouter > source

import React from "react";
import { Router } from "react-router";
import { createHashHistory as createHistory } from "history";

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

We’ll see that the two are a shell, with very little code and almost the same code, creating a history object and passing it through with the child component, except for the createHistory() introduced. So the resolution of both is really the resolution of the and history libraries.

The history library

History source repository: github.com/ReactTraini…

The React Router relies on the History library, which is a 7K + STAR session history management library. In this section we’ll look at the use of the History library and why the React Router chose History to manage session history.

Before we look at specific usage, let’s think about our “session history management” requirements. With session history management, it’s easy to think of maintaining a page access history stack, pushing a history when jumping to a page, and popping a history back. However, we can see from the native implementation of hash and history routes in section 1 that different routing modes have different APIS for manipulating session history and different ways to listen to session history, and there are not only two front-end routing modes. The React Router also provides static mode for RN development and SSR.

Instead of carrying these differences and judgments into the React Router code, we wanted to add a layer of abstraction to mask the differences between the operation session histories of several modes.

History lets you easily manage session history anywhere JavaScript is running. A History object abstracts out the differences in various environments and provides a minimal API that allows you to manage the history stack, navigate, and maintain state between sessions.

This is the first sentence of the history document, which is a good summary of the role, advantages, and scope of use of history.

import { createBrowserHistory } from 'history'; Const history = createBrowserHistory(); // Get the current location object, like window.location const location = history.location; // Set the listener event callback, Listen ((location, action) => {console.log(location.pathname, location.state); }); Push ('/home', {some: 'state'}); push('/home', {some: 'state'}); // To stop listening, call the function returned by listen().Copy the code

The API is simple and easy to understand, so I won’t go over it. For the sake of space, this section only introduces the use of the History library. The implementation principle is left at the end of the article so that readers can focus on the React Router implementation first.

Router implementations are already known, and are essentially the same, except that they introduce a different createHistory() method. The code for the react-router package is a relatively common component. Other packages are included here:

// The RouterContext is not a native React Context. Since React16 and 15 contexts are incompatible, the React Router uses a third-party Context to support both React16 and 15
// This context is based on the mini-create-react-context implementation. This library is also Polyfil for react Context, so it can be considered the same
import RouterContext from "./RouterContext";
import React from 'react';

class Router extends React.Component {
  // This method is used to generate a match object for the root path
  
       
        
         
          
           
            
             
            
           
          
         
        
       
      
  static computeRootMatch(pathname) {
    return { path: "/".url: "/".params: {}, isExact: pathname === "/" };
  }

  constructor(props) {
    super(props);

    // The location object is fetched from the history instance and stored in state. Later, setState will change the location to trigger a re-rendering
    / / the location object contains a hash/pathname/search/state properties, such as is the current routing information
    this.state = {
      location: props.history.location
    };
    // isMounted and pendingLocation are private variables that confuse the user. Why listen for route changes in constructor instead of componentDidMount
    // Simply put, since the child component is mounted before the parent component, if you listen on componentDidMount, it is possible that the history.location has changed before the listener event is registered. So we need to register the listener event in constructor and record the changed location until the component has been mounted and then updated to state
    // If this part of the hack is removed, it is simply set up to listen the route, and update the routing information in the state when the route changes
    // To determine whether the component is already mounted, the componentDidMount phase assigns true
    this._isMounted = false;
    // Store the location that changed during constructor execution
    this._pendingLocation = null;

    // determine whether a staticContext is being rendered on the server.
    if(! props.staticContext) {// Use history.listen() to add a route listening event
      this.unlisten = props.history.listen(location= > {
        if (this._isMounted) {
          // If the component is already mounted, update state's location directly
          this.setState({ location });
        } else {
          // if the component is not mounted, save the location and wait until the didmount phase to setState
          this._pendingLocation = location; }}); }}// Set _isMounted to true and update location with setState
  componentDidMount() {
    this._isMounted = true;

    if (this._pendingLocation) {
      this.setState({ location: this._pendingLocation }); }}// When the component is uninstalled, synchronize the listening of unbound routes
  componentWillUnmount() {
    if (this.unlisten) this.unlisten();
  }

  render() {
    return (
      // Provider passes values down to components in the component tree
      <RouterContext.Provider// Pass through child componentschildren={this.props.children || null}
        value={{
          history: this.props.history// Current routing informationlocation: this.state.location, // Whether it is the root pathmatch: Router.computeRootMatch(this.state.location.pathname), // used for server renderingstaticContext
          staticContext: this.props.staticContext}} / >); }}export default Router
Copy the code

It looks like a lot of code, but if you strip out the various scenarios in the code, you actually do two things. One isto package a context for the child component, so that information (history and location objects) can be passed to all its descendants. The other is to bind the route listening event so that setState is triggered every time the route changes.

In fact, we can see why the routing component needs to be wrapped in the routing container component, because the routing information is passed by the outer container component to all descendant components through the context, and the descendant components can match and render the corresponding content after they get the current routing information. In addition, when the route changes, the container component triggers the child component to re-render via setState().

The summary of this chapter

After looking at the implementation, let’s make a comparison with the native implementation. We mentioned earlier that the two main points of front-end routing are listening and matching route changes, which helps us complete the listening step. In the React Router, this is done by the History library. In the React Router, this is done by the history library. The code calls history.listen to complete the monitoring of several mode routes.

In addition, in the native implementation, we also ignore the nesting of routes. In fact, we only bind the listening event to the root node, without considering the routes of the child components. In React Router, routing information is transmitted to its descendants through context, so that all routing components under it can sense the changes of routes. And get the routing information.

The realization of the Route

As mentioned earlier, the core of front-end routing lies in listening and matching. We implement listening in the above section, so this section will analyze how to do matching. Similarly, we will review the usage first:

Matching mode:

// Exact match
// A strict match
// Case sensitive
<Route path="/user" exact component={User} />
<Route path="/user" strict component={User} />
<Route path="/user" sensitive component={User} />
Copy the code

Path path

// It is a string of characters
// Name parameters
// It is an array
<Route path="/user" component={User} />
<Route path="/user/:userId" component={User} />
<Route path={["/users", "/profile"]} component={User} />
Copy the code

Render mode:

// Render by subcomponent
// Render via props.component
// props. Render
<Route path='/home'><Home /></Route>
<Route path='/home' component={Home}></Route>
<Route path='/home' render={()= > <p>home</p>} ></Route>

// Example: Here the final render result is User and the priority is subcomponent > Component > render
<Route path='/home' component={Home} render={()= > <p>About</p>} ><User />
</Route>
Copy the code

All you do is simply match the path passed in and render the corresponding component. In addition, it also provides several different matching modes, path writing and rendering methods, source code implementation, and these configuration items are closely related:

import React from "react";
import RouterContext from "./RouterContext";
import matchPath from ".. /utils/matchPath.js";

function isEmptyChildren(children) {
  return React.Children.count(children) === 0;
}

class Route extends React.Component {
  render() {
    return({/* The Consumer receives the context passed from 
      
       , which contains the history object, location(current routing information), match(matching object), etc. */
      }
      <RouterContext.Consumer>
      {/* Get the routing information from the match object (source priority: Switch → props. Path → context) props.computedMatch is passed down from 
      
       , it is a calculated match, the highest priority is the path attribute on the 
       
         component, The match object on the matchPath context, which will be explained in detail in the next section, combines the current location and match into new props, This props is going to use the Provider to pass down three render methods provided by the 
        
          component, priority children > component > render This is because Preact, by default, uses an empty array to represent the absence of children (Preact is an interesting 3KB React alternative library) */
        
       
      }
        {context= > {
          const location = this.props.location || context.location;
        
          const match = this.props.computedMatch
            ? this.props.computedMatch  
            : this.props.path           
            ? matchPath(location.pathname, this.props)  
            : context.match;

          
          constprops = { ... context, location, match };let { children, component, render } = this.props;         
          if (Array.isArray(children) && isEmptyChildren(children)) { 
            children = null;
          }

          // Pass down the new props through the context
          // Level 1 judgment: Render the children or Component if there is a match object
          Render children if there are children, render Component if there are no children
          // If the children component is a function, execute the function first with the routing information props as the callback argument
          return (
            <RouterContext.Provider value={props}>            
              {props.match                                    
                ? children                                    
                  ? typeof children === "function"            
                    ? children(props)
                    : children
                  : component                                 
                  ? React.createElement(component, props)
                  : render
                  ? render(props)
                  : null
                : typeof children === "function"
                ? children(props)
                : null}
            </RouterContext.Provider>); }} </RouterContext.Consumer> ); }}export default Route;
Copy the code

The implementation of Route is relatively simple, and the code is divided into two parts: getting the Match object and rendering components. We see multiple match objects in the code, which are actually generated by the root component’s computedMatch() or matchPath() and contain the current match information. For the process of generating the match object, we will leave it to the next section, where we only need to know that if the current Route matches the Route, the corresponding match object will be generated, if there is no match, the match object will be null.

// Match object instance
{
  isExact: true.params: {},
  path: "/".url: "/"
}
Copy the code

Route provides three rendering methods: subcomponents, props.component, and props. Render. There are priorities among the three, so that we can see the structure of multi-layer term-expression rendering.

The code uses four levels of nested ternary expressions to implement a priority rendering of the child > component property passed to the component > children function.

The red nodes are the final render result:

matchPath

If we were to implement route matching, how would we do it? Congruent comparison? Regular judgment? It should seem like a simple implementation anyway, but if we open the matchPath() code, it takes 60 lines of code to invoke a third-party library to do this:

import pathToRegexp from "path-to-regexp";

// It is recommended to look directly at matchPath() and then look at compilePath
const cache = {};
const cacheLimit = 10000;
let cacheCount = 0;

// compilePath spells regular regexp from path and matching parameters options, and keys are path parameters
function compilePath(path, options) {
  const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
  const pathCache = cache[cacheKey] || (cache[cacheKey] = {});

  if (pathCache[path]) return pathCache[path];

  const keys = [];                                  
  // Keys is an empty array, and pathToRegexp appends parameters parsed in path to keys
  const regexp = pathToRegexp(path, keys, options); 
  PathToRegexp is a tool that converts string paths into regular expressions
  const result = { regexp, keys };                  
  // Returns the regexp and keys parsed in the path

  console.log('cacheCount', cacheCount);
  console.log('cacheLimit', cacheLimit);
  console.log('pathCache', pathCache);;
  if (cacheCount < cacheLimit) {
    pathCache[path] = result;
    cacheCount++;
  }

  return result;
}

function matchPath(pathname, options = {}) {
  if (typeof options === "string" || Array.isArray(options)) {      
  // Normally, options are props of 
      
       , which is an object; The options passed in for a call to the 'react-router-redux' library are path only
      
    options = { path: options };
  }

  const { path, exact = false, strict = false, sensitive = false } = options;   
  // Obtain the route path and matching parameters, and assign initial values

  const paths = [].concat(path);                    
  // Unified path type (path can be array ['/', '/user'] or string "/user")

  return paths.reduce((matched, path) = > {          
  // Reduce is used to output only one result while iterating through paths. If you use API like Map to loop, you will get an array
    if(! path && path ! = ="") return null;          
    // No path, return null
    if (matched) return matched;                    
    // If a match is found, return the result of the last match

    const { regexp, keys } = compilePath(path, {    
    // Set the regexp based on the route path and matching parameters. Keys is the path parameter (e.g. /user:id id).
      end: exact,
      strict,
      sensitive
    });
    const match = regexp.exec(pathname);            
    // Call the regular prototype method exec, which returns an array of results or NULL

    if(! match)return null;                        
    // If no match is found, return null

    const [url, ...values] = match;                 
    // From the result array
    const isExact = pathname === url;               
    // Whether the match is accurate

    if(exact && ! isExact)return null;             
    // If an exact match is not found, return null

    // Here are a few examples to help you intuitively understand the call process
    // Pass path: /user
    // regexp: /^\/user\/? (? =\/|$)/i
    // url: /user
    / / return the result: {" path ":"/user ", "url" : "/ user", "isExact" : true, "params" : {}}

    2 / / examples
    //  传入的path:    /user/:id
    // regexp: /^\/user\/(? : [^ \ /] +? ) \ /? (? =\/|$)/i
    // url: /user/1
    / / return the result: {" path ":"/user / : id ", "url" : "/ user / 1", "isExact" : true, "params" : {" id ":" 1 "}}

    return {
      path,                                           
      // Path to match
      url: path === "/" && url === "" ? "/" : url,    
      // The matching part of the URL
      isExact,                                        
      // Whether the match is accurate
      params: keys.reduce((memo, key, index) = > {     
      // Convert the route keys directly returned by path-to-regexp
        memo[key.name] = values[index];
        returnmemo; }, {}}; },null);
}

export default matchPath;
Copy the code

summary

This section explains how the React Router is matched and rendered by mathPath. MathPath uses path-to-regexp to match routes. Render matched subcomponents with different priorities and matching patterns.

The end of the

The React Router does not React to the React Router. The React Router does not React to the React Router.

  • forListening to theImplementation of the React Router feature was introducedhistoryIn order to shield the different modes of routing in the monitoring implementation of the difference, and routing information tocontextIs passed to the component wrapped around the <Router>, enabling all routing components wrapped within itRoute changes are detected and routing information is received. Procedure
  • inmatchingThe React Router section is introducedpath-to-regexpThe routing component <Route>, as a higher-order component, wraps business components, and renders corresponding components with different priorities by comparing the current routing information with the path passed in


On the whole, the React Router source code is relatively simple and clear. The design and implementation of front-end routing reflected in the source code will also inspire readers. The React Router source code will not be used in this article, but it will not be used in this article. The React Router source code will not be used in this article. For front-end routing, we still have a lot to discover, and source code parsing is just a small step along the way. Front-end routing, under the continuous iteration of front-end ER, will continue to explore and advance in the current wave of front-end technology, to play its value in a broader scene.

Due to time constraints, this article is in a hurry and sloppy, please forgive me. The following holes have not been filled in yet, which is left to the readers to think about ~

  • Centralized static configuration routing versus distributed dynamic component routing
  • And < Link> component source code parsing
  • React Router hooks
  • History library source code analysis








Scan code to pay attention to IMWeb front-end community public number, get the latest front-end good articles

Micro blog, nuggets, Github, Zhihu can search IMWeb or IMWeb team to follow us.