sequence

There is a sense that using the React-router-dom when writing projects is very convenient to use, but difficult to maintain because the roads are scattered across components. The config mode provided in react-router-dom is used to write routes. The advantage of this mode is that we can centralize the logic in one place and configure routes easily

The project address

Gitee.com/d718781500/…

1. Centralized routing

Let’s start by defining the following data in/SRC /router/index.js

The React routing official documentation provides an example of configuring centralized routing, which is similar to a VUE route and generates a configuration file, as expected

// A route configuration is required, which is an array
import Discover from ".. /pages/Discover"
import Djradio from ".. /pages/Discover/Djradio"
import Playlist from ".. /pages/Discover/Playlist"
import Toplist from ".. /pages/Discover/Toplist"
import Friends from ".. /pages/Friends"
import Mine from ".. /pages/Mine"
import Page404 from ".. /pages/Page404"
const routes = [
    {
        path: "/friends".component: Friends
    },
    {
        path: "/mine".component: Mine
    },
    
    {
        path: "/discover".component: Discover,
        children: [{path: "/discover/djradio".component: Djradio
            },
            {
                path: "/discover/playlist".component: Playlist

            },
            {
                path: "/discover/toplist".component: Toplist
            }
        ]
    },
    {//Page404 This configuration must be after all routes are configured
        path: "*".component: Page404
    }
]

export default routes
Copy the code

We can use the above configuration to generate a route. The configuration of the redirect exact is not written. Let’s start with a simple configuration

2. File directories

The above configuration uses a centralized routing configuration pattern similar to VUE, so let’s show the structure directory of my current demo

Project directory structure

SRC /pages directory structure

├ ─ Discover │ │ ABC. Js │ │ index. The js │ │ │ ├ ─ Djradio │ │ │ index. The js │ │ │ lf. Js │ │ │ │ │ └ ─ gv │ │ index. The js │ │ │ ├ ─ Playlist │ │ index. Js │ │ │ └ ─ Toplist │ index. The js │ ├ ─ Entertaiment │ index. The js │ ├ ─ Friends │ index. The js │ xb. Js │ ├ ─ mime │ Index. Js │ └ ─ Page404 index. JsCopy the code

With these structures in place, the import files mentioned in 1 can be combined to look uncomplicated. We can then encapsulate a component called CompileRouter which is used to compile routes

3. Create CompileRouter

This component is created in SRC /utils, which calculates the component through the incoming route configuration, so the question is, why create this component?

Let’s review how the React route is written. The React route requires a base component, the HashRouter or BrowserRouter, which act as a cornerstone component

Then you need a routing recipe that accepts a path map to a Component

So let’s write some pseudocode to illustrate that

// Install NPM I react-router-dom in your project.
import {HashRouter as Router,Route} from "react-router-dom"
class Demo extends React.Component {
    render(){
        // Base road
        <Router>// Route recipe component matches Component by path<Route path="/" component={Home}/>
             <Route path="/mine" component={Mine}/>
        </Router>}}Copy the code

This is the basic usage, so our job for CompileRouter is to generate a Route just like the one in the code above and then display it on the component

Now that we understand the basics of Compile, let’s start coding

The design for CompileRouter is to receive data in an array that matches the route configuration, as shown in the 1 code, with the attribute routes

