This article is an original article, please quote the source, welcome everyone to collect and share 💐💐

The introduction

Hello everyone, some time ago, I wrote a Vue3 engineering project that works well. In fact, I wanted to transplant it to React side long ago. However, I was busy with work and put it off until now.

Now that we have moved to The Vite2 + React + TypeScript project, we would like to introduce how to build and use peripheral plugins properly and integrate them into the whole project. We also welcome you to read and add better ideas.

Next, in order to make you better understand the idea of engineering this project, this paper will read the following key words step by step:

  • React
  • Typescript
  • Vite
  • Redux Toolkit
  • mockjs
  • vite-plugin-mock
  • Ant Design Mobile

React

React Hook has received mixed reviews since its birth. It has the following advantages and disadvantages compared with traditional class component writing:

Advantages of hooks

1. Easier to reuse code: each useHook can generate independent state, easier to extract components, engineering decoupling, etc.; 2. Less code: There is no need to define tedious React Component template code. Read and write states do not need to be interspersed in each life hook, which makes the code structure shallow and simple. Hooks shortcomings

1. Performance overhead of side effects: If useEffect is used improperly to monitor a state change, it is easy to cause the interdependence of other states and generate call chain, bringing additional performance overhead; In addition, listen on global properties “such as: location… , and may cause global pollution;

2. Asynchronous code processing: when multiple states have dependencies, it is difficult to deal with their read and write order;

All single-file components of this project are written by hooks of React V16.8 +. The main consideration is that this project mainly introduces the engineering framework. The hook writing method can better help the definition and separation of components, present the modular structure, and better understand the whole structure.

Typescript

In recent years, TypeScript has become more and more popular on the front end, and TypeScript has become a necessary skill on the front end. TypeScript is a superset of JS types and supports generics, types, namespaces, enumerations, etc., making up for the shortcomings of JS in large-scale application development.

Vite

Vite is a new front-end build tool that dramatically improves the front-end development experience. Compared to Webpack, Vite still has its very unique advantages, here is a recommended article “Vite good and bad” for your reference.

Why did the project choose vite instead of webpack? Considering the community and individuals, there are several points :(I will not expand the details, the tweets have been very detailed analysis)

  • Vite is lighter and fast enough to build

    Webpack is implemented using NodeJS, while Viite uses esbuild pre-built dependencies. Esbuild is written using Go and is not an order of magnitude faster than pre-built dependencies of packers written in JavaScript.
  • The react template is also supported by Vite
  • Rapid development momentum, the future can be expected

Of course, everything has two sides, so far, Vite also has many defects, such as: ecology is not mature webpack, hidden unstable factors in the production environment are the problems it is facing now.

However, if we dare to move forward with our dreams, where will the technological development come from without the birth of new forces? Vite, by contrast, is more like a teenager and has moved on.

Redux Toolkit

React’s state management library has always been a disaster zone for wheels, with various design patterns that I won’t cover here.

The project is not complex, requiring low performance directly with useContext, useReducer, simple and easy to achieve; If you’re looking for a good design pattern that fits into the structure of your project, hand write a wheel based on Redux.

This project chooses Redux Toolkit as the project management. Firstly, it is an excellent framework among many products, with simple use and clear structure. Second, it encapsulates immer, making it easy to write asynchronous logic and work with most scenarios.

Engineering construction

So let’s get back to the point. We use these techniques to integrate them into a project. Generally used for enterprise production projects, to have the following capabilities:

  • Strong fault tolerance and expansibility
  • High cohesion of components, reduce coupling between modules
  • Clear project execution bus, easy to add slot logic
  • Highly abstract global methods
  • Resource compression + performance optimization

Against these indicators, let’s build a preliminary engineering framework step by step.

1. The technology stack

Programming: React16.8 + + Typescript build tools: Vite routing | state management: the react – the router – dom v6 + @ reduxjs/toolkit UI Element: Ant Design Mobile

2. Engineering structure

