Single-page application routing implementation principle: take React-Router as an example

foreword

I just came into contact with react-router 2 years ago, and I think it's amazing. Just define a few Routes and Links, and you can control the routing of the entire React application. But at that time, I only thought about how to use it, and I also wrote 2 articles related to it  #17  #73  (It seems that the articles at that time were really badly written) Today, let's study it carefully, Hope to solve the following 3 problems.

  1. What is the implementation principle of single page application routing?
  2. How does react-router integrate with react?
  3. How to implement a simple react-router?

The history of hash

The first web page was multi-page, and after Ajax appeared, SPA gradually became available. However, SPAs at that time had two drawbacks:

  1. During the user's use, the url will not change in any way. When the user operates a few steps and accidentally refreshes the page, it will return to the original state.
  2. Due to the lack of url, it is not convenient for search engines to index.

How to do it? →  Use  the hash on the hash
url The original intention is to use it as an anchor point, which is convenient for users to navigate up and down in a very long document. It is not the original intention for routing control of SPA. However, hash satisfies such a feature: when changing the url, the page is not refreshed , and the browser also provides  event listeners such as onhashchange  , so hash can be used for routing control. (This part of the Red Book P394 also has related instructions.) Later, this mode became popular, and onhashchange was written into the HTML5 specification.

Let's take an example to demonstrate "refreshing the page locally by changing the hash value", this example comes from the front-end routing implementation and react-router source code analysis , By joeyguo

<ul>
    <li><a href="#/">turn white</a></li>
    <li><a href="#/blue">turn blue</a></li>
    <li><a href="#/green">turn green</a></li>
</ul>
function Router() {
    this.routes = {};
    this.currentUrl = '';
}
Router.prototype.route = function (path, callback) {
    this.routes[path] = callback || function () {
        };
};
Router.prototype.refresh = function () {
    console.log('Trigger a hashchange, the hash value is', location.hash);
    this.currentUrl = location.hash.slice(1) || '/';
    this.routes[this.currentUrl]();
};
Router.prototype.init = function () {
    window.addEventListener('load', this.refresh.bind(this), false);
    window.addEventListener('hashchange', this.refresh.bind(this), false);
};
window.Router = new Router();
window.Router.init();
var content = document.querySelector('body');
// change Page anything
function changeBgColor(color) {
    content.style.backgroundColor = color;
}
Router.route('/', function () {
    changeBgColor('white');
});
Router.route('/blue', function () {
    changeBgColor('blue');
});
Router.route('/green', function () {
    changeBgColor('green');
});

The effect of operation is shown in the following figure: We can see from the figure that it is indeed possible to partially refresh the page by changing the hash. In particular, it should be noted that when entering the page for the first time, if the url already contains a hash, an onhashchange event will also be triggered, which ensures that the initial hash can be recognized. Problem: Although hash solves the problem of SPA routing control, it introduces new problems →  there will be a # on the url, which is very unsightly Solution: Abandon hash and use history
hash


The evolution of history

Browsers implemented history a long time ago. However, the early history can only be used for multiple pages to jump, such as:

// This part can refer to the Red Book P215
history.go(-1); // go back one page
history.go(2); // go forward two pages
history.forward(); // forward one page
history.back(); // go back one page

In the HTML5 specification, history adds the following APIs

history.pushState(); // add a new state to the history state stack
history.replaceState(); // replace the current state with the new state
history.state // returns the current state object

By history.pushStateor history.replaceState, it can also be done: change the url without refreshing the page . So history also has the potential to realize routing control. However, there is one thing missing: the change of hash will trigger the onhashchange event, what event will the change of history trigger ? →  Unfortunately, no .
How to do it? → Although we can't monitor history change events, if we can list all possible ways to change history, and then intercept them one by one, wouldn't it be equivalent to monitoring history changes ?
For an application, url changes can only be caused by the following three ways:

  1. Click the browser's forward or back button;
  2. Click the a tab;
  3. Modify routes directly in JS code

The 2nd and 3rd ways can be regarded as one, because the default event of the a tag can be disabled, and then the JS method can be called. The key is the first one, an  onpopstate  event has been added to the HTML5 specification, through which the click of the forward or back button can be monitored.
It is important to note that the onpopstate event is not triggered by calling history.pushStatesum .history.replaceState

Summary: After the above analysis, history can be used for routing control, but it needs to start from three aspects .

React-Router v4

The version of React-Router is also weird. From 2 to 3 to 4, every API change is earth-shaking. This time we will take the latest  v4 as an example.

const BasicExample = () => (
  <Router>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/topics">Topics</Link></li>
      </ul>

      <hr/>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/topics" component={Topics}/>
    </div>
  </Router>
)

