The Mini-Router is a lightweight front-end routing component library based on React. The purpose of the mini-Router is to help you understand the development principle of the front-end routing library and the practical process of developing routing components with React. For students who are new to front-end and React development, it is a good way to gain an in-depth understanding of front-end routing knowledge.

Hash and History routes

Generally, the routing library provides two routing modes: hash route and native route. Hash routes are prefixed with /# as the path. When the hash value changes, the browser TAB page does not refresh the web page. Native route Directly changes the path in the URL. You need to use the History API of the browser to control the route. If you directly change the path in the address bar of the browser, the current TAB will refresh the web page. Generally, different routing modes are selected based on project requirements.

The event Hashchange is triggered when the hash value of the hash route changes, and the hash value (which contains the current path information) can be retrieved via window.location.hash. The hash router functions on these two apis.

function locationHashChanged() {
  if (location.hash === '#cool-feature') {
    console.log("You're visiting a cool feature!"); }}window.onhashchange = locationHashChanged;
Copy the code

For native routes, browsers provide a History API for modifying route states such as pushState(), go(), and back(). When the route rollback is triggered, the popState event is triggered. ** Note that the popState event cannot be triggered with pushState. Native routers provide a History API that relies on the browser.

window.onpopstate = function(event) {
  alert(`location: The ${document.location}, state: The ${JSON.stringify(event.state)}`)
}

history.pushState({page: 1}, "title 1"."? page=1")
history.pushState({page: 2}, "title 2"."? page=2")
history.replaceState({page: 3}, "title 3"."? page=3")
history.back() // alerts "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back() // alerts "location: http://example.com/example.html, state: null"
history.go(2)  // alerts "location: http://example.com/example.html?page=3, state: {"page":3}"
Copy the code

Mini-rouer Routing component library

Mini-router is developed based on the React-router-DOM usage and API. Min-router similarly provides five core components:

  • <HashRouter>URL hashing based router component. /#/home /#/user/1000;
  • <HistoryRouter>HTML5 History State-based routing components. /home, /article/hell-mini-router, etc.
  • <Switch>A logical component that controls the Route component rendering, similar to the switch Case syntax of a programming language;
  • <Route>Render the routing container component of the corresponding path, and implement the render of the React component according to the configuration props.
  • <Link>The container component that controls the router’s forward function.

First, we will use create-react-app to create a React application, and create a mini-Router folder in the SRC directory. We will put all the code in this folder, and this project will be used to test our Mini-Router library.

In SRC /mini-router, create an entry file index.js and a utility class file utils. Js to export core components and store utility class functions, respectively.

// src/mini-router/index.js
import { HashRouter, HistoryRouter } from "./components/Router";
import Switch from "./components/Switch";
import Route from "./components/Route";
import Link from "./components/Link";

export { HashRouter, HistoryRouter, Switch, Route, Link };
Copy the code

Create a new Componnents folder that holds the code for our core components. In this folder, create four new files: router. js, switch. js, route. js, and link. js.

// src/mini-router/Router.js
function HashRouter(props) {}

function HistoryRouter(props) {}

export { HashRouter, HistoryRouter };

// src/mini-router/Switch.js
function Switch(props) {}

export default Switch;

// src/mini-router/Route.js
function Route(props) {}

export default Route;

// src/mini-router/Link.js
function Link(props) {}

export default Link;
Copy the code

Create a store folder that holds the code for our component’s data communication. Use the Context and useReducer provided by React to implement a lightweight state manager. At the same time, five new files are created in this folder respectively: actionCreators. Js, Constants.js, context.js, Reducer.js and index.js.

Sotre /index.js is used to export the Reducer function and our customized useRouterReducer hook, which is used for routing path communication.

// src/mini-router/store/index.js
import { useReducer } from "react";
import reducer, { defaultState } from "./reducer";

function useRouterReducer() {
  return useReducer(reducer, defaultState);
}

export { reducer, useRouterReducer };
Copy the code

That completes the basic project structure of the Mini-Router, and it’s time to refine the components, data layer, and utils.js code. For reader testing purposes, change SRC /index.js and SRC/app.js to the following code. Finally, after completing the Mini-Router, run the project to see the effect of our implementation of the routing component library.

// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { HistoryRouter } from "./mini-router";

ReactDOM.render(
  <HistoryRouter>
    <App />
  </HistoryRouter>.document.getElementById("root"));// src/App.js
import { Switch, Route, Link } from "./mini-router";