.├ ──.md.├ ─ Index.html ├── ├─ Package.json ├─ public ├─ SRC │ ├─ app.tsX └ │ ├─ App.module. Less │ ├─ API Request Center │ ├─ Assets ├── ├─ Constants Constants │ ├─ Vite - env. Which s global declarations │ ├ ─ ─ main. The TSX main entrance │ ├ ─ ─ pages page directory │ ├ ─ ─ routes routing configuration │ ├ ─ ─ types ts type definition │ ├ ─ ─ store state management │ └ ─ ─ utils Basic Kit ├─ Test Test Cases ├─.eslintrc.js EsLint ├─.Prettierrc. Json Prettier Config ├─.Gitignore Git ignores the config ├ ─ vite. ConfigCopy the code

SRC /utils contains global methods that can be invoked by project-wide files, as well as the project-initialized event bus “described below”. SRC /types and SRC /constants hold the type definitions and constants of the project, respectively, and are classified by page structure.

3. Engineering configuration

Set up the Vite + React project

# npm 6.x
npm init vite@latest my-vue-app --template react-ts

# NPM 7+ requires additional double lines:
npm init vite@latest my-vue-app -- --template react-ts

# yarn
yarn create vite my-vue-app --template react-ts

# pnpm
pnpm create vite my-vue-app -- --template react-ts
Copy the code

Then follow the prompts to operate!

Vite configuration

/* eslint-disable no-extra-boolean-cast */
import { defineConfig, ConfigEnv } from 'vite';
import styleImport from 'vite-plugin-style-import';
import react from '@vitejs/plugin-react';

import { viteMockServe } from 'vite-plugin-mock';
import { visualizer } from 'rollup-plugin-visualizer';

import path from 'path';

