preface

After three months, I finally finished the first stage of the project with my friends, and I have gained a lot. However, since it was the first time for us to fully use React for development in a formal project, we did not have enough experience in the early stage of project construction, which led to some limitations in project optimization and we had to choose some second-best methods.

Below I according to the whole development process for a comb and “fill in the gaps”, I hope it can also be helpful to you, if there is wrong or inadequate in the article also please big bosses don’t hesitate to advise (this article is mainly about the background project construction process).

In addition, the current iteration of technology in all aspects of the front end is really a bit fast, and some of the writing methods between different versions are different, so I will put a label on the versions of the dependencies I use to distinguish them.

To prepare

1. Initialize the project

NPM is now available in version 5.2 with the NPX command, and node comes with NPM.

npx create-react-app react-demo
Copy the code

or

npm install -g create-react-app
create-react-app react-demo
Copy the code

2. Install required dependencies (optional)

  • react-router-dom

    Provides routing functions.

    / / [email protected]
    npm install react-router-dom
    Copy the code
  • Redux, react-redux, redux-thunk

    Redux: Manage application state (data state).

    React-redux: React-redux is a react-specific library packaged by the authors of Redux.

    Redux-thunk: Makes store.dispatch, which can only accept objects, accept objects/methods, and automatically execute a method if it receives one without triggering a Store update for Redux.

    // [email protected], [email protected], [email protected]
    npm install redux react-redux redux-thunk
    Copy the code
  • redux-devtools-extension

    Store Data management debugging tool.

    / / [email protected]
    npm install redux-devtools-extension
    Copy the code
  • Immer, use – immer

    Immer: Implement JS immutable data structures.

    Use-immer: Provides the useImmer method.

    / / [email protected], [email protected]
    npm install immer use-immer
    Copy the code
  • React-app-rewired, customize-cra, react-app-rewi-multiple-entry

    Is the react – app – rewired: changing the CRA configuration tool that provides without exposing the project configuration, modify the function of the project configuration.

    Customize-cra: Provides helper methods for modifying the Webpack configuration.

    React-app-rewi-multiple-entry: Adds multiple page entries.

    // [email protected], [email protected], [email protected]
    npm install react-app-rewired customize-cra  --save-dev
    Copy the code
  • dotenv-cli

    Load environment variables from the.env file into process.env.

    / / [email protected].
    npm install dotenv-cli --save-dev
    Copy the code
  • Less and less – loader

    Less: CSS preprocessing language.

    Less-loader: webpack compiles less into CSS loader.

    / / [email protected], [email protected]
    npm install less less-loader --save-dev
    Copy the code

Multi-environment Configuration

During the development process, the project will have multiple environments: development, test, UAT, production, etc. We will deploy the project according to each environment. In this case, we need to load the environment variables from the corresponding environment file into process.env through Dotenv to achieve multi-environment configuration (official website).

  1. Create the following files in the project root directory and add the environment variable BASE_URL:

    • .env: used to set some common configurations.
    • .env.development: Development environment configuration, add variables:REACT_APP_BASE_URL=development.api.com
    • .env.test: Test environment configuration, add variables:REACT_APP_BASE_URL=test.api.com
    • .env.production: Production environment configuration, add variables:REACT_APP_BASE_URL=production.api.com
  2. Modify scripts in package.json file:

