1. The core principles of React-Router

The React-Router has two modes, both of which rely on methods of the Window object to implement route hops

  • HashRouter
  • BrowserRouter

1.1 、HashRouter

Window. onhashchange event, click see MDN

### hashChange implements hacks
if(! window.HashChangeEvent)(function(){
	var lastURL=document.URL;
	window.addEventListener("hashchange".function(event){
		Object.defineProperty(event,"oldURL",{enumerable:true,configurable:true,value:lastURL});
		Object.defineProperty(event,"newURL",{enumerable:true,configurable:true,value:document.URL}); lastURL=document.URL; }); } ());# use
window.addEventListener('hashChange'.function(event) {
    console.log('the new URL, event.newURL);
    console.log('the old URL', event.oldURL)
})
Copy the code

1.2、 BrowserRouter

Jump to the link via the History API

1.2.1 pushState (from MDN) [same as replaceState]

PushState () takes three arguments: a state object, a title (currently ignored), and (optionally) a URL. Let’s explain the three parameters in detail:

  • State object – The state object state is a JavaScript object that creates a new history entry with pushState (). Whenever the user navigates to a new state, the POPState event is fired, and the state property of the event contains a copy of the history entry state object.

    • A state object can be anything that can be serialized. The reason is that Firefox saves the status object on the user’s disk for use when the user restarts the browser, and we have a 640K size limit for the status object after its serialized representation. If you pass pushState() a serialized state object greater than 640K, it will throw an exception. If you need more space, sessionStorage and localStorage are recommended.
  • Title – Firefox currently ignores this parameter, but may use it in the future. Passing an empty string here should be safe against future changes to this method. Alternatively, you can pass a short title for the state of the jump.

  • URL – This parameter defines a new historical URL record. Note that the browser does not load the URL immediately after calling pushState(), but it may load it later in certain circumstances, such as when the user reopens the browser. The new URL does not have to be an absolute path. If the new URL is a relative path, it is treated as relative to the current URL. The new URL must be of the same origin as the current URL, otherwise pushState() will throw an exception. This parameter is optional. The default value is the current URL.

let stateObj = {
    foo: "bar"
}
history.pushState(stateObj,"Set parameters"."/index.html")
Copy the code

React Router implementation

2.1 React Router Components

BrowserRouter.js

Technical summary: As a top-level route, this section mainly defines the state parameters required in the route. The jump between routes is implemented using window.history.pushState method. As an independent component, this example also flexibly uses the advanced use of React context parameter passing, which is worth using for reference when I am in charge of components.

import React from 'react';
import Context from './context';
let pushState = window.history.pushState;
window.history.pushState = (state, title, url) => {
    pushState.call(window.history, state, title, url);
    window.onpushstate.call(this, state, url)
}
export default class HashRouter extends React.Component {
    state = {
        location: {
            pathname: window.loation.pathname, 
            state:null
        }
    }
    componentDidMount() {
        window.onpopstate = (event) => {
            if(this.block) {
                let confirm = window.confirm(this.block(this.state.location))
                if(! confirm)return; } this.setState({ location: { ... this.state.location, pathname: window.location.pathname, state: event.state } }) } window.onpushstate = (state, pathname) => { this.setState({ location: { ... this.state.location, pathname, state } }) } }render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to) {
                    if(that.block) {
                        let confirm = window.confirm(that.block(typeof to === 'object'? to:{pathname:to}));if(! confirm)return;
                    }
                    if(typeof to === 'object') {
                        let { pathname, state } = to;
                        window.history.pushState(state, ' ', pathname)
                    } else {
                        window.history.pushState(null, ' ', to)
                    }
                },
                block(message) {
                    that.block = message
                }
            }
        }
        return (
            <Context.Provider value={value}>
                {this.props.children}
            </Context.Provider>
        )   
    }
    }
Copy the code

HashRouter.js

Similar to BrowserRouter implementation

