Build full record based on react enterprise spa project

Time:2020-2-15

Preface

This article is about how to build enterprise level react projects. All the technologies used are the latest and most mainstream. I will write a “complete record of building enterprise level SSR projects based on react” later. Please look forward to it!

Technology selection

Package Name Version
antd ^3.16.6
axios ^0.18.0
connected-react-router ^6.4.0
classnames ^2.2.6
immutable ^4.0.0-rc.12
@loadable/component ^5.10.0
react ^16.8.6
react-redux ^7.0.3
react-router-config ^5.0.0
react-router-dom ^5.0.0
react-scripts 3.0.1
redux ^4.0.1
redux-actions ^2.6.5
redux-logger ^3.0.6
redux-persist ^5.10.0
redux-persist-expire ^1.0.2
redux-persist-transform-immutable ^5.0.0
redux-saga ^1.0.2
history ^4.7.2

Create a new project using create react app

create-react-app react-project

The directory structure is as follows

    |-- .gitignore
    |-- README.md
    |-- package.json
    |-- yarn.lock
    |-- public
    |   |-- favicon.ico
    |   |-- index.html
    |   |-- logo192.png
    |   |-- logo512.png
    |   |-- manifest.json
    |   |-- robots.txt
    |-- src
        |-- App.css
        |-- App.js
        |-- App.test.js
        |-- index.css
        |-- index.js
        |-- logo.svg
        |-- serviceWorker.js

Then we expose the webback and execute the following command:

yarn eject

The directory structure is as follows:

    |-- .gitignore
    |-- README.md
    |-- package.json
    |-- yarn.lock
    |-- config
    |   |-- env.js
    |   |-- modules.js
    |   |-- paths.js
    |   |-- pnpTs.js
    |   |-- webpack.config.js
    |   |-- webpackDevServer.config.js
    |   |-- jest
    |       |-- cssTransform.js
    |       |-- fileTransform.js
    |-- public
    |   |-- favicon.ico
    |   |-- index.html
    |   |-- logo192.png
    |   |-- logo512.png
    |   |-- manifest.json
    |   |-- robots.txt
    |-- scripts
    |   |-- build.js
    |   |-- start.js
    |   |-- test.js
    |-- src
        |-- App.css
        |-- App.js
        |-- App.test.js
        |-- index.css
        |-- index.js
        |-- logo.svg
        |-- serviceWorker.js

Add dependency package

"dependencies": {
    "@babel/plugin-proposal-decorators": "^7.4.0",
    "@babel/plugin-syntax-dynamic-import": "^7.2.0",
    "@loadable/component": "^5.10.0",
    "antd": "^3.16.6",
    "axios": "^0.18.0",
    "babel-plugin-import": "^1.11.0",
    "babel-plugin-transform-decorators-legacy": "^1.3.5",
    "classnames": "^2.2.6",
    "connected-react-router": "^6.4.0",
    "history": "^4.7.2",
    "immutable": "^4.0.0-rc.12",
    "node-sass": "^4.11.0",
    "prettier": "^1.16.4",
    "react-redux": "^7.0.3",
    "react-router-config": "^5.0.0",
    "react-router-dom": "^5.0.0",
    "redux": "^4.0.1",
    "redux-actions": "^2.6.5",
    "redux-logger": "^3.0.6",
    "redux-persist": "^5.10.0",
    "redux-persist-expire": "^1.0.2",
    "redux-persist-transform-compress": "^4.2.0",
    "redux-persist-transform-encrypt": "^2.0.1",
    "redux-persist-transform-immutable": "^5.0.0",
    "redux-saga": "^1.0.2"
  },

Add dependency package, let’s run it to see if there is any problemyarn start
Build full record based on react enterprise spa project

Add framework foundation profile

1. Add unified formatting standard for. Editorconfig file

root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.yml]
indent_style = space
indent_size = 2

2. Add. Eslintrc file code inspection standard

{
    "extends": ["react-app", "plugin:prettier/recommended"]
}

3. Remove the Babel setting in package.json, and add the support of. Babelrc file for Babel