// https://vitejs.dev/config/
export default defineConfig(({ command }: ConfigEnv) = > {
  return {
    base: '/'.plugins: [
      react(),
      // mock
      viteMockServe({
        mockPath: 'mock'.// Mock file address
        localEnabled:!!!!! process.env.USE_MOCK,// Develop the package switch
        prodEnabled:!!!!! process.env.USE_CHUNK_MOCK,// Make packing switch
        logger: false.// Whether to display request logs on the console
        supportTs: true
      }),
      styleImport({
        libs: []}),!!!!! process.env.REPORT ? visualizer({open: true.gzipSize: true.filename: path.resolve(__dirname, 'dist/stats.html')}) :null].resolve: {
      alias: [{find: The '@'.replacement: '/src'}},css: {
      // CSS preprocessor
      preprocessorOptions: {
        less: {
          javascriptEnabled: true.charset: false.additionalData: '@import "./src/assets/less/common.less"; '}}},build: {
      terserOptions: {
        compress: {
          drop_console: true}},outDir: 'dist'.// Specify the output path
      assetsDir: 'assets' // Specify the path to generate static resources}}; });Copy the code

The mock interface is added in the mock directory. The mock interface starts with the command NPM run dev:mock.

FYI: The Vite-plugin-mock plugin provides DevTools network interception capability under vite scaffolding. If you want to implement more mock scenarios, use MockJS “project installed, ready to use”.

Coding standards

tsconfig

eslint

prettier

Event bus

The initialize(app) method is called in the main. TSX entry to standardize the project initialization process and to facilitate the insertion of custom logic into the process. The initialize code is as follows:

import React from 'react';
import ReactDOM from 'react-dom';
import { Toast } from 'antd-mobile';
import App from './App';
import { initialize } from '@/utils/workflow';

// Initialize the bus
initialize().then(flat= > {
  if(! flat) { Toast.show({icon: 'fail'.content: 'Initialization failed'
    });
    return;
  }

  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>.document.getElementById('root')); });Copy the code

In the method, the page’s REM adaptive layout initialization and other operations are completed respectively. In addition, Initialize supports asynchronous logic injection, which can be added by itself and returned with a Promise package.

Ps: Initialize method execution time Before the main App is mounted, do not place DOM operation logic here

4. React Router

Use react-router-dom v6 to hook a router. In addition, v6 version still has many advantages, please refer to the official team interpretation.

TSX composite components

// src/App.tsx
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import { DotLoading } from 'antd-mobile';
import { Provider } from 'react-redux';
import RouterComponent from '@/routes';
import Header from '@/components/header';
import store from '@/store';
import style from './app.module.less';

const App = () = > {
  return (
    <Provider store={store}>
      <div className={style.appBody}>
        <React.Suspense fallback={<DotLoading />} ><BrowserRouter>
            <Header />
            <RouterComponent />
          </BrowserRouter>
        </React.Suspense>
      </div>
    </Provider>
  );
};

export default App;
Copy the code

The RouterComponent and Header are wrapped in the BrowserRouter because routing capabilities are used for the entire page. Let’s look at the implementation of the RouterComponent:

// src/routes/index.tsx
import React, { FC, useEffect } from 'react';
import routes from './routesConfig';
import { Route, Routes, useNavigate, Navigate } from 'react-router-dom';
import { ErrorBlock } from 'antd-mobile';
import { IRoute } from '@/types/router';
import { isLogin } from '@/utils/userLogin';

// Route decorator
const RouteDecorator = (props: { route: IRoute }) = > {
  const { route } = props;
  const navigate = useNavigate();

  useEffect(() = > {
    // Authenticate the route guard
    if(route.meta? .requireAuth) {if(! isLogin()) { navigate('/login', { state: { redirect: route.pathname } }); }}// Custom route guard
    route.beforeCreate && route.beforeCreate(route);
    return () = > route.beforeDestroy && route.beforeDestroy(route);
  }, [route]);

  return <route.component />;
};

const RouterComponent: FC = () = > (
  <Routes>
    <Route path="/" element={<Navigate to="/index" />} / ><Route path="*" element={<ErrorBlock fullPage />} />
    {routes.map(route => (
      <Route
        key={route.pathname}
        path={route.pathname}
        element={<RouteDecorator route={route} />}} / >))</Routes>
);

export default RouterComponent;
Copy the code
  • Define two special routes: redirect and 404.
  • Define aroutesConfigThe configuration file, which records information about each routing page, is defined as follows:
      export interface IRoute extends RouteProps {
        / / path
        pathname: string;
        / / name
        name: string;
        // Chinese description, which can be used for sidebar list
        title: string;
        // react component function
        component: FC;
        // The hook executed when the page component is created
        beforeCreate: (route: IRoute) = > void;
        // The hook executed when the page component is destroyed
        beforeDestroy: (route: IRoute) = > void;
        / / property
        meta: {
          navigation: string;
          requireAuth: boolean;
        };
      }
    Copy the code
  • Define RouteDecorator RouteDecorator: the main function is route guard, plus perform custom hooks on each route page creation and destruction;
  • In config, each component passesreact-lazily-componentLazy plug-in loading, optimize the loading strategy;

5. Request Center

SRC/API contains asynchronous requests for each page, and directories are also divided by page structure. SRC/API /index.ts is its entry file, which is used to aggregate each request module, with the following code:

import { Request } from './request';
import box from './box';
import user from './user';

// Initialize axios
Request.init();

export default {
  box,
  user
  / /... Other request modules
};
Copy the code

SRC/API /request.ts SRC/API /request.ts SRC/API /request.ts

import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import { Toast } from 'antd-mobile';

import {
  IRequestParams,
  IRequestResponse,
  TBackData
} from '@/types/global/request';

interface MyAxiosInstance extends AxiosInstance {
  (config: AxiosRequestConfig): Promise<any>;
  (url: string, config? : AxiosRequestConfig):Promise<any>;
}

export class Request {
  public static axiosInstance: MyAxiosInstance;

  public static init() {
    // Create an axios instance
    this.axiosInstance = axios.create({
      baseURL: '/api'.timeout: 10000
    });
    // Initialize the interceptor
    this.initInterceptors();
  }

  // Initialize the interceptor
  public static initInterceptors() {
    // Set the POST header
    this.axiosInstance.defaults.headers.post['Content-Type'] =
      'application/x-www-form-urlencoded';
    /** * Request interceptor * Each request is preceded by a token */ in the request header if it exists
    this.axiosInstance.interceptors.request.use(
      (config: IRequestParams) = > {
        const token = localStorage.getItem('ACCESS_TOKEN');
        if (token) {
          config.headers.Authorization = 'Bearer ' + token;
        }
        return config;
      },
      (error: any) = > {
        Toast.show({
          icon: 'fail'.content: error }); });// Response interceptor
    this.axiosInstance.interceptors.response.use(
      // The request succeeded
      (response: IRequestResponse): TBackData= > {
        const {
          data: { code, message, data }
        } = response;
        if(response.status ! = =200|| code ! = =0) {
          Request.errorHandle(response, message);
        }
        return data;
      },
      // The request failed
      (error: AxiosError): Promise<any> = > {const { response } = error;
        if (response) {
          // The request has been issued, but it is outside the scope of 2xx
          Request.errorHandle(response);
        } else {
          Toast.show({
            icon: 'fail'.content: 'Network connection is abnormal, please try again later! '
          });
        }
        return Promise.reject(response?.data);
      }
    );
  }

  /** * HTTP handshake error *@param The RES response callback performs different operations depending on the response *@param message* /
  private static errorHandle(res: IRequestResponse, message? :string) {
    // Determine the status code
    switch (res.status) {
      case 401:
        break;
      case 403:
        break;
      case 404:
        Toast.show({
          icon: 'fail'.content: 'Requested resource does not exist'
        });
        break;
      default:
        // Error message judgment
        message &&
          Toast.show({
            icon: 'fail'.content: message }); }}}Copy the code

There are several things going on here:

  1. Configure the AXIos instance in the interceptor Settings request and the corresponding interception operation, to order the server to returnretcodeandmessage;
  2. rewriteAxiosInstanceThe TS type (byAxiosPromisePromise<any>), correct the call to determine the type of returned data;
  3. Set an initialization functioninit(), generates an instance of AXIos for the project to call;
  4. configurationerrorHandleHandle, handling error;

Of course, in step 2, you can add additional request interception, such as RSA encryption, local cache policy, etc. When logic is too much, it is recommended to introduce through functions.

At this point, we can happily use Axios to request data.

// API module → Request center
import { Request } from './request'; userInfo: (options? : IRequestParams):Promise<TUser> =>
  Request.axiosInstance({
    url: '/userInfo'.method: 'post'.desc: 'Get user information'.isJSON: true. options })// Business module → API module
import request from '@/api/index';

request.user
  .userInfo({
    data: {
      token
    }
  })
  .then(res= > {
    // do something...
  });
Copy the code

5. SSR

To complement…

The performance test

Development environment startup

Vite pre-bundling to the main application at cold start took 1463ms to start up, which is fast enough to say 😆.

The built resource bundle

The subcontracting strategy is to cut according to the routing page and separate JS and CSS separately.

Lighthouse test

The above is a local test. The first screen is about 1000ms~1500ms. The pressure mainly comes from the loading of Vendor. js and the pulling of the first screen image resources (the first screen image resources come from the network). In fact, after loading through module segmentation, the js package of the home page is compressed to 4.3 KB by gzip. Of course, the real scenario is that the loading speed of local resources cannot be achieved after the cloud server is deployed in the project, but the optimization can be accelerated through CDN, and the effect is quite significant.

Performance

Refer to the article

The Good and The Bad of Vite the Core Difference between Vite and Webpack

Write in the last

Thank you for reading and welcome correction, welcome to pay attention to my public number “is ma Fei Ma”, play together! 🌹 🌹

GitHub project portal