import React from 'react';
import Context from './context';
export default class HashRouter extends React.Component {
    state: {
        location: {pathname: window.location.hash.slice(1), state: null } 
    }
    locationState = null;
    componentDidMount() {
        window.location.hash = window.location.hash || '/';
        window.addEventListener('hashchange', () => { this.setState({ location: { ... this.state.location, pathname: window.location.hash.slice(1), state: this.locationState } }) }) }render() {
        let that = this;
        let value = {
            location: that.state.location,
            history: {
                push(to) {
                    if(that.block) {
                        let confirm = window.confirm(that.block(typeof to === 'object'? to:{pathname:to}));if(! confirm)return;
                    }
                    if(typeof to === 'object') {let {pathname,state} = to;
                        that.locationState = state;
                        window.location.hash = pathname;
                    }else{ that.locationState = null; window.location.hash = to; } }, block(message){ that.block = message; }}}return (
        <Context.Provider value={value}>
            {this.props.children}
        </Context.Provider>
    )
}
Copy the code

context.js

The context file is extracted separately as an object referenced for public consumption

import React from 'react';
const context = React.createContext();
export default context;
Copy the code

Route.js

import Route from 'react'
import RouterContext from './context'
import pathToRegexp from 'path-to-regexp'
export default class Route extends React.Component {
    static contextType = RouterContext;
    render() {
        let {
            path="/",
            component: Component,
            exact: false,
            render,
            children
        } = this.props;
        let paramNames = [];
        # https://github.com/pillarjs/path-to-regexp 
        Use the pathToRegexp regex library to parse generated path parameters
        let regxp = pathToRegexp(path, paramNames,{end: exact});
        let result = pathname.match(regxp);
        let props = {
            location: this.context.location,
            history: this.context.history
        }
        If the paths match
        if(result) {
            paramNames = paramNames.map(item => item.name);
            let{url, ... values} = result;let params = {};
            for(let i = 0; i < paramNames.length; i++) {
                params[paramNames[i]] = values[i]
            }
            props.match = {
                path,
                url,
                isExact: url === pathname,
                params
            }
            Render parameters exist in Component,render or render cases
            if(Component) {
                return<Component {... props} /> }else if(render){
                return render(props);
            } else if(children) {
                return children(props);
            } else {
                return null
            }
        } else {
            if(children) {
                return children(props)
            } else {
                return null
            }
        }
    }
}
Copy the code

Switch

If we use the Router object directly, we can see that the browser doesn’t exactly match the routing component displayed, so we need to wrap the Switch in the outermost layer

import React from 'react';
import pathToRegexp from 'path-to-regexp';
import RouterContext from './context';
export default class Switch extends React.Component {
    static contextType = RouteContext;
    render() {
        Decouple the path to the current address bar
        let {pathname} = this.context.location;
        # Unify the child element object format to array format
        let children = Array.isArray(this.props.children)? this.props.children:[this.props.children];
        for(let i = 0; i< children.length; i++) {
            let child = children[i];
            let {
                path = '/',
                exact = false
            } = child.props;
            let paramNames = [];
            # generate regular expressions
            letRegexp = pathToRegexp(path, paramNames, {end:exact});let result = pathname.match(regexp);
            if(result) { reutrn child; }}return null
    }
}
Copy the code

Link.js

The encapsulation of the A element

import React from 'react';
import ReactRouter from './context';
export default class Link extends React.Component {
    static contextType = RouterContext
    render() {
        return( <a {... this.props} onClick={() => this.context.history.push(this.props.to)}>{this.props.children}</a> ) } }Copy the code

Redirect.js

redirect

import React from 'react';
import RouterCOntext from './context';
export default class Redirect extends React.Component {
    static contextType = RouterContext;
    render() {
        this.context.history.push(this.props.to);
        returnnull; }}Copy the code

withRouter

The history parameter is empty when we apply the multi-layer component, which requires the higher-order component to pass the parameter

import React from 'react';
import Route from './Route';
export default function(WrappedComponent) {
    return props => <Route component={WrappedComponent} />
}
Copy the code

Prompt

Jump check, but basically useless

import React from 'react';
import RouterContext from './context';
export default class Prompt extends React.Component {
    static contextType = RouterContext;
    componentWillUnmount() {
        this.context.history.block(null)
    }
    render() {
        let history = this.context.history;
        const {when,message} = this.props;
        if(when) {
            history.block(message)
        }else {
            history.block(null)
        }
        returnnull; }}Copy the code