{
    "presets": [
      "react-app"
    ],
    "plugins": [
      [
        "import",
        {
          "libraryName": "antd",
          "libraryDirectory": "es",
          "style": "css"
        },
        "antd"
      ]
    ]
  }
  

The directory structure is as follows:

    |-- .babelrc
    |-- .editorconfig
    |-- .gitignore
    |-- README.md
    |-- package.json
    |-- yarn.lock
    |-- config
    |   |-- env.js
    |   |-- modules.js
    |   |-- paths.js
    |   |-- pnpTs.js
    |   |-- webpack.config.js
    |   |-- webpackDevServer.config.js
    |   |-- jest
    |       |-- cssTransform.js
    |       |-- fileTransform.js
    |-- public
    |   |-- favicon.ico
    |   |-- index.html
    |   |-- logo192.png
    |   |-- logo512.png
    |   |-- manifest.json
    |   |-- robots.txt
    |-- scripts
    |   |-- build.js
    |   |-- start.js
    |   |-- test.js
    |-- src
        |-- App.css
        |-- App.js
        |-- App.test.js
        |-- index.css
        |-- index.js
        |-- logo.svg
        |-- serviceWorker.js

Run the code to see if there is a problem.

Delete all files under SRC

The directory structure is as follows:

    |-- .babelrc
    |-- .editorconfig
    |-- .gitignore
    |-- README.md
    |-- package.json
    |-- yarn.lock
    |-- config
    |   |-- env.js
    |   |-- modules.js
    |   |-- paths.js
    |   |-- pnpTs.js
    |   |-- webpack.config.js
    |   |-- webpackDevServer.config.js
    |   |-- jest
    |       |-- cssTransform.js
    |       |-- fileTransform.js
    |-- public
    |   |-- favicon.ico
    |   |-- index.html
    |   |-- logo192.png
    |   |-- logo512.png
    |   |-- manifest.json
    |   |-- robots.txt
    |-- scripts
    |   |-- build.js
    |   |-- start.js
    |   |-- test.js
    |-- src

SRC directory

1. Add index.js

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { ConfigProvider, message } from "antd";
import zhCN from "antd/es/locale/zh_CN";
import moment from "moment";
import "moment/locale/zh-cn";
import * as serviceWorker from "./serviceWorker";
import "./assets/css/index.css";
import "./assets/css/base.scss";
import "./assets/css/override-antd.scss";

moment.locale("zh-cn");
message.config({
  duration: 2,
  maxCount: 1
});
//Remove console.log from all pages
if (process.env.NODE_ENV === "production") {
  console.log = function() {};
}
ReactDOM.render(
  //Add support of antd for Chinese
  <ConfigProvider locale={zhCN}>
    <App />
  </ConfigProvider>,
  document.getElementById("root")
);

2. Add app.js file
App.js contains the configuration of redux. The code is as follows:

import React, { Component } from "react";
import { Spin } from "antd";
import { Provider } from "react-redux";
import { renderRoutes } from "react-router-config";
import { ConnectedRouter } from "connected-react-router/immutable";
import { PersistGate } from "redux-persist/es/integration/react";
import configureStore, { history } from "./redux/store";
import AppRoute from "./layout/AppRoute";

const { persistor, store } = configureStore();
store.subscribe(() => {
  // console.log("subscript", store.getState());
});

class App extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    const customContext = React.createContext(null);
    return (
      <Provider store={store}>
        <PersistGate loading={<Spin />} persistor={persistor}>
          <ConnectedRouter history={history}>
            <AppRoute />
          </ConnectedRouter>
        </PersistGate>
      </Provider>
    );
  }
}

export default App;

3. Add assets folder
The directory structure is as follows:

|-- assets
            |-- audio
            |-- css
            |-- fonts
            |-- image
            |-- video

4. Add components folder
At the same time, create the common folder. The directory structure is as follows:

|-- components
            |-- common

5. Add config folder
And add the base.config.js file, which contains some basic framework information and background URL and port data. The code is as follows:

