The react-Router has been implemented from 0 to 0. The react-router has been implemented from 0 to 0. The react-router has been implemented from 0 to 0

As for what react-Router helps us to implement, I won’t elaborate too much. I’ll just go to the official document and talk about the implementation

The react to the router’s official website: reactrouter.com/web/guides/…

In addition, the react-Router source code relies on two libraries, Path-to-Regexp and History, so I will introduce these two libraries directly here. Although I will talk about the basic use of the following, students can read the following official documents if they have time

The path – to – the regexp document: www.npmjs.com/package/pat…

History library documentation: github.com/ReactTraini…

One more thing to note: here’s what I wroterouterThe principle is to usehooks+ function component to write, and the official is written using class components, so if you are righthooksIs not very clear, have to fill in the knowledge of this aspect, why choosehooksBecause now most of the big factories are inreactOn the basic are strongly recommended to usehook, so we have to keep up with The Times is not, and I focus on and we talk about the principle, rather than with the official source code exactly the same, if you want to 1 to 1 copy source code without their own understanding, then you go to see the official source code on the line, why see this blog post

In this column blog, we’ll talk about the following:

  1. Encapsulate your own generationmatchObject methods
  2. historyThe use of libraries
  3. RouterandBrowserRouterThe implementation of the
  4. RouteComponent implementation
  5. SwitchandRedirectThe implementation of the
  6. withRouterThe implementation of the
  7. LinkandNavLinkimplementation
  8. The aggregationapi

Encapsulate your own generationmatchObject methods

Before wrapping, I want to share the path-to-Regexp library with you

The main reason why we talk about this library first is because it’s used by the React-Router. I looked at it and I didn’t think it was necessary to implement it ourselves. It’s because the library implements something very simple, but the details are so tedious that there are so many factors to consider that I don’t think it’s necessary to), and the library does something very simple: turn a string into a regular expression

As we know, the react-Router basically renders different pages based on different paths, so this process is actually the process of path A matching page B, so we wrote this code before

<Route path="/news/:id" component={News} /> // If the path matches a path like /news/:id, render the news component
Copy the code

How does a React-router determine if the browser address bar’s path matches the Route component’s path attribute?

If the path is /news/:id, then /news/123 /news/321 will be matched by the React-Router

Is the method we can think of roughly as follows:

