preface

Front-end routing has always been a classic topic, both in daily use and in interviews. This article demystified routing together by implementing a simple version of the React-Router.

Through this article, you can learn:

  • What front-end routing is essentially.
  • 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?

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.

Routing differences

Generally speaking, there are two types of browser-side routes:

  1. Hash routes are characterized by urls followed by hash routes#, such asbaidu.com/#foo/bar/baz.
  2. The url of the history route is the same as that of the common path. Such asbaidu.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:

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.

history

PushState (history.pushState) ¶ pushState (history.pushState) ¶ pushState (history.pushState) ¶

history.pushState(state, title[, url])

Where state stands for state object, this allows us to create our own state for each routing record, and it is also 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?

Here’s the thing to think about. Normally, using 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 makes the URL change. But instead of reloading the page, it’s 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

To 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.

To realize 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:

  1. history.push.
  2. 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.

To realize 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.

To 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.

UseLocation and useHistory are implemented

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

Implement validation 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.

If you want to further study the complete source code of this article, you can pay attention to the public number “front-end from advanced to hospital”, reply to “routing”, you can get the complete source code, pay attention to the first-hand dry goods information, but also get the “advanced guide” and “front-end algorithm zero-based advanced guide”.