export default {
  company: "Awbeci",
  Title: "background management system platform",
  Subtitle: "background management system platform",
  copyright: "Copyright © 2019 Awbeci All Rights Reserved.",
  logo: require("../assets/image/hiy_logo.png"),
  host: "http://10.0.91.189",
  port: "19101",
  persist: "root"
};

The directory structure is as follows:

|-- config
            |-- base.conf.js

6. Add hoc advanced components folder (optional)
At the same time, the control.js file is created to calculate the width and height automatically according to the screen resolution. The code is as follows:

import React from "react";
import { is, Map, fromJS } from "immutable";

const control = WrappedComponent =>
  class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        //Height and width of viewing area
        document: {
          body: {
            width: 0,
            height: 0
          },
          //Height and width of sidebar
          sidebar: {
            width: 0,
            height: 0
          },
          //Content area height and width
          content: {
            width: 0,
            height: 0
          },
          header: Map({
            height: 64,
            width: 0,
            menu: Map({
              height: 0,
              width: 0
            })
          })
        }
      };
    }
    componentWillMount() {
      let cw = document.body.clientWidth;
      let ch = document.body.clientHeight;
      this.computedLayout(cw, ch);
    }
    componentDidMount() {
      window.addEventListener("resize", this.computedLayout);
    }
    componentWillUnmount() {
      window.removeEventListener("resize", this.computedLayout);
    }
    computedLayout = () => {
      let width = document.body.clientWidth;
      let height = document.body.clientHeight;

      this.setState((state, props) => ({
        //todo:
      }));
    };

    shouldComponentUpdate(nextProps, nextState) {
      const thisProps = this.props || {};
      const thisState = this.state || {};
      nextState = nextState || {};
      nextProps = nextProps || {};

      if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) {
        return true;
      }

      for (const key in nextProps) {
        if (!is(thisProps[key], nextProps[key])) {
          return true;
        }
      }

      for (const key in nextState) {
        if (!is(thisState[key], nextState[key])) {
          return true;
        }
      }
      return false;
    }
    render() {
      return <WrappedComponent {...this.state} {...this.props} />;
    }
  };

export default control;

The directory structure is as follows:

|-- HOC
        |-- control.js

7. Add app folder
The app folder contains framework layout, page layout components, routing and other configuration files. The directory structure is as follows:

|-- app
       |-- AppRoute.js
       |-- Loading.js
       |-- RouterView.js
       |-- layout
       |   |-- index.js
       |   |-- index.scss
       |-- master
           |-- index.js
           |-- index.scss

8. Add pages folder
The pages folder contains the log in and home page files. The directory structure is as follows:

|-- pages
        |-- Index.js
        |-- NoFound.js
        |-- NoPermission.js
        |-- login
            |-- Login.js
            |-- login.scss

9. Add Redux folder
The Redux folder contains the configuration of Redux actions, Redux saga and middleware. The directory structure is as follows:

|-- redux
        |-- reducers.js
        |-- sagas.js
        |-- store.js
        |-- auth
        |   |-- authAction.js
        |   |-- authReducer.js
        |   |-- authSaga.js
        |-- layout
        |   |-- layoutPageAction.js
        |   |-- layoutPageReducer.js
        |-- middleware
            |-- authTokenMiddleware.js

10. Add router folder
At the same time, add the index.js file. The directory structure is as follows:

|-- router
        |-- index.js

11. Add service folder
The service folder encapsulates the requests for the background API interface. The directory structure is as follows:

|-- service
        |-- apis
        |   |-- 1.0
        |       |-- index.js
        |       |-- urls.js
        |-- request
            |-- ApiRequest.js

Build full record based on react enterprise spa project

Detailed configuration

The above code and directory structure have been given. Next, we will explain how to configure Redux, Redux saga, react acitons, immutable, etc

1. Configure action
To configure action, we chooseredux-actionsPlug in as follows

import { createActions } from "redux-actions";

export const authTypes = {
  AUTH_REQUEST: "AUTH_REQUEST",
  AUTH_SUCCESS: "AUTH_SUCCESS",
  AUTH_FAILURE: "AUTH_FAILURE",
  SIGN_OUT: "SIGN_OUT",
  CHANGE_PASSWORD: "CHANGE_PASSWORD"
};