"scripts": {
  "start": "react-app-rewired start"."build:dev": "dotenv -e .env.development react-app-rewired build"."build:test": "dotenv -e .env.test react-app-rewired build"."build:prod": "dotenv -e .env.production react-app-rewired build"."test": "react-app-rewired test"."eject": "react-scripts eject"
},
Copy the code
  1. Create the config-overrides. Js file in the root directory.

    When we run/package the project through react-app-rewired, the relevant configuration in config-overrides. Js will be read first, and the original webpack configuration rules will be modified before packaging. Here we will create an empty config-overrides. Js file and then configure it according to the needs of the project.

  2. test

    With the above configuration, we can access the environment variables in the configuration through process.env and run/package the code for each environment according to the different compile commands.

    Run NPM start to start the project. Print process.env.react_app_base_url in app.js and you can see the console output development.api.com (you can test it for other environments, so I won’t go into that here. In addition, if you change the contents of the environment variable file or config-overrides. Js, you need to re-execute NPM start to take effect.

Multiple entry configuration

Because our project is divided into many ends (official website, two PC middle stage, one PC background and one mobile terminal background). For the background, in the case of both PC and mobile (mobile is the emasculated version of PC), some help methods, business logic and so on are the same, so we put the two ends under the same project for development.

To achieve this, we need to extend the existing WebPack configuration to support multiple portals.

1. Add files related to mobile devices

Add related files to the project root directory:

  • index-m.js: Mobile terminal entry file.
  • public/mobile.html: mobile terminalhtmlTemplate file.
  • AppMobile.js: Add mobile terminal configuration components inindex-m.jsIs introduced in.

The original index.js, public/index.html, and app. js files are used as PC files.

Modify the config – overrides. Js:

const { override } = require('customize-cra')

const multipleEntry = require('react-app-rewire-multiple-entry')([
  {
    entry: 'src/index-m.js',
    template: 'public/mobile.html',
    outPath: '/mobile.html'
  }
])

const customWebpack = () => config => {
  multipleEntry.addMultiEntry(config)
  return config
}

module.exports = {
  webpack: override(
    customWebpack(),
  )
}
Copy the code

Now that we’ve added the mobile entry file to our existing WebPack configuration, let’s test it out (you can add something to app.js and AppMobile.js, as long as it’s easy to distinguish between PC and mobile).

Re-execute NPM start, visit http://localhost:3002/index.html and http://localhost:3002/mobile.html (where everyone according to their own projects run port access). Everything turned out so well.

2. Configure the proxy

But the question is, what do we do when we want to differentiate between multiple ends, not make it a multi-page app, which is essentially a single page app?

Take your time

Nginx proxy (Nginx) : Nginx proxy (Nginx) : Nginx proxy (Nginx) Nginx basics, zero to one practice), access different ends according to different ports. Action is better than heart, try ~

Sudo Nginx -s reload:

server { listen 8888; server_name localhost; Location / {proxy_pass http://127.0.0.1:3002; } } server { listen 8889; server_name localhost; location ~ ^[^.]*$ { rewrite /(.*) /mobile.html break; Proxy_pass http://127.0.0.1:3002; } location / {proxy_pass http://127.0.0.1:3002; }}Copy the code

Visit http://localhost:8888/ and http://localhost:8889/, it seems ok, but…

3. Modify the websocket

Just when I was secretly happy, reality hit me hard.

When I open the console, I see the following error message (why 404 and 200, I don’t know, if you know) :

The result of this error is that hot updates fail, all changes need to be manually refreshed, and all warnings fail, which is definitely unacceptable.

So first let’s take a looknetworksockjs-nodeRelated information requested:

This is where the connection failed because our project was actually running on port 3002 and when we set up the proxy using Nginx, we accessed ports 8888 and 8889. So, we just need to keep port 3002 for sending sockjS-Node requests to solve this problem.

That is, we need to confirm how we set the URL when we create the WebSocket connection, and then set it to the value we want.

With a cause and a solution, you can breathe a sigh of relief (or at least a direction). Let’s take a look at the exact location of the error (click on the console output error message)webpackHotDevClient.js:60) :

See here is a kind of suddenly enlightened feeling, success is not far away from us! If WDS_SOCKET_HOST and WDS_SOCKET_PORT are set, the corresponding values are set. If this parameter is not set, the current access domain name and port are used. The final concatenation is the URL of the WebSocket connection.

So, we just need to specify WDS_SOCKET_HOST and WDS_SOCKET_PORT as the desired domain name and port in the environment configuration to solve this problem. Add the following to the.env.development file (hot updates are normally only needed in the development environment) :

WDS_SOCKET_HOST = 127.0.0.1 WDS_SOCKET_PORT = 3002Copy the code

Re-run the project NPM start and visit http://localhost:8888/ and http://localhost:8889/ respectively. No error is reported

The UI component library

In the project, Ant Design and Ant Design Mobile were selected as UI component libraries for PC and Mobile respectively.

Installation:

/ / [email protected], [email protected]
npm install antd antd-mobile --save
Copy the code

1. antd

Import the antD style file import ‘antd/dist/antd.less’ in index.js.

Antd supports tree shaking based on ES Modules by default. For js, import {Button} from ‘antd’ will be loaded on demand.

2. antd-mobile

Currently antD-Mobile also needs to manually implement on-demand loading.

Modify config-overrides. Js file (official website)

const { override, fixBabelImports } = require('customize-cra')
// ...
module.exports = {
  webpack: override(
    customWebpack(),
    fixBabelImports('import', {
      libraryName: 'antd-mobile'.style: true}}))Copy the code

Rerunning the project, and then simply importing the module from ANTD-Mobile without importing the styles separately.

3. Added less support and custom themes

New Less support

After completing the above two steps, testing the reference component at this point shows that the component style does not work. This is because crA-created projects do not support compiling less files by default, so we need to extend the configuration of webpack via config-overrides. The Antd website is also updated recently. The previous example of Antd website is addLessLoader. Instead, craco-less is introduced to help load less styles and modify variables.

Complete config-overrides. Js:

const { override, addLessLoader, fixBabelImports } = require('customize-cra')

const multipleEntry = require('react-app-rewire-multiple-entry')([
  {
    entry: 'src/index-m.js'.template: 'public/mobile.html'.outPath: '/mobile.html'}])const customWebpack = () = > config= > {
  multipleEntry.addMultiEntry(config)
  return config
}

module.exports = {
  webpack: override(
    customWebpack(),
    addLessLoader({
      lessOptions: {
        javascriptEnabled: true.modifyVars: {
        }
      }
    }),
    fixBabelImports('import', {
      libraryName: 'antd-mobile'.style: true}}))Copy the code

Rerun the project and the corresponding component style will take effect.

Custom theme

In the configuration above, we can change the theme styles of ANTD and ANTD-Mobile using modifyVars, for example: theme colors, text colors, rounded corners, etc.

Sample:

// ...
addLessLoader({
  lessOptions: {
    javascriptEnabled: true.modifyVars: {
      '@primary-color': '#4085F5'.'@text-color': 'rgba (0, 0, 0, 0.65)'.'@brand-primary': '#4085F5'.'@fill-body': '#F7F8FA'.'@color-text-base': '# 333333',}}})/ /...
Copy the code

CSS Module

CSS modules and regular CSS are supported using the [name].module. CSS file naming convention. The CSS Module allows you to determine the scope of the CSS by automatically creating a unique classname in the format [filename]\_[className]\_\_[hash].

We used Less as a preprocessor in the project, and making it support CSS Modules required modification of the WebPack configuration. Thankfully, we used the react-app-rewired method to modify the CRA configuration, as well as the addLessLoader method to extend it. By default, addLessLoader has already modified the configuration of the related less-loader in Webpack, so we can use [name].module.less directly.

We can take a quick look at the source code of addLessLoader:

  1. The default processed style name in the format[local]--[hash:base64:5];
  2. Distinguished by two re’s.less.module.lessTwo types.

So let’s get straight to the test.

New style/base. The module. Less:

.baseContainer {
  padding: 50px;
  .title {
    color: pink;
  }
  .fontSize {
    font-size: 20px; }}Copy the code

Modify the App. Js:

import React from 'react'
import style from './style/base.module.less'

function App() {
  return (
    <div className={style.baseContainer}>
      <div className={` ${style.title} ${style.fontSize} `} >App</div>
    </div>)}export default App
Copy the code

View the run result:

For more details on how to use the portal, see the website.

routing

1. Configure static routes

Those of you who have used Vue know that in Vue, VUe-Router provides a lot of convenience for us, such as static route configuration and navigation guard. In React, we had to do all of this manually.

React-router-config configures static routes on Github.

Install the React -router-config helper for configuring static routes for the React router.

/ / [email protected]
npm install react-router-config --save
Copy the code

It provides two methods: matchRoutes and renderRoutes. Let’s focus on renderRoutes.

Look at the source (node_modules/reactrouter – config/modules/renderRoutes. Js) :

import React from "react";
import { Switch, Route } from "react-router";

function renderRoutes(routes, extraProps = {}, switchProps = {}) {
  return routes ? (
    <Switch {. switchProps} >
      {routes.map((route, i) => (
        <Route
          key={route.key || i}
          path={route.path}
          exact={route.exact}
          strict={route.strict}
          render={props= >route.render ? ( route.render({ ... props, ... extraProps, route: route }) ) : (<route.component {. props} {. extraProps} route={route} />)}} / >))</Switch>
  ) : null;
}

export default renderRoutes;
Copy the code

Source content is very simple, is to map the routes. Some of you may wonder: why are we loading routes this way? My understanding is to make our programs simpler and more controllable.

More concise: it can simulate the writing of routes in Vue to achieve static route configuration and clear route structure; Also, when there are multiple layouts in the project, we can write routes and register routes more clearly.

React allows you to handle memory leaks. For example, when a page is being redirected, the interface request is not finished, and a page-related operation is performed in a callback. That’s when memory leaks start to happen. My idea for this is to cancel pending requests when the page jumps (depending on the business). The “cancel” action can be done in renderRoutes (which I consider a navigational guard).

Create route/ renderroutes.js and copy the source code from the dependencies so we can extend as needed.

New route/index. Js:

const routes = [
  { path: '/login'.exact: true.component: Login},
  { path: '/ 404'.component: NotFound},
  { 
    path: '/goods'.component: GoodsLayout,
    routes: [{path: '/goods/list'.component: GoodsList}
    ]
  },
  {
    component: BasicLayout,
    routes: [{path: '/'.exact: true.component: Home},
      { path: '/home2'.exact: true.component: Home2 },
      { path: '/home3'.exact: true.component: Home3 }
    ]
  }
]
Copy the code

At this point, the static route configuration is complete. For details, see the react-router-config on Github.

2. The extension

With renderroutes.js, we have implemented and managed most of the routing functions, such as route authentication and cancellation requests.

For a quick look at the implementation of route authentication, modify renderroutes.js:

import React from 'react'
import { Switch, Route } from 'react-router'

function renderRoutes(routes, authed, extraProps = {}, switchProps = {}) {
  return routes ? (
    <Switch {. switchProps} >
      {routes.map((route, i) => (
        <Route
          key={route.key || i}
          path={route.path}
          exact={route.exact}
          strict={route.strict}
          render={props= >{ if (route.auth && ! Route.auth.includes (authed)) {return route.render? ( route.render({ ... props, ... extraProps, route: route }) ) : (<route.component {. props} {. extraProps} route={route} />)}} />))}</Switch>
  ) : null
}

export default renderRoutes
Copy the code

Here is by passing in authed (the current user’s permission), in the render method to judge: if the user has permission to enter the target page permission, then normal jump, otherwise intercept and make the corresponding processing.

As for how to judge the route authentication in real situations, the business logic needs to be designed.

Axios cancels the request

React may leak memory, which is caused by status updates after uninstallation. An asynchronous request callback is typically followed by a setData related operation.

Currently, axios is the one we use most in the project, and AXIos provides us with two cancellation methods. Go straight to the document:

From the documentation, we can see that the first method cancels multiple requests, while the second method cancels only one. So I’m going to choose the first option directly.

1. Simply encapsulate Axios

Installation:

/ / [email protected], [email protected]
npm install axios express --save
Copy the code

newserver/index.js:

Prepare two interfaces and set 2s delay for easy testing.

const express = require('express')
const app = express()

app.all(The '*'.function (req, res, next) {
  res.header('Access-Control-Allow-Origin'.The '*');
  res.header('Access-Control-Allow-Headers'.'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
  res.header('Access-Control-Allow-Methods'.'PUT, POST, GET, DELETE, OPTIONS');

  if (req.method == 'OPTIONS') {
    res.send(200)}else next()
})

app.get('/api/test'.function (req, res) {
  setTimeout(() = > {
    res.send('Test results')},2000)
})
app.post('/api/test2'.function (req, res) {
  setTimeout(() = > {
    res.send('Test results')},2000)})let server = app.listen(9999.function () {

  let host = server.address().address
  let port = server.address().port

  console.log('Access address is:', host, port)
})
Copy the code

newutils/request:

import axios from 'axios'

const service = axios.create({
  baseURL: 'http://127.0.0.1:9999'.timeout: 20000
})

service.interceptors.request.use(
  (config) = > {
    return config
  },
  (error) = > {
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  (response) = > {
    return response
  },
  (error) = > {
    return Promise.reject(error)
  }
)

export default service
Copy the code

newmodel/index.js:

import fetch from '.. /utils/request.js'

class Model {

  api(options = {}) {
    if(! options.method) options.method ='get'

    return new Promise((resolve, reject) = > {
      let request
      let config = {
        method: options.method,
        url: options.url,
      }

      switch (options.method) {
      case 'get': request = fetch({ ... config,params: options.params
        })
        break
      case 'post': request = fetch({ ... config,data: options.data
        })
        break
      default:
      }
      request
        .then(response= > {
          resolve(response)
        }).catch(error= > {
          reject(error)
        })
    })
  }

  get (options = {}) {
    options.method = 'get'
    return this.api(options)
  }

  post (options = {}) {
    options.method = 'post'
    return this.api(options)
  }
}

export default Model
Copy the code

newmodel/common.js:

import Model from './index'

class Common extends Model {
  getTest(options = {}) {
    options.url = '/api/test'
    return this.get(options)
  }
  getTest2(options = {}) {
    options.url = '/api/test2'
    return this.post(options)
  }
}

const commonModel = new Common()
export default commonModel
Copy the code

Use:

import React, { useState } from 'react'
import { Button } from 'antd'
import commonModel from '@/model/common'

function Index2() {

  const [test, setTest] = useState(0)

  const sendRequest = () = > {
    commonModel.getTest().then((data) = > {
      setTimeout(() = > {
        setTest(1)},1000)
    })
    commonModel.getTest2().then((data) = > {
      setTest(123)})}return (
    <div>
      <div>An overview of 2</div>
      <div>{test}</div>
      <Button onClick={sendRequest}>send request</Button>
    </div>)}export default Index2
Copy the code

Click the button to send the request, and the page shows that test value is 123 first and then 1, then AXIOS configuration is successful.

Simulate a memory leak: open the console, click the button to send a request, and within two seconds jump to another page. We can see the following error:

This is because when we jump to a page, the request from the previous page does not end and a status update is triggered, which then causes a memory leak. So let’s solve this problem.

2. axios.CancelToken

Modify theutils/request.js:

// ...
const getCancelToken = () = > {
  let CancelToken = axios.CancelToken
  let source = CancelToken.source()
  return source
}
// ...
service.interceptors.request.use(
  (config) = > {
    config.cancelToken = config.cancel_token
    return config
  },
  (error) = > {
    return Promise.reject(error)
  }
)
// ...
export {
  getCancelToken
}
Copy the code

Modify themodel/index.js:

// ...
return new Promise((resolve, reject) = > {
      let request
      let config = {
        method: options.method,
        url: options.url,
      }
      if (options.cancel_token) config.cancel_token = options.cancel_token

      switch (options.method) {
/ /...
Copy the code

Modify page:

import React, { useEffect, useRef, useState } from 'react'
import { Button, Space } from 'antd'
import commonModel from '@/model/common'
import { getCancelToken } from '@/utils/request'

function Index2() {
  const source = useRef(getCancelToken())

  const [test, setTest] = useState(0)

  useEffect(() = > {
    return () = > {
      source.current.cancel('Cancel request')
      source.current = null}}, [])const sendRequest = () = > {
    commonModel.getTest({
      cancel_token: source.current.token
    }).then((data) = > {
      setTimeout(() = > {
        setTest(1)},1000)
    })
    commonModel.getTest2({
      cancel_token: source.current.token
    }).then((data) = > {
      setTest(123)})}return (
    <div>
      <div>An overview of 2</div>
      <div>{test}</div>
      <Space>
        <Button onClick={sendRequest}>send request</Button>
      </Space>
    </div>)}export default Index2
Copy the code

Repeating the above procedure, we see that the console prints “cancel request” twice, and the memory leak problem does not occur again.

A brief overview of the ideas:

  • Pass to the method that needs to cancel the requestcancel_token;
  • throughmodel/index.jsTo pass toutils/request.js;
  • utils/request.jsreceivecancel_tokenAnd add toaxiosRequest interceptor configuration for.
  • Call when the page is unloadedcancelMethod to cancel the request.

It would be too much trouble to do this for every page. Is there a common place to handle cancellation requests?

RenderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes = renderRoutes

3. Unified processing

The idea is to use an identifier to distinguish between apis that need to cancel requests during route switching. Add cancel_token to these tagged apis; Finally, these API requests are cancelled during route switching.

Create a new utils/global.js to store the cancel_token and cancel methods.

let global = {
  source: {
    token: null.cancel: null
  },
  timestamp: null
}

const changeGlobal = (key, value) = > {
  global[key] = value
}

export {
  global,
  changeGlobal
}
Copy the code

Modify modal/common.js to indicate which interfaces need to be “cancelled” through the variable needCancel.

import Model from './index'

class Common extends Model {
  getTest(options = {}) {
    options.url = '/api/test'
    options.needCancel = true
    return this.get(options)
  }
  getTest2(options = {}) {
    options.url = '/api/test2'
    options.needCancel = true
    return this.post(options)
  }
}

const commonModel = new Common()
export default commonModel
Copy the code

Modify modal/index.js to add cancel_token to the interface of the token.

import fetch from '.. /utils/request.js'
import { global } from '.. /utils/global'

class Model {
// ...
      if (options.needCancel && global.source) config.cancel_token = global.source.token

      switch (options.method) {
// ...
Copy the code

Modify theroute/renderRoutes.js.

import React from 'react'
import { Switch, Route, Redirect } from 'react-router'
import { getCancelToken } from '@/utils/request'
import { global, changeGlobal } from '.. /utils/global'

function renderRoutes(routes, authed, extraProps = {}, switchProps = {}) {

  returnroutes ? ( <Switch {... switchProps}> {routes.map((route, i) => ( <Route key={route.key || i} path={route.path} exact={route.exact} strict={route.strict} render={props => { if (route.auth && ! Cancel_token if (global.source.token && global.source.cancel) {route.auth.includes(authed)) {// Cancel_token if (global.source.token && global.source.cancel) Cancel (' cancel request ') changeGlobal('source', getCancelToken()) changeGlobal('timestamp', new Date().getTime()) // ...Copy the code

Repeat the above procedure and you’re done

conclusion

One thing after another, one project after another, too busy to paddle ┭┮﹏┭┮… Fortunately, although I was constantly writing business, I still gained something in the project. Take a break from busy work, the article is a little rough, knowledge points are more fragmentary, I will try to perfect the time later. Do you have any suggestions in the comments section

To be continued…