The foreword 0.
Front-end routing has always been a classic topic, both in daily use and in interviews. In this article, we implement a mini version of the React-Router to debunk the mystery of routing.
Through this article, you can learn:
-
What is front-end routing in essence
-
Some potholes and attention points in the front-end routing
-
Difference between hash route and history route
-
What does the Router component and Route component do
1. The nature of routing
To put it simply, browser-side routing is not actually a web hop (without any interaction with the server), but a set of actions that occur purely on the browser side. Essentially, front-end routing is:
Change and listen to the URL to make a DOM node display the corresponding view.
That’s all. Newbies should not be intimidated by the concept of routing.
2. Route differences
Generally speaking, there are two types of browser-side routes:
-
Hash routes are characterized by hash signs (#) after urls, such as baidu.com/#foo/bar/baz.
-
The url of the history route is the same as that of the common path. Such as baidu.com/foo/bar/baz.
We’ve talked about the nature of routing, so it’s really just a matter of figuring out how the two routes change and how the component listens and completes the presentation of the view.
Without further ado, what apis are used to implement front-end routing for each route:
2.1 the hash
Using location.hash = ‘foo’ to change the path from baidu.com to baidu.com/#foo.
The window.addeventListener (‘hashchange’) event is used to listen for changes to the hash value.
2.2 the history
PushState (history.pushState) ¶ pushState (history.pushState) ¶ pushState (history.pushState) ¶
**
history.pushState(state, title[, url])
-
State stands for state object, which allows us to create our own state for each routing record, and it is serialized and stored on the user’s disk so that it can be restored after the user restarts the browser.
-
Title is currently useless.
-
Url The most important url parameter in routing is, instead, an optional parameter, placed last.
PushState ({}, ”, ‘foo’) to change baidu.com to baidu.com/foo.
** Why does the browser page not reload after the path is updated? **
One thing to think about here is that a normal jump through location.href = ‘baidu.com/foo’ would cause the browser to reload the page and request the server, But the magic of **history.pushState**** is that it lets the URL change **, but does not reload the page, leaving it entirely up to the user to decide how to handle the URL change.
Therefore, front-end routing in this manner must be available on browsers that support the Histroy API.
Why 404 after refreshing?
The server does not map the url to baidu.com/foo, so it returns 404. The server returns index.html for pages that it does not recognize. In this case, the home page containing front-end routing related JS code will load your front-end routing configuration table. In this case, although the server gives you the home page file, but your URL is baidu.com/foo, the front-end route will load the view corresponding to the path /foo, perfect solution to the 404 problem.
The browser provides window.addeventListener (‘ popState ‘), but it only listens for route changes when the browser moves back and forward, not for pushState. There are solutions, of course, which will be discussed later when implementing the React-Router
3. Realize the react – mini – the router
The React-Router implemented in this paper is based on the history version and uses minimal code to restore the main functions of the route, so there are no high-level features such as regular matching or nested sub-routes, and the simplest version is achieved from zero to one by regression to the heart.
3.1 implement the history
For the official API of History that is difficult to use, we specially extract a small file to encapsulate it and provide it externally:
-
History. Push.
-
History. Listen.
These two apis lighten the mental load of the user.
We use the observer mode to encapsulate a simple Listen API that allows users to listen for path changes made by history.push.
// Store the history.listen callback
let listeners: Listener[] = [];
function listen(fn: Listener) {
listeners.push(fn);
return function() {
listeners = listeners.filter(listener= >listener ! == fn); }; }Copy the code
This allows the outside to pass through:
history.listen(location= > {
console.log('changed', location);
});
Copy the code
In this way, route changes are sensed, and in location, we also provide key information such as state, PathName, and search.
Implementing the core method push to change paths is also simple:
function push(to: string, state? : State) {
// Parse the url passed in by the user
// Decompose into pathname, search, etc
location = getNextLocation(to, state);
// Call the native history method to change the route
window.history.pushState(state, ' ', to);
// Execute the listener passed in by the user
listeners.forEach(fn= > fn(location));
}
Copy the code
When history.push(‘foo’) is invoked, it essentially calls window.history.pushState to change the path and tells the listen callback to execute it.
Of course, remember that popState is also used to listen for user clicks on the browser back and forward button, and performs the same processing:
// Handle browser forward and backward operations
window.addEventListener('popstate'.() = > {
location = getLocation();
listeners.forEach(fn= > fn(location));
});
Copy the code
Next we need to implement the Router and Route components, and you’ll see how they work with this simple history library.
3.2 implement the Router
The core principle of a Router isto use a Provider to transmit key routing information such as location and history to its sub-components, and to make the sub-components aware of the route changes:
import React, { useState, useEffect, ReactNode } from 'react';
import { history, Location } from './history';
interface RouterContextProps {
history: typeof history;
location: Location;
}
export const RouterContext = React.createContext<RouterContextProps | null> (null,);export const Router: React.FC = ({ children }) = > {
const [location, setLocation] = useState(history.location);
// Subscribe to changes in history at initialization time
// If the route changes, a child component using useContext(RouterContext) is notified to re-render
useEffect(() = > {
const unlisten = history.listen(location= > {
setLocation(location);
});
returnunlisten; } []);return (
<RouterContext.Provider value={{ history.location}} >
{children}
</RouterContext.Provider>
);
};
Copy the code
Note in the comments that we used history.listen to listen for route changes during component initialization. Once the route changes, setLocation is called to update the location and passed to the child component through the Provider.
This step also triggers a change in the Provider’s value, notifying all child components that have subscribed to history and location with useContext to rerender.
4. Realize the Route
The Route component accepts path and Children prop, which essentially determines what components need to be rendered in a path. We can get the current path through the location information passed by the Router’s Provider. So all this component needs to do is determine if the current path matches and render the corresponding component.
import { ReactNode } from 'react';
import { useLocation } from './hooks';
interface RouteProps {
path: string;
children: ReactNode;
}
export const Route = ({ path, children }: RouteProps) = > {
const { pathname } = useLocation();
const matched = path === pathname;
if (matched) {
return children;
}
return null;
};
Copy the code
The implementation here is relatively simple, the path directly uses the congruent, in fact the real implementation is more complex, using the path-to-regexp library to handle dynamic routing and other cases, but the core principle is actually that simple.
5. Implement useLocation and useHistory
Use useContext to simply wrap a layer and get the history and location passed by the Router.
import { useContext } from 'react';
import { RouterContext } from './Router';
export const useHistory = () = > {
returnuseContext(RouterContext)! .history; };export const useLocation = () = > {
returnuseContext(RouterContext)! .location; };Copy the code
6. Implement verification demo
At this point, the following routes are ready to run:
import React, { useEffect } from 'react';
import { Router, Route, useHistory } from 'react-mini-router';
const Foo = () = > 'foo';
const Bar = () = > 'bar';
const Links = () = > {
const history = useHistory();
const go = (path: string) = > {
const state = { name: path };
history.push(path, state);
};
return (
<div className="demo">
<button onClick={()= > go('foo')}>foo</button>
<button onClick={()= > go('bar')}>bar</button>
</div>
);
};
export default() = > {return (
<div>
<Router>
<Links />
<Route path="foo">
<Foo />
</Route>
<Route path="bar">
<Bar />
</Route>
</Router>
</div>
);
};
Copy the code
conclusion
Through the learning of this article, I believe that friends have already figured out the principle of front-end routing, in fact, it is just a package to the browser to provide API, and in the framework layer to do the corresponding rendering linkage, vue-Router framework is also a similar principle.