export default createActions({
  [authTypes.AUTH_REQUEST]: ({ username, password }) => ({ username, password }),
  [authTypes.AUTH_SUCCESS]: data => ({ data }),
  [authTypes.AUTH_FAILURE]: () => ({}),
  [authTypes.SIGN_OUT]: () => ({}),
  [authTypes.CHANGE_PASSWORD]: (oldPassword, newPassword) => ({ oldPassword, newPassword })
});

2. Configure reducer
Similar to actions, Redux actions are also used. The code is as follows:

import { handleActions } from "redux-actions";
import { authTypes } from "./authAction";
import { Map, fromJS, merge } from "immutable";

const initState = fromJS({
  user: null,
  token: ""
});

const authReducer = handleActions(
  {
    [authTypes.AUTH_SUCCESS]: (state, action) => {
      return state.merge({
        user: action.data.user,
        token: action.data.token
      });
    },
    [authTypes.SIGN_OUT]: (state, action) => {
      return state.merge({
        user: null,
        token: ""
      });
    }
  },
  initState
);

export default authReducer;

After adding, don’t forget to register for reducer

import { combineReducers } from "redux";
import { connectRouter, LOCATION_CHANGE } from "connected-react-router/immutable";
import layoutReducer from "./layout/layoutReducer";
import authReducer from "./auth/authReducer";

export default history =>
  combineReducers({
    router: connectRouter(history),
    layoutReducer,
    authReducer
  });

3. Configure Redux Saga

The code is as follows:

import { call, put, takeLatest, select } from "redux-saga/effects";
import { push } from "connected-react-router";
import authAction, { authTypes } from "./authAction";
import { layoutPageTypes } from "../layout/layoutAction";
import { message } from "antd";
import Apis from "../../service/apis/1.0";
import config from "../../config/base.conf";

function strokeItem(name, value) {
  localStorage.setItem(name, value);
}

function clearItem(name) {
  localStorage.removeItem(name);
}

function* test() {
  yield put({
    type: authTypes.AUTH_SUCCESS,
    data: {
      user: {
        name: "Awbeci"
      },
      token: "awbeci token"
    }
  });
  yield put({
    type: layoutPageTypes.GET_MENUS,
    menus: [
      {
        icon: "file",
        id: 1,
        isShow: "1",
        Title: "page 1",
        url: "/"
      },
      {
        icon: "file",
        id: 2,
        isShow: "1",
        Title: "page 2",
        url: "/departmentManage"
      },
      {
        icon: "file",
        id: 3,
        isShow: "1",
        Title: "page 3",
        url: "/userManage"
      }
    ]
  });
  yield put({
    type: layoutPageTypes.SAVE_MENU_INDEX,
    payload: {
      keyPath: ["1"]
    }
  });
  yield put(push("/"));
}

function* signout(action) {
  yield call(clearItem, "token");
  yield call(clearItem, `persist:${config.persist}`);

  //Clear token
  //Set the first menu selected
  yield put({
    type: layoutPageTypes.SAVE_MENU_INDEX,
    payload: {
      keyPath: ["126"]
    }
  });
  yield put({
    type: layoutPageTypes.SAVE_MENU_COLLAPSED,
    payload: {
      collapsed: false
    }
  });
  yield put({
    type: layoutPageTypes.GET_MENUS,
    menus: []
  });
  //Jump to login page
  yield put(push("/login"));
}

function* signin(action) {
  try {
    yield call(test);
  } catch (error) {
    Message.info ("wrong user name or password");
    yield call(clearItem, "token");
  } finally {
  }
}

export default function* watchAuthRoot() {
  yield takeLatest(authTypes.AUTH_REQUEST, signin);
  yield takeLatest(authTypes.SIGN_OUT, signout);
}

To add saga files, don’t forget to register as follows:

import { all, fork } from "redux-saga/effects";
import authSaga from "./auth/authSaga";