// This file is compiled using the routes configuration
import React from 'react'
import { Switch, Route } from "react-router-dom";
export default class CompileRouter extends React.Component {
    constructor() {
        super(a)this.state = {
            c: []}}renderRoute() {
        let { routes } = this.props;// Get the routes configuration
        //1. Generate the Route component using routes
        // Make sure routes is an array
        // console.log(routes)
        // Render does not repeat calls to componentDidMount and componentWillUnmount
        if (Array.isArray(routes) && routes.length > 0) {
            // Make sure the routes passed in is an array
           // Loop over routes passed in
            let finalRoutes = routes.map(route= > {
                // Each route looks like this {path:" XXX ", Component :" XXX "}
                / / if the route have child nodes {path: "XXX", component: "XXX", the children: [] {path: "XXX"}}
                return <Route path={route.path} key={route.path} render={// If there is a nested route, then we can change the routerouteIn thechildrenThe configuration data is passed to the component for the component to call againCompileRouter> < span style = "box-sizing: border-box; <route.component routes={route.children} />} / >
            })

            this.setState({
                c: finalRoutes
            })
        } else {
            throw new Error('Routes must be an array and have a length greater than 0')}}componentDidMount() {
        // Make sure the Route component is computed the first time renderRoute is called
        this.renderRoute()
    }
    render() {
        let { c } = this.state;
        return (
            <Switch>
                {c}
            </Switch>)}}Copy the code

The above code is used to handle routes data and declare such a component. I’ve commented out what each step does

4. Use CompileRouter

In fact, we can think of the encapsulated component as a view component in vue-Router
for the moment, and then we need to render level 1 routing on the page

In the SRC/app. Js

import React from 'react'
import { HashRouter as Router, Link } from 'react-router-dom'
// Import our encapsulated crime CompileRouter
import CompileRouter from "./utils/compileRouter"
Import the routing configuration data defined in 1
import routes from "./router"
console.log(routes)
class App extends React.Component {
    render() {
        return (
            <Router>
                <Link to="/friends">A friend</Link>
                |
                <Link to="/discover">found</Link>
                |
                <Link to="/mine">my</Link>{/* As a view component of vue-Router we need to pass routing configuration data */}<CompileRouter routes={routes} />
            </Router>)}}export default App
Copy the code

When this is done, the page should actually display level 1 routing perfectly

5. Nested routines are handled by

We have rendered the level 1 route above and can jump, but what about the level 2 route? This is easy, just find the parent of the secondary route and continue to CompileRouter

We can see from the configuration that the Discover route has nested routines, so we take the Discover route as an example. First, let’s look at the structure diagram

The index.js is the Discover view component, which is the parent of the nested route, so we just need to continue to use CompileRouter in this index.js

import React from 'react'
import { Link } from "react-router-dom"

import CompileRouter from ".. /.. /utils/compileRouter"
function Discover(props) {

    let { routes } = props // This data was passed to children when ComileRouter was compiled
    // console.log(routes)
    let links = routes.map(route= > {
        return (
            <li key={route.path}>
                <Link to={route.path}>{route.path}</Link>
            </li>)})return (
        <fieldset>
            <legend>found</legend>
            <h1>I found out you can't say drink more hot water</h1>
            <ul>
                {links}
            </ul>{/* The children data can render Route*/}<CompileRouter routes={routes} />
        </fieldset>
    )
}
Discover.meta = {
    title: "Discovered".icon: ""
}
export default Discover
Copy the code

So we’re going to remember, whenever there’s a nested pattern there are two things we’re going to do

  1. Configure routes
  2. Used again in parent routes of nested routinesCompileRouterAnd passed inroutesCan be

6. require.context

Above we implemented a centralized routing configuration, but we found a problem

A lot of components are introduced, in fact, more are introduced in the project, and it would be disastrous for us to introduce them one by one, so we can use a nice API that Webpack provides,require.context let’s talk about how it works first

Import require.context automatically, using this method can reduce tedious component import, and can deeply recursive directory, do things that import cannot do

use

You can create your own context by using require.context().

We can pass four arguments to this function:

  1. A directory to search,

  2. A tag indicates whether to search for subdirectories as well,

  3. A regular expression that matches a file.

  4. Mode Indicates the loading mode of the module. The common values are sync, lazy, lazy-once, and eager

    • Sync is packaged directly into the current file, synchronously loaded and executed

      Lazy lazy loading separates separate chunk files

      Lazy-once lazy loading separates the chunk files from each other and reads the code in memory directly after the next load.

