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