/*Add listening to action*/
export default function* rootSaga() {
  yield all([fork(authSaga)]);
}

4. Configure store
When configuring the store, Redux logger, Redux persist and immutable.js have been configured together. The code is as follows:

import { createStore, compose, applyMiddleware } from "redux";
import { routerMiddleware } from "connected-react-router/immutable";

import { createMigrate, persistStore, persistReducer } from "redux-persist";
import createEncryptor from "redux-persist-transform-encrypt";
import immutableTransform from "redux-persist-transform-immutable";
import storage from "redux-persist/es/storage";

import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import { createBrowserHistory } from "history";
import createRootReducer from "./reducers";
import rootSaga from "./sagas";
import config from "../config/base.conf";
import { authTokenMiddleware } from "./middleware/authTokenMiddleware";

export const history = createBrowserHistory();
// create the router history middleware
const historyRouterMiddleware = routerMiddleware(history);
// create the saga middleware
const sagaMiddleware = createSagaMiddleware();

//Combining Middleware
const middleWares = [sagaMiddleware, historyRouterMiddleware, logger];
//Encrypt localstorage
const encryptor = createEncryptor({
  secretKey: "hiynn",
  onError: function(error) {}
});

const persistConfig = {
  transforms: [
    immutableTransform()
  ],
  key: config.persist,
  storage,
  version: 2
};

const finalReducer = persistReducer(persistConfig, createRootReducer(history));
export default function configureStore(preloadedState) {
  const store = createStore(finalReducer, preloadedState, compose(applyMiddleware(...middleWares)));
  let persistor = persistStore(store);
  sagaMiddleware.run(rootSaga);
  return { persistor, store };
}

5. Use store
Add a reference to the store in the app.js file. The code is as follows:

import React, { Component } from "react";
import { Spin } from "antd";
import { Provider } from "react-redux";
import { renderRoutes } from "react-router-config";
import { ConnectedRouter } from "connected-react-router/immutable";
import { PersistGate } from "redux-persist/es/integration/react";
import configureStore, { history } from "./redux/store";
import AppRoute from "./app/AppRoute";

const { persistor, store } = configureStore();
store.subscribe(() => {
  // console.log("subscript", store.getState());
});

class App extends Component {
  constructor(props) {
    super(props);
  }
  render() {
    const customContext = React.createContext(null);
    return (
      <Provider store={store}>
        <PersistGate loading={<Spin />} persistor={persistor}>
          <ConnectedRouter history={history}>
            <AppRoute />
          </ConnectedRouter>
        </PersistGate>
      </Provider>
    );
  }
}

export default App;

AppRoute.js

import React, { Component } from "react";
import { connect } from "react-redux";
import { Switch, Redirect } from "react-router";
import { BrowserRouter as Router, HashRouter, Route } from "react-router-dom";
import Login from "../pages/login/Login";
import LayoutContainer from "./layout";
import NoFound from "../pages/NoFound";

@connect(store => ({
  store
}))
class AppRoute extends Component {
  //User authentication
  Authentication() {
    return this.props.store.authReducer.get("token") ? <Redirect to="/" /> : <Login />;
  }
  render() {
    return (
      <>
        {/ * solve the problem that GitHub GH pages must be published in hash or history mode will report an error,
      If you want to use history mode to remove the following hashrouter * /}
        {/* <HashRouter> */}
          <Switch>
            <Route path="/login" render={() => this.Authentication()} />
            <Route path="/" exact component={LayoutContainer} />
            <Route component={NoFound} />
          </Switch>
        {/* </HashRouter> */}
      </>
    );
  }
}

export default AppRoute;

6. Configure Middleware
The middleware function is to reset the token to apirequest when refreshing the page so that the token will not be lost.

import { REHYDRATE } from "redux-persist/lib/constants";
import ApiRequest from "../../service/request/ApiRequest";
import { authTypes } from "../auth/authAction";
import { fromJS } from "immutable";