Convert all path attributes to regular expressions (e.g. /news/:id to /^\/news(? : \ / ([^ # \ \ /?] +? ) / # \ \ /? ? $/ I), and then take the path value from the address bar and match it with the regular expression. If the path value matches, render the corresponding route. If the path value does not match, render the other logic

path-to-regexpThat’s what he does. He converts the path string we gave him into a regular expression for us to match

Installation: yarn add path-to-regexp -sCopy the code
// We can play around with this library
import { pathToRegexp } from "path-to-regexp";

const keys = [];

// pathToRegexp(path, keys? , options?)
// path: is the path rule we want to match
// keys: if you pass it, when it matches, the corresponding argument key will be passed to the keys array
// Options: add additional rules to the path rule, such as sensitive and case-sensitive
const result = pathToRegexp("/news/:id", keys);

console.log("result", result);

console.log(result.exec("/news/123")); // output ["/news/123", "123", index: 0, input: "/news/123", groups: undefined]
console.log(result.exec("/news/details/123")); / / output is null
console.log(keys); / / output is an array, the array is an object {modifier: "name:" id ", the pattern: "[^ # \ \ /?] +?" , prefix: "/", suffix: ""}
Copy the code

Of course, this library has a lot of gameplay, it is not specifically for the React-Router implementation, it just happened to be used by the React-Router, students interested in this library can check out its documentation

The main reason we use this library isto encapsulate a public method and provide some building blocks when we write router source code, because we know that the React-Router will inject history, location and other properties into the component once the path matches. We have to prepare these things in advance, so our goal at the moment is very simple

If a path value matches the specified path regex, we will generate an object containing the location, history, and other properties for later use. To be more explicit, we will get the match object of the react-router

We will find that this feature is independent, split him like that can be used in any place, as long as the match I will generate an object, I also no matter what are you doing with the objects to not pass I fart matter, this is also in software development, a better way of development, we can stop here to carefully consider the benefits

So what I’m going to do next is very simply encapsulate a process path-related approach that will provide basic support as we develop other Router capabilities

Let’s create our own react-router directory in the React project and create a new file pathmatch.js in it

This also means that we will no longer pull the React-Router from NPM, but will refer to the React-Router directly in our own projects

Each step in pathmatch.js has comments that should help you understand

// src/react-router/pathMatch.js
import { pathToRegexp } from "path-to-regexp";


/ * * * *@param {String} Path Specifies the path rule * passed in@param {String} Url Indicates the URL * to verify the PATH rule@param {Object} * * What this function does is very simple. When I call this function and pass the * argument, the function returns an object with the following members: * {* params: * key: value *}, * path: path rule * URL: Url that matches the path rule. If the path rule does not match, it is null * isExact: Whether to exactly match *} * */
function pathMatch(path = "", url = "", options = {}) {
  // So inside this function, we do the following:
  // 1. Call the Path-to-regex library and help us match parameter values according to the configuration
  // 2. Return the matching result

  // First, if you read the path-to-regex document, you will notice a problem
  // We pass exact as an exact match in the React-router, but we use end in this library
  // So the first step is to change the configuration object passed by the user into the configuration object that the path-to-regex wants
  const matchOptions = getOptions(options);
  const matchKeys = []; // matchKeys is an array that we use to hold the key of the argument after the match

  // Then obtain the corresponding regular expression in path-to-regexp
  const pathRegexp = pathToRegexp(path, matchKeys, matchOptions);

  // Here we use the corresponding regular expression to match the url passed by the user
  const matchResult = pathRegexp.exec(url); 
  
  console.log("matchResult", matchResult);
  // Return null if no match is found
  if( !matchResult ) return null;

  // If it matches, we know it returns an array of classes. We need to iterate through matchKeys and the array of classes
  // Generate the params object in the final match object
  const paramsObj = paramsCreator(matchResult, matchKeys);

  return {
    params: paramsObj,
    path,
    url: matchResult[0].// matchResult as the 0th item in the class array is the part of the matching path rule
    isExact: matchResult[0] === url
  }
}

/ * * * *@param {Object} Options * This method converts the configuration object passed by the user to the configuration object required by the path-to-regex */
function getOptions({ sensitive = false, strict = false, exact = false }) {
  const defaultOptions = {
    sensitive: false.strict: false.end: false
  }
  return {
    ...defaultOptions,
    sensitive,
    strict,
    end: exact
  }
}


/ * * * *@param {*} matchResult 
 * @param {*} MatchKeys * This method combines matchResult and matchKeys to generate a new Params object */
function paramsCreator(matchResult = [], matchKeys = []) {
  // First matchResult is an array of classes, and we need to convert it to a real array
  // You can use array. from, [].slice.call, etc
  // And we know that the first term of matchResult is the path, we don't need it, so it is more convenient to directly use slice.call
  const matchVals = [].slice.call(matchResult, 1);
  const paramsObj = {};
  matchKeys.forEach((k, i) = > {
    // Remember that k is an object, and we only need its name attribute
    paramsObj[k.name] = matchVals[i];
  })

  return paramsObj; // Throw the parameter object out
}


export default pathMatch;
Copy the code

At this point, our pathMacth module is generated, and every time we call the pathMatch method, it returns us a match object in the React-Router based on the argument

historyThe use of libraries

We know that when a react-router matches a component, the React-router will inject some properties into the component. We already have a method for generating the match property, but we have to write the location and history ourselves

Actually,locationishistoryObject. We’re donelocation.historyIt will take care of itself

One thing we need to know is that the history method is used to help us switch routes, but we know that our router mode has a hash mode, the browser mode (sometimes called history mode) mode, and even memory mode on the native side. We also know that history helps us operate in different ways (e.g. hash mode, hash mode, browser mode, browser history). Router is based on whether you’re introducing a BrowserRouter or some other type of router, so what we’re going to do is we’re going to make this BrowserRouter, okay, because it’s probably a lot of code, but the principle is the same, I’m not going to write HashRouter or memoryRouter

In the React-Router, it also relies heavily on the third party library we mentioned above: History

Let’s take a look at the history library first, and maybe in the next blog we’ll go straight to the principles. Unlike Path-to-Regexp, the principles are still important, and this blog will not write the source code for the history library due to the length of the article

One of the main functions of this library isto provide you with a History API for creating different address stacks

To put it more simply, we call the library’s named export method, and through a lot of wrapping, we can directly generate the History object provided in the React-Router context

We can just use this library

import { createBrowserHistory } from "history"; // Import a function that creates the operation browser history API

// This function can also receive a configuration object, or you can not pass it
// createBrowserHistory(config?) ;
const history = createBrowserHistory({
  // The basename configuration is used to set the base path. In most cases, our site's root path is /
  // So we won't consider basename most of the time. If you need to consider basename, just fill it in here
  // If you set basename to /news, you can visit /news/details
  // your pathname will be resolved to /details
  basename: "/".forceRefresh: false.// Specifies whether to force the page refresh. The History API does not refresh the page. If set to true,
  // When you call a method such as push, the page will be displayed digitally
  keyLength: 6.// The length of the key value used by the location object. (The key value is used to determine uniqueness. For example, if you access the same path at the same time, there is no key value.)
  getUserConfirmation: (msg, cb) = > cb(window.confirm(msg)), // To determine if the user really needs to jump (but only if the history block function is set and the page actually jumps)
});
console.log("history");
Copy the code

The history object in the React context object provided by the BrowserRouter is the same as the history object in the React context object, but there are a few subtle differences. This is useful for us to write his source code later

Need to pay attention to the place is: students do not think this iswindow.locationandwindow.historyWell, this is a combination of thetahistorySelf generated objects, his opposite attributes are a lot of packaging, do not be confused, we will understand the source code a bit more clear

  • Action: The action represents the type of the last action on the current stack.
    • For the first time throughcreateBrowserHistoryWhen you create itactionFixed forPOP
    • If you callhistorythepushMethod,actionintoPUSH
    • If you callhistorythereplaceMethod,actionintoREPLACE
  • Push: pushes an address to the current address stack pointer
  • Go: controls the offset of the current stack pointer. If it is 0, the address does not change (we know that the browser’s history.go(0) will refresh the page)
  • The goBack: the equivalent ofgo(-1)
  • GoForwar: the equivalent ofgo(1)
  • Replace: Replaces the address of the pointer
  • listen: This function is used to listen for changes in the address stack pointer. This function takes a function as an argument to call back when the address changes. The callback function takes two arguments (the location object, the action). It returns a function to unlisten, which I’m sure you’ll understand when we use it later
  • Location object: represents information in the current address bar
  • CreateHref: pass a Location object in. It generates an address for you based on the contents of the location
  • Block: Sets a block that is triggered when the user jumps to the page, and information about the block is passed togetUserConirmationIn the

RouterandBrowserRouterThe implementation of the

With that said, we’re just talking about path-to-Regexp and the History library, so we’re going to officially implement the Router component

In React, the Router component is used to provide context, while the BrowserRouter creates a History object that controls the browser’s history API and passes it to the Router

We create a new file in the React-router, route.js, and we create a new routerContext.js to store the context

// react-router/RouterContext.js
import { createContext } from "react";

const routerContext = createContext();

routerContext.displayName = "Router";

export default routerContext;
Copy the code

// We know that the Router component must have a history object. It doesn't matter how the history object came from, but it must be passed to it via a property
import React, { useState, useEffect } from "react";

import pathMatch from "./pathMatch";

import routerContext from "./RouterContext";

/** * The Router component does only one thing: He's going to provide a context * the context is history, match, location * * and we know that when we create history, we have createBrowserHistory, CreateHashHistory, etc. * So we can't write anything dead inside the Router, we pass history as a property * and outside we're creating different histories for different components and passing them to the Router component, * React does the same thing *@param {*} props 
 */
export default function Router(props) {
  // We write the following logic in the Router:
  // 1. Take the match object, location object, and History object and mix them together
  // 2. If the page address changes and the Router needs to rerender in response to the change, how does the Router respond

  // The main reason for changing location to state is because there are several things we need to do when our page address changes
  // - Change the status of the action in history, for example go to POP, push to push, if we don't have our own state
  // So we have no place to change the location
  // - When the page address changes, we need to re-render the component, we can use listen to listen, but re-render the component ourselves
  // You can use your own forceUpdateHook to handle this, but if you have a location status, you can kill two birds with one rock
  const [locationState, setLocationState] = useState(props.history.location); 
  const [action, setAction] = useState(props.history.action);
  useEffect(() = > {
    const removeListen = props.history.listen(({location, action}) = > {
      // When the page address changes, I want to be able to listen to it, and then I want to refresh the component again
      setLocationState(location)
      setAction(action);
    })
    return removeListen;
  }, [])

  const match = pathMatch("/", props.history.location.pathname);
  return (
    <routerContext.Provider value={{
      match.location: locationState.history: {
        . props.history.action
      }
    }}>
      { props.children }
    </routerContext.Provider>)}Copy the code

The Router component is not complete enough, we need to write browserrouter. js component to create a react-router-dom directory under SRC, create files index.js and browserrouter.js

// index.js
export { default as BrowserRouter } from "./BrowserRouter.js";
Copy the code
// BrowserRouter.js
// What BrowserRouter does is very simple. It creates a History object that controls the History API
// Pass it as an attribute to the Router component
import React from "react";
import Router from ".. /react-router/Router.js";
import { createBrowserHistory } from "history";

export default function BrowserRouter(props) {
  const history = createBrowserHistory(props);
  return (
    <Router history={history}>
      { props.children }
    </Router>)}Copy the code

At this point our BrowserRouter component is finished

RouteComponent implementation

The Route component is basically used to match different components based on different paths. It’s not that complicated, it’s just to render different components by different paths. If you write a little bit sloppy, you can always use if else to judge all the time and you can also write a Route component. Take a look at the implementation of the Route component

We set up the rout.js file in the React-router

import React from "react";
import pathMatch from "./pathMatch";
import routerContext from "./RouterContext";
// First, we need to know something about the process:
// 1. The Route component will have the following properties:
// - path
// - children
// - component
// - render

// - sensitive
// - strict
// - exact

// In chilren, Component, render there are some logic rules as follows:
// children: As long as you give the value of the children attribute, chilren will show whether the route matches successfully or not
// render: the function to execute once the match is successful
// Component: The component that will be rendered once the match is successful

// Children > render > Component

// Of course you can use propTypes to constrain some props. You can also use ts to constrain props
// I will not be bound, a little lazy haha
export default function Route(props) {

  // As a Route component, it also has history, location and match objects
  // You can reassemble these objects yourself, but I don't think it's necessary
  // Use the data in the context, but we do have to redo the match object
  // Match
  return (
    <routerContext.Consumer>{ value => { const { location, history } = value; Location, history const {exact = false, exact = false, strict = false} = props; const match = pathMatch(props.path, location.pathname, { sensitive, exact, strict }) const ctxValue = { location, Return (history, match}) return (history, match})<routerContext.Provider value={ctxValue}>
              { getRenderChildren(props.children, props.render, props.component, ctxValue) }
            </routerContext.Provider>)}}</routerContext.Consumer>)}/** * Render the rendered element according to certain matching logic * this is the core function of the Route component */
function getRenderChildren(children, render, component, ctxValue) {

  // From our previous logic, we know that once the children attribute has a value, it is needless to say that we simply ignore the other values
  if( children ! =null ) {
    // We know that we can write a function, which can get the value of the context
     return typeof children === "function" ? children(ctxValue) : children;
  }
  
  // If there is no value for children, it is a match. If there is no match, it is a match
  if( ctxValue.match == null ) return null;

  // This means that the match is made. If the match is made, then run render directly
  if( typeof render === "function" ) return render(ctxValue);

  // Finally render the Component
  if( component ) {
    let Component = component;
    // We know that the component to be matched also has location, history, match, etc
    return <Component {. ctxValue} / >
  }

  // The component has no component

  return null; // Give him null as usual

}

Copy the code

In fact, here we are withreact-routerThere’s one more difference. When hisRouteIf the path component is not available, it will render the matching component directly. I didn’t write it here, why? Because I think it’s illogicalpathDidn’t give me what I do for you, ha, why should I mention this because I think we have to learn a framework or a thing, want to bring your own logic to learn (why did he do this, for example, if you are you do), he is not necessarily the right, you also is not necessarily wrong, you know his logic, If you feel unreasonable, you must keep your own logic, so as to avoid being a learning machine, and can exercise our thinking ability

The Route component is now complete

SwitchandRedirectThe implementation of the

The implementation of the Switch function is very simple, because we need to wrap Swicth around the Route component, so we think about the logic in a second, we just need to pass the children property in the Switch and control the rendering. If you use an official Switch, the component that you don’t match will not exist in the React component tree

Let’s create a new switch.js in the React-router directory

// react-router/Swicth.js
import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";

export default function Swicth(props) {
  / / we want to do is: the props in the children turn out, and then if which match the path of the path and the current path
  // Render, and once one is rendered, none will be rendered again
  // How do we know the current path
  return (
    <routerContext.Consumer>{ value => { const { location } = value; const {children = {}} = props; // In this case, we are going to iterate over the children, but before we iterate over the children, we need to know that the children can be multiple cases // 1. Array: proof that multiple react elements were passed in, we don't care about // 2. ResultChildren = [] resultChildren = []; resultChildren = []; if( children instanceof Array ) resultChildren = children; else if( children instanceof Object ) resultChildren = [children]; for( const item of resultChildren ) { const { path = "", exact = false, sensitive = false, strict = false, component: Component = null } = item.props; // We know that location.pathname is the actual browser address, The Route component we write is the path rule // so we can only match using the pathMatch function we encapsulated earlier const match = pathMatch(path, location.pathname, {exact, Sensitive, strict}) if(match! = null ) { console.warn("i am warning"); return Component == null ? Component :<Component />}} return null if no match is found; }}</routerContext.Consumer>)}Copy the code

The Swicth component is complete. In fact, these components are not very difficult, as long as you follow the logic of it, it must be possible to implement

Now all we need to do is implement our Redirect component by creating a new redirect.js in the React-router directory

// react-router/Redirect.js
// The Redirect component is used to do redirection, but the logic can be very simple. When you encounter the Redirect component, you go through the location
// the replace method will render it to the specified path

import React from "react";
import routerContext from "./RouterContext";
import pathMatch from "./pathMatch";

export default function Redirect( props ) {
  console.log("I got a match.")
  // We know that Redirect accepts the following attributes
  // 1. From: indicates the matched path
  // 2. To: indicates the path to go after matching to the path. If to is an object, it can take parameters
        // -pathname: match the path to the future
        // -search: indicates the normal search
        // -state: specifies the state you want to attach
        // The pathname is an object, so you can parse the pathname and throw the parameters as attributes
  // 3. Push: whether to use history.push (because it uses replace by default)
  // The other attributes are exact, sensitive, strict
  const { from = "", to = "", push = false, exact = false, sensitive = false, strict = false } = props;
  // This is where we compare from to the current location, so we use context again
  return (
    <routerContext.Consumer>{ ({location, history}) => { console.log("props", props); const match = pathMatch(from, location.pathname, { strict, sensitive, exact }) if( match ! = null) {// it matches, and all we need to do is push it to the corresponding component console.log("to", to); // If you do not put history.push in the asynchronous queue, the listen event may not be initialized yet, and then it will not listen. SetTimeout (() => {history.push(to)}, 0)} // Return null if no match is found; }}</routerContext.Consumer>)}Copy the code

At this point, the Redirect component is complete

withRouterThe implementation of the

This is a hoc that simply injects the routing context into the component as an attribute

Let’s create a new withrout.js in the React-router directory

import React from "react";
import routerContext from "./RouterContext";

export default function(Comp) {
  // It accepts a Comp as an argument and returns a new component
  function newComp(props) {
    return (
      <routerContext.Consumer>
        {
          values => (<Comp {. props} {. values} / >)}</routerContext.Consumer>)}// Set the name of the display
  newComp.displayName = `withRouter(${Comp.displayName || Comp.name}) `;
  return newComp;
}
Copy the code

LinkandNavLinkimplementation

After writing this Link and NavLink, I was basically paralyzed, but fortunately, I finally finished writing the Link and NavLink themselves are not hard

If you want to make it easier, just write an A element to block the default event and use history. Push

Let’s create a new link.js in react-router-dom

// react-router-dom/Link.js
import React from "react";
import routerContext from ".. /react-router/RouterContext";

export default function Link(props) {
  const{to, ... rest} = props;return (
    <routerContext.Consumer>
      {value => {
        return (
          <a href={to} {. rest} onClick={e= >{ e.preventDefault(); // There are some cases where to is an object // And there are some cases where to needs to be passed, so you need to write some functions to help you parse the string or parse the object // There are some cases where you need to write some functions to help you parse the string or parse the object. So it's a good idea to use history.createhref to generate the address // depending on whether a parameter is replace or push // but that's the core idea, Value.history. push(props. To); }}> { props.children }</a>)}}</routerContext.Consumer>)}Copy the code

If the location matches, it will give you a class name

Let’s create a new navlink.js under react-router-dom

// react-router-dom/NavLink.js
import React from "react";
import Link from "./Link";
import routerContext from ".. /react-router/RouterContext"
import pathMatch from ".. /react-router/pathMatch";


export default function(props) {
  const {activeClass = "active", to = "". rest} = props;return (
    <routerContext.Consumer>
      { value => {
        const match = pathMatch(to, value.location.pathname, )
        console.log("match result", match);
        return (
          <Link to={to} className={match ? activeClass : ""}  {. rest} >{ props.children }</Link>)}}</routerContext.Consumer>)}Copy the code

So far Link and NavLink we have finished, but Link and NavLink have a lot of need to improve the place, I just output the core principle, you have ideas can be their own supplement

The aggregationapi

We know that the code that we introduced in the React-router was directly introduced in the react-router-dom, and it’s not hard to export it

// react-router-dom/index.js
export { default as Redirect } from ".. /react-router/Redirect";
export { default as Route } from ".. /react-router/Route";
export { default as Router } from ".. /react-router/Router";
export { default as Switch } from ".. /react-router/Switch";
export { default as withRouter } from ".. /react-router/withRouter";
export { default as Link } from "./Link";
export { default as NavLink } from "./NavLink";
Copy the code

That’s all right

At this point, the end of the hope to be able to have a large hand sub point to teach 0.0