preface

I stayed in a small company for more than three years. The front end team was very small and there was no front end boss. I was completely in a state of self-exploration. I started to take charge of front-end projects independently two years ago, and I am keen on hand-built projects. For me at that time, everything was in a state of hazy, although there is a thought of the project design is better, but there is no better/ideas (like just start writing project, calling the backend interface are dispersed in each module, no unified in a directory on maintenance, if the backend interface has changed, You need to search globally to modify the interface one by one… . Later, I read a lot of books, articles, other people’s open source projects and painful project reconstruction history. Gradually, I accumulated some project experience and accumulated my own accumulation (configuring project templates, writing scaffolding, building component library…). Gradually, the front end is engineered in this direction.

The purpose of writing this article is to give a reference to those who are in the same situation as me and like to build projects by their own hands, so that beginners can take less detours. If you have better suggestions, please let me know, thank you very much.

Project file tree structure

Project characteristics

Normalize.css

CSS reset is relatively “violent”, whether you have no use, all reset to the same effect, and the impact of a large range of consistency across browsers. Normalize. CSS is an alternative to CSS reset because it emphasizes universality and maintainability rather than consistency. It provides a high degree of consistency across browsers in the default HTML element styles. Normalize.css is a modern, high-quality alternative to traditional CSS reset, ready for HTML5.

CSS modularization is supported by default

  • You can use csS-Loader parameters to implement CSS modularization
module: {
   rules: [{test: /\.css$/.use: ['style-loader'.'css-loader'] {},test: /\.less$/.use: [
         'style-loader',
         {
           loader: 'css-loader'.options: {
             importLoaders: 2.localsConvention: 'camelCase'.modules: {
               localIdentName: '[name]__[local]--[hash:base64:5]'}}},'less-loader']}],}Copy the code

postcss-loader + autoprefixer

  • Automatic compatibility handles style issues across browsers
module: {
  rules: [{test: /\.css$/.use: ['style-loader'.'css-loader'.'postcss-loader'] {},test: /\.less$/.use: [
        'style-loader',
        {
          loader: 'css-loader'.options: {
            importLoaders: 2.localsConvention: 'camelCase'.modules: {
              localIdentName: '[name]__[local]--[hash:base64:5]'}}},'postcss-loader'.'less-loader']}],}Copy the code

postcss.config.js


// postcss-loader automatically finds and invokes this file
const autoprefixer = require('autoprefixer');
module.exports = {
    plugins: [autoprefixer()],
};
Copy the code

Custom configuration HTML

  • Configuration is more flexible, especially for multi-page applications

      
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge, chrome=1"/>
    <meta name="renderer" content="webkit"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>Home page</title>
    <%htmlWebpackPlugin.options.dependencies.css.forEach(css=>{% ><link rel="stylesheet" href="<%= css %>">
    <% %})>
</head>
<body>
<div id="root"></div>
<%htmlWebpackPlugin.options.dependencies.js.forEach(js=>{% ><script src="<%=js%>"></script>
<% %})>
</body>
</html>
Copy the code

webpack.html.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');

/** * Production environment resource package path configuration */
const libs = {
    // iconfont: {
    // js: ['./fonts/iconfont.js'],
    // css: ['./fonts/iconfont.css']
    // },
};

/** * Development environment resource pack path configuration */
if (process.env.NODE_ENV === 'development') {
    Object.assign(libs, {
        // iconfont:{
        // js: ['//at.alicdn.com/t/font_xxx.js'],
        // css: ['//at.alicdn.com/t/font_xxx.css']
        // },
        dll: {
            js: ['/public/dll/dllLibs.dll.js']}}); }function createHtmlWebpackPluginConfig(chunkName, path, modules, chunks) {
    const config = {
        favicon: './src/entry/favicon.ico'.filename: `${chunkName}.html`.template: `${path || './src/entry/index.html'}`.inject: true.chunks: ['vendors', chunkName].concat(chunks),
        chunksSortMode: 'dependency'.minify: {
            removeComments: true.collapseWhitespace: false
        },
        dependencies: {
            css: [].js: []}}; modules && modules.forEach(m= > {
        if (m && m.css && m.css.length > 0) {
            config.dependencies.css = config.dependencies.css.concat(m.css);
        }
        if (m && m.js && m.js.length > 0) { config.dependencies.js = config.dependencies.js.concat(m.js); }});return new HtmlWebpackPlugin(config);
}

module.exports = [
    createHtmlWebpackPluginConfig('index'.' ', [libs.dll]),
];
Copy the code

DllPlugin

  • However, I am used to using this dependency cache plug-in in the development environment, which is not configured in the production environment. Therefore, when TESTING in the development environment of the new version of Webpack, the speed of improvement is not very obvious. For the future Webpack 5, This plugin makes even less sense.
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');

module.exports = {
    mode: 'development'.context: path.resolve(__dirname, ".. /"),
    entry: {
        dllLibs: ['react'.'react-dom'.'lodash'.'antd'.'react-redux'.'redux'.'history'.'react-router-dom'.'connected-react-router'.'axios'.'events'.'moment'.'react-beautiful-dnd']},output: {
        path: path.resolve('public'),
        [name] indicates the name of the current dynamic link library
        filename: 'dll/[name].dll.js'.// The default is the global variable var. If exported in this way, it can only be accessed globally by script
        libraryTarget: 'var'.// The name of the global variable used to store the dynamic link library, for example, _dll_libs
        library: '_dll_[name]',},plugins: [
        new DllPlugin({
            // The global variable name of the dynamically linked library needs to be the same as that in output.library
            // The value of this field is the value of the name field in the output manifest.json file
            // For example, libs.manifest.json has "name": "_dll_libs"
            name: '_dll_[name]'.// The name of the manifest.json file that describes the output of the dynamic link library
            path: path.join('public'.'dll/[name].manifest.json'),})]};Copy the code

@babel/preset-typescript

  • Use @babel/preset-typescript to parse TS. If you want to validate TS files, just run this commandnpm run type-check

package.json

"scripts": {
     "type-check": "tsc --watch",
}
Copy the code

tsconfig.json

{"compilerOptions": {// don't generate files, just type check "noEmit": true,}}Copy the code
  • For more TS translation solutions, see Webpack translation Typescript existing solutions

tsconfig-paths-webpack-plugin

tsconfig.json

{"compilerOptions": {// baseUrl": "./", "paths": {/* Set of pathmaps */ "@public/*": ["public/*"], "@src/*": ["src/*"], "@assets/*": ["src/assets/*"], "@styles/*": ["src/assets/styles/*"], "@common/*": ["src/common/*"], "@components/*": ["src/components/*"], "@library/*": ["src/library/*"], "@routes/*": ["src/routes/*"], "@store/*": ["src/store/*"], "@server/*": ["src/server/*"], "@api/*": ["src/server/api/*"], "@utils/*": ["src/utils/*"] } } }Copy the code

webpack.base.config.js

resolve: {
    plugins: [
      // Map the path configuration in tsconfig.json to webpack
      new TsconfigPathsPlugin({
        configFile: './tsconfig.json'})].// Since the TsconfigPathsPlugin plugin is used, there is no need to map the path
      // alias: {
      // "@src": path.resolve('src'),
      // "@public": path.resolve('public'),
      // "@assets": path.resolve('src/assets'),
      // },
}
Copy the code

Configurable routes and lazy route loading are supported

export interface RouteConfigDeclaration {
    /** * Current routing path */
    path: string;
    /** * Current route name */name? :string;
    /** * Whether routes are strictly matched */exact? :boolean;
    /** * Whether route authentication is required */isProtected? :boolean;
    /** * Whether route redirection is required */isRedirect? :boolean;
    /** * Whether to dynamically load routes */isDynamic? :boolean;
    /** ** Dynamic load route prompt text */loadingFallback? :string;
    /** * Routing component */
    component: any;
    /** ** child path */routes? : RouteConfigDeclaration[]; }export const routesConfig: RouteConfigDeclaration[] = [
    {
        path: '/',
        name: 'root-route',
        component: App,
        routes: [
            {
                path: '/home'.// exact: true,
                isDynamic: true.// loadingFallback: 'Not the same loading content... ',
                // component: Home,
                // component: React.lazy(
                / / () = >
                // new Promise(resolve =>
                // setTimeout(
                / / () = >
                // resolve(
                // import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
                / /),
                / / 2000,
                / /),
                / /),
                // ),
                component: React.lazy((a)= >
                    import(/* webpackChunkName: "home"*/ '@src/views/home/Home'),
                ),
                routes: [
                    {
                        path: '/home/child-one',
                        isDynamic: true,
                        component: React.lazy((a)= >
                            import(/* webpackChunkName: "child-one" */ '@src/views/home/ChildOne'),
                        ),
                    },
                    {
                        path: '/home/child-two',
                        isRedirect: true,
                        isDynamic: true,
                        component: React.lazy((a)= >
                            import(/* webpackChunkName: "child-two" */ '@src/views/home/ChildTwo'),
                        ),
                    },
                ],
            },
            {
                path: '/login',
                isDynamic: true,
                isRedirect: true,
                component: React.lazy((a)= >
                    import(
                        /* webpackChunkName: "login" */
                        '@src/views/login/Login'
                    ),
                ),
            },
            {
                path: '/register',
                isDynamic: true,
                component: React.lazy((a)= >
                    import(/* webpackChunkName: "register"*/ '@src/views/register/Register'),),},],},];Copy the code

ESLint+Prettier

  • Use ESLint +Prettier to unify front-end code styles
  • You can configure it in the editor to automatically format the code when the file is saved. Use Prettier in WebStorm to format code automatically.

husky + lint-staged

  • Check your code style and fix it before committing: Each commit only checks the files that have been modified (compared to git staging), saving a lot of time.
"husky": {
  "hooks": {
    "pre-commit": "lint-staged"
  }
},
"lint-staged": {
  "src/**/*.{js,jsx,ts,tsx}": [
    "eslint --fix",
    "prettier --write"
  ]
},
Copy the code

rematch

  • Rematch uses Dva and Mirror as references to redux. Rematch has no redundant Action types, Action Creators, switch statements, Thunks, Saga, or store configurations, greatly simplifying the cost of Redux.

models/register/index.ts

// Add state
const INCREMENT = 'INCREMENT';

import { RootDispatch, RootState } from '@src/store';

export interfaceRegisterStateDeclaration { pageName? :string;
    count: number;
}

const state: RegisterStateDeclaration = {
    pageName: 'register',
    count: 0};export default {
    name: 'register',
    state,
    reducers: {
        [INCREMENT]: (state: RegisterStateDeclaration, payload): RegisterStateDeclaration= > {
            // Prints a proxy instance object
            // console.log(state);
            state.count += 1;
            // Finally return the entire state tree (the current model's state tree -- login)
            returnstate; }},// There are two ways to write this: one is to use a constant as the key, and the other is to define it directly
    effects: (dispatch: RootDispatch) = > ({
        // async incrementAsync(payload, rootState: RootState) {
        async incrementAsync() {
            await new Promise(resolve= >
                setTimeout((a)= > {
                    resolve();
                }, 1000));// Issue the action in login
            // dispatch.login.INCREMENT();
            this.INCREMENT(); }}),// effects: {
    // async incrementAsync(payload, rootState: RootState) {
    // await new Promise(resolve =>
    // setTimeout(() => {
    // resolve();
    / /}, 1000),
    / /);
    // this.INCREMENT();
    / /},
    // },
};
Copy the code

events

  • Use Events to create a global event hub (publish subscription), and although redux has been used in the project as a tool for global communication, in some cases you still rely on event subscriptions and notifications.

utils

  • Built-in some useful utility functions, as follows:
/** * Test variable type * @param type */
function isType(type) {
    return function(value) :boolean {
        return Object.prototype.toString.call(value) === `[object ${type}] `;
    };
}

export const variableTypeDetection = {
    isNumber: isType('Number'),
    isString: isType('String'),
    isBoolean: isType('Boolean'),
    isNull: isType('Null'),
    isUndefined: isType('Undefined'),
    isSymbol: isType('Symbol'),
    isFunction: isType('Function'),
    isObject: isType('Object'),
    isArray: isType('Array'),};Copy the code

The project address

react-ts-project-template

reference

Use ESLint + Prettier to unify front-end code styles

Build super-smooth code from Husky and Lint-staged to review the workflow

Recommended reading

TS FAQ sorting (more than 60, continuously updated ing)

Do you really understand the React lifecycle

React Hooks 【 nearly 1W words 】+ project combat

React SSR: + 2 projects

Implement a simple Webpack from 0 to 1

Webpack translates existing Typescript schemas

Cookie, Session, Token, JWT