      Eager will not separate separate chunk files, but return promise. The code will be executed only after the promise is invoked. It can be understood that the code is loaded first, but we can control the delay of executing this part of code.

Webpack parses require.context() in the code at build time.

The syntax is as follows:

require.context(
  directory,
  (useSubdirectories = true),
  (regExp = / ^ \ \ /. * $/),
  (mode = 'sync'));Copy the code

Example:

require.context('./test'.false./\.test\.js$/);
// create a context where the file is from the test directory and request ends with '.test.js'.
require.context('.. / '.true./\.stories\.js$/);
// create a context in which all files are from the parent folder and all of its children. Request ends with '.stories.js'.
Copy the code

api

The function has three attributes: resolve, keys, and ID.

  • Resolve is a function that returns the module ID of the resolved request.

  • let p = require.context("...".true."xxx")
    p.resolve("One path")
    Copy the code
  • Keys is also a function that returns an array of all the requests that could be handled by this Context Module.

The return value of require.context is a function where we can pass in the path of the file and get the modular component

let components = require.context('.. /pages'.true./\.js$/.'sync')

let paths = components.keys()// Get the addresses of all incoming files
// console.log(paths)
let routes = paths.map(path= > {
    let component = components(path).default
    path = path.substr(1).replace(/\/\w+\.js$/."")
    return {
        path,
        component
    }
})
console.log(routes)
Copy the code

conclusion

Although there are many apis and returned values, let’s just take two as examples

  1. The keys method, which retrieves the paths of all modules, returns an array

    let context = require.context(".. /pages".true./\.js$/);
    
    let paths = context.keys()// Get all file paths
    Copy the code
  2. Gets all modules in the path

    let context = require.context(".. /pages".true./\.js$/);
    
    let paths = context.keys()// Get all file paths
    
    let routes = paths.map(path= > {
        // Get imported components in batches
        let component = context(path).default;
        console.log(component)
        })
    Copy the code

So that’s all you need to know. Let’s move on

7. Conversion of flat data into tree structure (convertTree algorithm)

I named this algorithm myself, but first we need to understand why we need to convert the data to tree

Our expected routes data should look like this

// What is the purpose?
// Generate a routing configuration
 const routes = [
     {
         path: "".component:xxx
          children:[
                 {
                     path:"xxx"
                     component:xxx
                 }
            ]
     }
 ]
Copy the code

But the actual data after we use require.context looks like this

As you can see, this data is completely flat, without any nesting, so our first step is to convert this flat data into the tree structure that we expect. Let’s do it step by step

7.1 Flattening data using require.context

The first step is to deal with a structure like the one above, where the code is annotated and not too difficult

//require.context()

// 1. A directory to search,
// 2. A flag indicates whether to search for subdirectories as well,
// 3. A regular expression that matches the file.
let context = require.context(".. /pages".true./\.js$/);

let paths = context.keys()// Get all file paths


let routes = paths.map(path= > {
    // Get imported components in batches
    let component = context(path).default;
    // Component extension properties to facilitate rendering menus
    let meta = component['meta') | | {}//console.log(path)
    // The purpose of this re
    / / because the address is. / Discover/Djradio/index, js does not directly use of this type, so must carry on the processing
    / / 1. After removing the former ". "the result is/Discover/Djradio/index, js
    //2. Index. Js is dead with regex because we expect /Discover/Djradio
    //3. If the path is not a folder, the result is /Discover/abc.js. The suffix cannot be used in the path attribute of the route configuration, so the.js suffix is replaced with the re
    path = path.substr(1).replace(/(\/index\.js|\.js)$/."")
    // console.log(path)
    return {
        path,
        component,
        meta
    }
})
Copy the code

7.2 Implementing convertTree Algorithm

So once we’ve processed the data, we’re going to encapsulate a method that’s going to process the flat data into a tree, and the algorithm is O(n^2) in time.

function convertTree(routes) {
    let treeArr = [];
    //1. Handle the id and parent of each data.
    routes.forEach(route= > {
        let comparePaths = route.path.substr(1).split("/")
        // console.log(comparePaths)
        if (comparePaths.length === 1) {
            Parent_id does not need to be added to the root node
            route.id = comparePaths.join("")}else {
            // Indicates a parent node
            // Handle your own id first
            route.id = comparePaths.join("");
            //comparePaths Except the last item is parent_id
            comparePaths.pop()
            route.parent_id = comparePaths.join("")}})//2. The parent node id is found in the database
    routes.forEach(route= > {
        // Check whether the current route has parent_id
        if (route.parent_id) {
            // Has a parent node
            // the route of parent_id is the parent node of the current route
            let target = routes.find(v= > v.id === route.parent_id);
            // Check whether the parent has the children attribute
            if(! target.children) { target.children = [] } target.children.push(route) }else {
            treeArr.push(route)
        }
    })

    return treeArr
}
Copy the code

After the above processing, you have a tree structure

Then we just need to export the data and pass it to CompileRouter on our app

7.3 Pay attention to later

Now you just need to create a file in Pages to automatically process and compile routes. For nested routes, add CompileRouter to your component