The actual result of the operation is shown in the figure below: We can see from the figure that the so-called partial refresh is essentially: the three comppnents are always there. When the route changes, the component that matches the current url is rendered normally; the component that does not match the current url is rendered as null, that 's all, this is actually the same as show and hide in the jQuery era. We have observed the phenomenon, and the realization ideas are discussed below.
rrv4

Thought analysis

react router

Code

For the idea analysis and code implementation of this article, refer to this article: build-your-own-react-router-v4 , By Tyler; you can also look at the translated version: from shallow to deep to teach you to develop your own React Router v4 , By Beard. Compared with the reference article, I mainly made the following two changes:

  1. In the original text, the event binding of onpopstate is carried out in each Route. For simplicity, I removed this part and only binds only one event to onpopstate, loops the instance array in this event, and calls the forceUpdate method of each Route in turn;
  2. After exporting a jsHistory object, jsHistory.pushStateyou can control routing navigation in JS by calling the method.
// App.js
import React, {Component} from 'react'
import {
    Route,
    Link,
    jsHistory
} from './mini-react-router-dom'

const App = () => (
    <div>
        <ul className="nav">
            <li><Link to="/">Home</Link></li>
            <li><Link to="/about">About</Link></li>
            <li><Link to="/topics">Topics</Link></li>
        </ul>

        <BtnHome/>
        <BtnAbout/>
        <BtnTopics/>
        <hr/>

        <Route exact path="/" component={Home}/>
        <Route path="/about" component={About}/>
        <Route path="/topics" component={Topics}/>
    </div>
);

const Home = () => (
    <div>
        <h2>Home</h2>
    </div>
);

const About = () => (
    <div>
        <h2>About</h2>
    </div>
);

const Topics = ({match}) => (
    <div>
        <h2>Topics</h2>
    </div>
);

class BtnHome extends Component {
    render() {
        return (
            <button onClick={jsHistory.pushState.bind(this, '/')}>Home</button>
        )
    }
}

class BtnAbout extends Component {
    render() {
        return (
            <button onClick={jsHistory.pushState.bind(this, '/about')}>About</button>
        )
    }
}

class BtnTopics extends Component {
    render() {
        return (
            <button onClick={jsHistory.pushState.bind(this, '/topics')}>Topics</button>
        )
    }
}

export default App
// mini-react-router-dom.js
import React, {Component, PropTypes} from 'react';

let instances = []; // used to store the router in the page
const register = (comp) => instances.push(comp);
const unRegister = (comp) => instances.splice(instances.indexOf(comp), 1);

const historyPush = (path) => {
    window.history.pushState({}, null, path);
    instances.forEach(instance => instance.forceUpdate())
};

window.addEventListener('popstate', () => {
    // Traverse all Routes and force re-rendering of all Routes
    instances.forEach(instance => instance.forceUpdate());
});

// Determine whether the path parameter of the Route matches the current url
const matchPath = (pathname, options) => {
    const {path, exact = false} = options;
    const match = new RegExp(`^${path}`).exec(pathname);
    if (!match) return null;
    const url = match[0];
    const isExact = pathname === url;
    if (exact && !isExact) return null;
    return {
        path,
        url
    }
};

export class Link extends Component {
    static propTypes = {
        to: PropTypes.string
    };

    handleClick = (event) => {
        event.preventDefault();
        const {to} = this.props;
        historyPush(to);
    };

    render() {
        const {to, children} = this.props;
        return (
            <a href={to} onClick={this.handleClick}>
                {children}
            </a>
        )
    }
}

export class Route extends Component {
    static propTypes = {
        path: PropTypes.string,
        component: PropTypes.func,
        exact: PropTypes.bool
    };

    componentWillMount() {
        register(this);
    }

    render() {
        const {path, component, exact} = this.props;
        const match = matchPath(window.location.pathname, {path, exact});

        // Route does not match the current url, it returns null
        if (!match) return null;

        if (component) {
            return React.createElement(component);
        }
    }

    componentWillUnMount() {
        unRegister(this);
    }
}

// The reason for exporting a jsHistory here,
// It is for the convenience of users to directly control the navigation in JS
export const jsHistory = {
    pushState: historyPush
};

The effect achieved is shown in the following figure:
demo

References

The code involved in this article can refer to this repository .

  1. Build your own React Router v4, By Tyler
  2. Teach you to develop your own React Router v4 from the simple to the deep , By beard
  3. Front-end routing implementation and react-router source code analysis , By joeyguo
  4. In-depth analysis of react-router 2.7.0 source code , By Zhu Jian

---------- over-------------

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325000533&siteId=291194637