function App() {
  return (
    <div>
      <h1>mini-router</h1>
      <ul>
        <li>
          <Link to="/">/home</Link>
        </li>
        <li>
          <Link to="/foo">/foo</Link>
        </li>
        <li>
          <Link to="/bar">/bar</Link>
        </li>
      </ul>
      <Switch>
        <Route path="/" component={()= > <div>Hello Home Page!</div>} / ><Route path="/:id" component={()= > <div>Hello :id Page!</div>} / ><Route
          path="/foo"
          exact
          component={({ history}) = > (
            <div>
              <div>Hello Foo Page!</div>
              <ul>
                <li>
                  <Link to="/foo/a">/foo/a</Link>
                </li>
                <li>
                  <Link to="/foo/b">/foo/b</Link>
                </li>
              </ul>
              <Switch>
                <Route path="/foo/:id" component={()= > <p>Foo :id Page!</p>} / ><Route
                  path="/foo/b"
                  exact
                  component={()= > (
                    <p onClick={()= > history.goBack()}>Foo B Page!</p>)} / ></Switch>
            </div>)} / ><Route path="/bar" component={()= > <div>Hello Bar Page!</div>} / ></Switch>
    </div>
  );
}

export default App;
Copy the code

Data Layer development

Constants.js Stores all constants used by the data layer, including action and route types from the Reducer.

// src/mini-router/store/constants.js
export const CHANGE_PATH = "mini-router/CHANGE_PATH";
export const CHANGE_MODE = "mini-router/CHANGE_MODE";

export const HASH_MODE = Symbol("mini-router/HASH_MODE");
export const HISTORY_MODE = Symbol("mini-router/HISTORY_MODE");
Copy the code

Actionactioncreators. Js stores different functional actions, such as modifying the path attribute and modifying the route type attribute.