/**Save token Middleware*/
export const authTokenMiddleware = store => next => action => {
  /**Action = rehydrate is triggered when refresh page persist*/
  if (action.type === REHYDRATE) {
    if (typeof action.payload !== "undefined") {
      let authReducer = action.payload.authReducer;
      if (authReducer) {
        const token = authReducer.get("token");
        ApiRequest.setToken(token ? token : null);
      }
    }
  }
  /**Action = auth? Success will be triggered when login is successful*/
  if (action.type === authTypes.AUTH_SUCCESS) {
    ApiRequest.setToken(action.data.token);
  }
  return next(action);
};

7. Configure static routing

import React from "react";
import loadable from "@loadable/component";
import RouterView from "../app/RouterView";
import NoFound from "../pages/NoFound";
import NoPermission from "../pages/NoPermission";
import Loading from "../app/Loading";
const Index = loadable(() => import("../pages/Index"), { fallback: <Loading /> });
//Note that the distinction between front-end routing and front-end menus is two different things
//Note: both menu and route are generated based on the route data
//The menu may not all be displayed on the page (hidden), but all routes must be defined
//Permission control can be added later
const routes = [
  {
    key: "1",
    Name: "homepage",
    path: "/",
    exact: true,
    component: Index
  }
];

export default routes;

The static route needs to be used with react router config. The code is as follows:

import { renderRoutes } from "react-router-config";
import routes from "../../router";

//Routes here is the route file above
renderRoutes(routes)

8. Package Axios
Including get, post, delete, put, upload, etc

import axios from "axios";
import { message } from "antd";
import config from "../../config/base.conf";

/**
 *HTTP service class
 * get
 * post
 * upload
 * put
 * patch
 * delete
 */
class ApiRequest {
  constructor() {
    //Create Axios instance
    this.instance = axios.create({
      baseURL: `${config.host}:${config.port}`
    });
  }

  /**
   *Set the token by listening to action = rehydrate| auth 〝 success through authtokenmiddleware middleware
   */
  setToken = token => {
    this.instance.defaults.headers.common["Authorization"] = token;
  };

  authentication = str => {
    let errJson = JSON.parse(str);
    if (errJson.response && errJson.response.status === 401) {
      Message. Error ("user authentication error, skipping login page! "";
      setTimeout(() => {
        localStorage.removeItem(`persist:${config.persist}`);
        window.location.href = "/login";
      }, 1500);
    }
  };

  upload(url, formData) {
    return new Promise((resolve, reject) => {
      this.instance
        .post(url, formData, {
          headers: {
            "Content-Type": "multipart/form-data"
          }
        })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          this.authentication(errStr);
          reject(errStr);
        });
    });
  }

  get(url, params = {}) {
    return new Promise((resolve, reject) => {
      this.instance
        .get(url, { params: { ...params } })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          this.authentication(errStr);
          reject(errStr);
        });
    });
  }

  delete(url, params = {}) {
    return new Promise((resolve, reject) => {
      this.instance
        .delete(url, { params: { ...params } })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          this.authentication(errStr);
          reject(errStr);
        });
    });
  }

  post(url, params = {}) {
    return new Promise((resolve, reject) => {
      this.instance
        .post(url, { ...params })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          if (url.includes("login")) {
            reject(errStr);
          } else {
            this.authentication(errStr);
          }
        });
    });
  }

  put(url, params = {}) {
    return new Promise((resolve, reject) => {
      this.instance
        .put(url, { ...params })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          this.authentication(errStr);
          reject(errStr);
        });
    });
  }

  patch(url, params = {}) {
    return new Promise((resolve, reject) => {
      this.instance
        .patch(url, { ...params })
        .then(({ data }) => {
          resolve(data);
        })
        .catch(error => {
          let errStr = JSON.stringify(error);
          this.authentication(errStr);
          reject(errStr);
        });
    });
  }
}

export default new ApiRequest();

summary

1. In fact, Redux, Redux saga and react router all introduce how to configure, but there is a problem in the order of plug-ins before and after integration
2. Connectedrouter is a plug-in for connecting Redux reducer and react router, and it should support immutable.js
3. React project integration immutable.js
4、antd-layoutui
5. Article code