  1. Creating a Routing page
  2. Nested routines are added to the parent routing component

8. Extend static properties

The effect we are currently creating is there, but there is a problem if we use it to render the menu. There is no content to render the menu, so we can extend the static attribute meta(or something else) to the component and make a few minor changes to our automated compilation code

component

Automatic processing logic complete code

//require.context()

// 1. A directory to search,
// 2. A flag indicates whether to search for subdirectories as well,
// 3. A regular expression that matches the file.
let context = require.context(".. /pages".true./\.js$/);

let paths = context.keys()// Get all file paths


let routes = paths.map(path= > {
    // Get imported components in batches
    let component = context(path).default;
    // Component extension properties to facilitate rendering menus
    let meta = component['meta') | | {}//console.log(path)
    // The purpose of this re
    / / because the address is. / Discover/Djradio/index, js does not directly use of this type, so must carry on the processing
    / / 1. After removing the former ". "the result is/Discover/Djradio/index, js
    //2. Index. Js is dead with regex because we expect /Discover/Djradio
    //3. If the path is not a folder, the result is /Discover/abc.js. The suffix cannot be used in the path attribute of the route configuration, so the.js suffix is replaced with the re
    path = path.substr(1).replace(/(\/index\.js|\.js)$/."")
    // console.log(path)
    return {
        path,
        component,
        meta
    }
})
// This data is flat and does not comply with our routing rules
// We need to do an algorithm to reduce the time complexity as much as possible o(n)
// Encapsulate a convertTree algorithm o(n^2)
// console.log(routes)

//id
//parent_id

function convertTree(routes) {
    let treeArr = [];
    //1. Handle the id and parent of each data.
    routes.forEach(route= > {
        let comparePaths = route.path.substr(1).split("/")
        // console.log(comparePaths)
        if (comparePaths.length === 1) {
            Parent_id does not need to be added to the root node
            route.id = comparePaths.join("")}else {
            // Indicates a parent node
            // Handle your own id first
            route.id = comparePaths.join("");
            //comparePaths Except the last item is parent_id
            comparePaths.pop()
            route.parent_id = comparePaths.join("")}})//2. The parent node id is found in the database
    routes.forEach(route= > {
        // Check whether the current route has parent_id
        if (route.parent_id) {
            // Has a parent node
            // the route of parent_id is the parent node of the current route
            let target = routes.find(v= > v.id === route.parent_id);
            // Check whether the parent has the children attribute
            if(! target.children) { target.children = [] } target.children.push(route) }else {
            treeArr.push(route)
        }
    })

    return treeArr
}

export default convertTree(routes)


// Get a module
// console.log(p("./Discover/index.js").default)

// What is the purpose?
// Generate a routing configuration
// const routes = [
/ / {
// path: "",
// component,
// children:[
// {path component}
/ /]
/ /}
// ]
Copy the code

Write in the last

In the next installment, I’ll write about how to handle CompileRouter for authentication and other applications in a project