The hash2pathName (hash) method is used to take a hash value and convert it to a pathname structure, such as hash2pathName (‘#/hello/world’) = ‘/hello/world’.

// src/mini-router/store/actionCreators.js
import * as actionTypes from "./contants";
import { hash2pathname } from ".. /utils";

export function changePath(data) {
  return {
    type: actionTypes.CHANGE_PATH,
    data,
  };
}

export function changeHashMode() {
  return {
    type: actionTypes.CHANGE_MODE,
    path: hash2pathname(window.location.hash),
    mode: actionTypes.HASH_MODE,
  };
}

export function changeHistoryMode() {
  return {
    type: actionTypes.CHANGE_MODE,
    path: window.location.pathname,
    mode: actionTypes.HISTORY_MODE,
  };
}
Copy the code

Reducer.js modifies the reducer function of state and the variable defaultState of the defaultState. Mini-router requires only the path and mode attributes. Path indicates the path of the current route, and mode indicates the router type used by the application. When the path of the application is changed, the Reducer function recalculates the new state, and the

and

components determine which routing components will be rendered based on the new state.

import * as actionTypes from "./contants";

export const defaultState = {
  path: "".mode: null};function reducer(state = defaultState, action) {
  switch (action.type) {
    case actionTypes.CHANGE_PATH:
      return { ...state, path: action.data };
    case actionTypes.CHANGE_MODE:
      return { ...state, path: action.path, mode: action.mode };
    default:
      returnstate; }}export default reducer;
Copy the code

Context.js holds context variables for communication between routing components.

const { createContext } = require("react");

const routerContext = createContext();

export { routerContext };
Copy the code

Router components:<HashRouter>and<HistoryRouter>The development of

The

component is relatively simple, wrapping props. Children in << RouterContext. Provider> because other components rely on routing state to work. Value ={{routerState: state, routerDispatch: Dispatch}} Value ={{routerState: state, routerDispatch: dispatch}} In this way, other components can be accessed through the useContext hook function.

The

component has two side effects: changing mode to HASH_MODE and listening for hashchange events. Is the core function of this component. When the Hashchange event is triggered, the Path property of The routerState is modified to re-render the routing component.

import { useCallback, useEffect } from "react";
import { routerContext } from ".. /store/context";
import { useRouterReducer } from ".. /store";
import {
  changePath,
  changeHashMode,
  changeHistoryMode,
} from ".. /store/actionCreators";
import { hash2pathname } from ".. /utils";

function HashRouter(props) {
  const { children } = props;
  const [state, dispatch] = useRouterReducer();

  const handleHashChange = useCallback(() = > {
    dispatch(changePath(hash2pathname(window.location.hash))); } []); useEffect(() = > {
    dispatch(changeHashMode());
    window.addEventListener("hashchange", handleHashChange, false);
    return () = > {
      window.removeEventListener("hashchange", handleHashChange, false);
    };
    // eslint-disable-next-line} []);return (
    <routerContext.Provider
      value={{ routerState: state.routerDispatch: dispatch }}
    >
      {children}
    </routerContext.Provider>
  );
}
Copy the code

The

component is basically the same as the

component. The difference is that the side effect function changes mode to HISTORY_MODE and listens for popState events.

function HistoryRouter(props) {
  const { children } = props;
  const [state, dispatch] = useRouterReducer();

  const handlePopstate = useCallback(() = > {
    dispatch(changePath(window.location.pathname)); } []); useEffect(() = > {
    dispatch(changeHistoryMode());
    window.addEventListener("popstate", handlePopstate, false);
    return () = > {
      window.removeEventListener("popstate", handlePopstate, false);
    };
    // eslint-disable-next-line} []);return (
    <routerContext.Provider
      value={{ routerState: state.routerDispatch: dispatch }}
    >
      {children}
    </routerContext.Provider>
  );
}
Copy the code

Routing components:<Switch>and<Route>The development of

The

component renders the component that matches the path rule. Three parameters are accepted in props: path, exact, and Component. Path indicates the path (normal path or parameter path) that the current component matches. Exact indicates whether the current component matches completely (the priority is higher than the parameter path that meets the rule) and the component to be rendered.

The match(to, from) method checks whether two routes match. Both common paths and parameter paths are supported. For example: the match (“/foo “, “/ foo”) = true and match (“/foo / : id “, “/ foo/bar”) = true.

The isChildrenPath(path, child) method determines whether the second parameter path is a child of the first parameter path. For example: isChildrenPath(“/foo”, “/foobar”) = true

GetParams (path, pattern) parses the parameters in the parameter path and returns a parameter object.

GetQuery (QueryString) Parses the QueryString string and returns a Query object.

Push (to, mode) controls the route forward to the TO path, which is realized by calling different push functions according to the mode parameter.

GoBack () controls route rollback.

import { useCallback, useContext, useMemo } from "react";
import { routerContext } from ".. /store/context";
import {
  hash2pathname,
  match,
  isChildrenPath,
  getParams,
  getQuery,
} from ".. /utils";
import { HASH_MODE, HISTORY_MODE } from ".. /store/contants";
import { push, goBack } from ".. /utils";

// Displays the route view
function Route(props) {
  const { path, component: Component } = props;
  const { routerState: state } = useContext(routerContext);

  const getMatchPropsByMode = useCallback(
    (mode) = > {
      const data = {
        [HASH_MODE]: {
          match: {
            path: path,
            params: getParams(path, state.path),
            query: getQuery(window.location.hash),
            url: hash2pathname(window.location.search),
          },
        },
        [HISTORY_MODE]: {
          match: {
            path: path,
            params: getParams(path, state.path),
            query: getQuery(window.location.search),
            url: window.location.pathname,
          },
        },
      };

      return data[mode];
    },
    [path, state.path]
  );

  const matched = match(path, state.path);
  const isChildren = isChildrenPath(path, state.path);
  const matchProps = getMatchPropsByMode(state.mode);
  const historyProps = {
    history: {
      push: (to) = > push(to, state.mode),
      goBack: () = > goBack(),
    },
  };

  const hoc = useMemo(() = > {
    return <Component {. matchProps} {. historyProps} / >;
    // eslint-disable-next-line
  }, [matchProps]);

  return (matched || isChildren) && hoc;
}

export default Route;
Copy the code

The

component can only be used in scenarios where the

component is nested, and only the

component with the highest matching weight will be rendered. The

component is used to solve the problem where multiple

components will be rendered simultaneously if multiple paths match. Internally, weight calculation of

components is implemented, and the rules are as follows:





  • if<Route>componentexact = trueAnd the current path andpathProperty exactly the same (normal path), weight is0b100;
  • if<Route>The component’spathProperty matches the current path (match()), the weight is0b010;
  • If the current path belongs to<Route />The component’spathThe subpath of the property, with a weight of0b001;

The default weight value (weight) is 0, as long as you are responsible for one of the rules, and the weight value. After all the

component permissions are calculated, the

component with the highest permissions and no weight of 0 is taken out for rendering. Do not render if it does not exist.

import { useContext } from "react";
import { match, isChildrenPath } from ".. /utils";
import { routerContext } from ".. /store/context";

// Used to control the Route component display, similar to the programming language switch case syntax
function Switch(props) {
  const { children } = props;
  const { routerState: state } = useContext(routerContext);

  let route = null;
  let routes = null;

  if (Array.isArray(children) && children.length) {
    routes = children.map((route) = > {
      let weight = 0;

      if (route.props.exact && route.props.path === state.path) {
        weight |= 1 << 2;
      }
      if (match(route.props.path, state.path)) {
        weight |= 1 << 1;
      }
      if (isChildrenPath(route.props.path, state.path)) {
        weight |= 1;
      }

      return {
        route,
        weight,
      };
    });
    routes.sort((a, b) = > b.weight - a.weight);
    route = routes.length ? (routes[0].weight ? routes[0].route : null) : null;
  }

  return route;
}

export default Switch;
Copy the code

Controller components:<Link>The development of

The component is simpler to implement, rendering the A flag and controlling the default click event, replacing the logic of routing forward with the push method. Note that popState cannot be triggered when using the History API’s pushState method, so you need to manually notify the Path property in routerState to render correctly.

import { useCallback, useContext } from "react";
import { routerContext } from ".. /store/context";
import { HISTORY_MODE } from ".. /store/contants";
import { changePath } from ".. /store/actionCreators";
import { push } from ".. /utils";

// A component used to control jumps between routes
function Link(props) {
  const { to, children } = props;
  const { routerState: state, routerDispatch: dispatch } = useContext(
    routerContext
  );

  const handleClick = useCallback(
    (e) = > {
      e.preventDefault();
      push(to, state.mode);

      // It violates the single rule and is a hack fragment, resulting in partial logical coupling. It is recommended to use publish/subscribe mode for decoupling
      if (state.mode === HISTORY_MODE) {
        dispatch(changePath(window.location.pathname));
      }
    },
    [state]
  );

  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}

export default Link;
Copy the code

The final step, utils.js

In the implementation of the component and data layer used a number of functions provided by utils.js, here is the specific code implementation.

import { HASH_MODE, HISTORY_MODE } from "./store/contants";

// Convert window.location.hash to window.location.pathname
export function hash2pathname(hash) {
  if (typeofhash ! = ="string") {
    return "";
  }

  if (hash.length > 0 && hash[0= = ="#") {
    return hash.slice(1);
  }

  return "";
}

// Determine if it is a parameter placeholder for a parameter path
function isPathPlaceholder(path) {
  return path.length > 1 && path["0"= = =":";
}

// Match two paths. Parameter path matching is supported
export function match(to, from) {
  if (typeofto ! = ="string" || typeof from! = ="string") {
    return false;
  }

  to = to.split("/");
  from = from.split("/");

  if(to.length ! = =from.length) {
    return false;
  }

  for (let i = 0; i < to.length; i++) {
    if(to[i] ! = =from[i] && ! isPathPlaceholder(to[i])) {return false; }}return true;
}

// Check whether the current path is a child of the target path, e.g. /foo/bar is a child of /foo
Nesting Is a solution to nesting
export function isChildrenPath(path, child) {
  if (typeofpath ! = ="string" || typeofchild ! = ="string") {
    return false;
  }
  if (path.length <= 1 || child <= 1) {
    return false;
  }
  if (path.length === child.length) {
    return false;
  }

  return child.startsWith(path);
}

// Obtain the corresponding parameter object based on the actual path and parameter path
export function getParams(to, from) {
  if (typeofto ! = ="string" || typeof from! = ="string") {
    return {};
  }

  to = to.split("/");
  from = from.split("/");

  if(to.length ! = =from.length) {
    return {};
  }

  let res = {};

  for (let i = 0; i < to.length; i++) {
    if (isPathPlaceholder(to[i])) {
      res[to[i].slice(1)] = decodeURIComponent(from[i]); }}return res;
}

// Get the route parameter object
export function getQuery(querystring) {
  if (querystring.indexOf("?") = = = -1) {
    return {};
  }

  querystring = querystring.slice(querystring.indexOf("?") + 1).split("&");

  if (querystring.length) {
    querystring = querystring.map((item) = > item.split("="));
  }

  let res = {};

  querystring.forEach(([key, value]) = > {
    res[key] = value;
  });

  return res;
}

// Control the browser TAB forward
export function push(to, mode) {
  switch (mode) {
    case HASH_MODE:
      pushOfHashRouter(to);
      break;
    case HISTORY_MODE:
      pushOfHistoryRouter(to);
      break; }}function pushOfHashRouter(to) {
  window.location.hash = ` #${to}`;
}

function pushOfHistoryRouter(to) {
  window.history.pushState({}, "", to);
}

// Control the browser TAB back
export function goBack() {
  window.history.back();
}
Copy the code

conclusion

As one of the core functions of single page application (SPA), front-end routing library is an important skill that Web front-end developers need to master. If we can develop a simple routing library on the basis of proficient use of front-end routing library, we can deepen the understanding of this knowledge domain. Once you are familiar with the two common routing patterns on the front end and the implementation principles behind them, it is not too difficult to develop a proprietary routing library based on any framework. This article implements a simple routing library from zero to one based on React. We hope it will be helpful for you to develop the routing library and practice React.

All the code used in this article: Q545244819 / Mini-router

reference

  • Window: hashchange event – Web APIs | MDN
  • History API – Web APIs | MDN
  • History.pushState() – Web APIs | MDN
  • react-router-dom