current position:Home>Front end Engineering - scaffold

Front end Engineering - scaffold

2021-08-27 14:03:02 Lao Liu talks big

Engineering is a kind of software engineering . performance , stability , Usability , Maintainability , Efficiency and other problems need to be solved in engineering .

The real development of the front end , That's it 10 Year time , Engineering development is not perfect .

And our daily development , First, solve the problem of creating a project . So vue There's a community vue-cli, react There's a community create-react-app, However, these scaffolds are only general solutions , They don't pay attention to the design of the upper layer , such as : Unified ajax encapsulation ,library external,docker The construction of , Formwork engineering constraints , Solidify team style code specification constraints , Unified control of public dependence , Service monitoring alarm , Dial test , Strong cache control ,CDN Access, etc .

If we have multiple service creation requirements ( Huge application splitting , Multiple projects ), The above questions , You need to manually access one by one , inefficiency .

here , We need a scaffold to solve these problems .

This article will be react For example , Implement simple scaffolding , The complete scaffold can be used for me ~

One . lerna

lerna Is a multi package management tool , He mainly solves many problems package Interdependence and management issues .

Generally speaking , Our scaffolding cli And formwork works ,webpack scripts, as well as utils All are Subcontracting management , And these packages are interdependent .

For more information , Please refer to : github.com/lerna/lerna

Initialize project

lerna init
 Copy code 

adopt lerna init Initialize a multi package project ,lerna Automatically created for us lerna.json as well as pakcages. This pakcages This is the directory of each package we will develop .

establish cli engineering

#  establish cli project 
lerna create gw-cli 

#  establish webpack scripts
lerna create gw-scripts

#  Create a template project 
lerna create gw-web-template

#  establish eslint, prettier modular 
lerna create eslint-config-gw
 Copy code 

Other commands :

Initialize each packages rely on

lerna bootstrap
 Copy code 

Create soft connections for package interdependencies

lerna link
 Copy code 

After the package development is completed , Multi package dependency automatic publishing

Release

lerna publish
 Copy code 

Two . gw-scripts

cli bin

Get into gw-scripts Folder , establish bin Folder ,bin Create under file index.js, here , It should be noted that ,package.json The following configuration needs to be added in the :

"bin": {
  "gw-scripts": "./bin/index.js"
}
 Copy code 

It means , When we execute gw-scripts On command , Will perform bin/index.js Content :

(./bin/index.js):

#!/usr/bin/env node

const spawn = require('cross-spawn');
const chalk = require("chalk");
const args = process.argv.slice(2);

const scriptIndex = args.findIndex(
  x => x === 'prod' || x === 'develop'
);
const script = scriptIndex === -1 ? args[0] : args[scriptIndex];
const nodeArgs = scriptIndex > 0 ? args.slice(0, scriptIndex) : [];

if(['develop', 'prod'].includes(script)) {
  /** * @returns result * { * status: 0, * singal: null, * output: [], * pid: xx, * error: '', * ... * } */
  const result = spawn.sync(process.execPath,
    nodeArgs
      .concat(require.resolve('../scripts/' + script))
      .concat(args.slice(scriptIndex + 1)),
    { stdio: 'inherit' }
  );

  if(result.signal) {
    console.log(`build error : signal = ${result.signal}`);
    console.log(`result = ${JSON.stringify(result)}`);
    process.exit(1);
  }
  process.exit(result.status);
}else {
  console.log()
  console.log( chalk.red(`not exist script : ${script}`) );
  console.log();
  console.log( chalk.cyan("your can run develop or prod") );
}

 Copy code 

Installation dependency

yarn add @babel/[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected] 
[email protected]
 Copy code 

To configure react webpack dev

For reference only

const HtmlWebpackPlugin = require("html-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const paths = require('./paths');

process.env.NODE_ENV = 'development';

const hasJsxRuntime = (() => {
  try {
    require.resolve('react/jsx-runtime');
    return true;
  } catch (e) {
    return false;
  }
})();

const config = {
  entry: paths.appIndexJs,
  output: {
    filename: 'bundle.js',
    path: paths.appBuild,
    publicPath: "/",
    library: paths.appName,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${paths.appName}`,
  },
  resolve: {
    alias: {
      "@": paths.appSrc
    },
    extensions: [".js", ".json", ".jsx", ".css", ".scss", ".tsx"]
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    "antd": "antd"
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        parser: {
          // require.ensure  Not the standard , Not allowed 
          requireEnsure: false
        }
      },
      {
        oneOf: [
          {
            test: /\.(js|jsx|ts|tsx)$/,
            exclude: /node_modules/,
            loader: require.resolve('babel-loader'),
            options: {
              babelrc: false,
              configFile: false,
              presets: [
                [
                  require.resolve('babel-preset-react-app'),
                  {
                    runtime: hasJsxRuntime ? 'automatic' : 'classic',
                  },
                ],
              ],
              // webpack  Future proposals , Cache the compiled results in : ./node_modules/.cache/babel-loader/
              cacheDirectory: true,
              cacheCompression: false,
              compact: false
            }
          },
          // css module
          {
            test: /\.module\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          },
          // // scss
          {
            test: /\.(scss|sass)$/,
            use: [
              'style-loader',
              'css-loader',
              'sass-loader'
            ]
          },
          // // sass module
          {
            test: /\.module\.(scss|sass)$/,
            use: [
              'style-loader',
              'css-loader',
              'sass-loader'
            ]
          }
        ]
      }
    ]
  },

  plugins: [
    new HtmlWebpackPlugin({
      inject: true,
      filename: "index.html",
      template: "public/index.html"
    }),
    new ProgressBarPlugin()
  ],

  node: {
    module: 'empty',
    dgram: 'empty',
    dns: 'mock',
    fs: 'empty',
    http2: 'empty',
    net: 'empty',
    tls: 'empty',
    child_process: 'empty',
  },
  performance: false,

  mode: 'development'
}

module.exports = config;

 Copy code 

react webpack prod

For reference only


const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const paths = require('./paths');

process.env.NODE_ENV = 'production';
process.env.BABEL_ENV = 'production';

module.exports = {
  mode: "production",
  bail: true,
  devtool: false,
  entry: paths.appIndexJs,
  output: {
    path: paths.appBuild,
    filename: "static/js/[name].[chunkhash:8].js",
    chunkFilename: "static/js/[name].[chunkhash:8].chunk.js",
    publicPath: '',
    library: paths.appName,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${paths.appName}`,

    devtoolModuleFilenameTemplate: info => path.resolve(paths.appSrc, info.absoluteResourcePath).replace(/\\/g, "/")
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          parse: {
            ecma: 8
          },
          compress: {
            ecma: 5,
            warnings: false,
            comparisons: false
          },
          mangle: {
            safari10: true
          },
          output: {
            ecma: 5,
            comments: false,
            ascii_only: true
          }
        },
        parallel: true,
        cache: true,
        sourceMap: false
      }),
      new OptimizeCSSAssetsPlugin({
        //  Enable  cssnano safe Pattern 
        cssProcessorOptions: {
          safe: true
        }
      })
    ],
    splitChunks: {
      chunks: "all",
      name: true,
      maxInitialRequests: Infinity
    },
    runtimeChunk: true
  },
  resolve: {
    alias: {
      "@": paths.appSrc
    },
    extensions: [".js", ".json", ".jsx", ".css", ".scss", ".tsx"]
  },
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM',
    "antd": "antd"
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        parser: {
          // require.ensure  Not the standard , Not allowed 
          requireEnsure: false
        }
      },
      {
        oneOf: [
          {
            test: /\.(js|jsx|ts|tsx)$/,
            exclude: /node_modules/,
            loader: require.resolve('babel-loader'),
            options: {
              babelrc: false,
              configFile: false,
              presets: [
                [
                  require.resolve('babel-preset-react-app'),
                  {
                    runtime: 'automatic',
                  },
                ],
              ],
              // webpack  Future proposals , Cache the compiled results in : ./node_modules/.cache/babel-loader/
              cacheDirectory: true,
              cacheCompression: false,
              compact: false
            }
          },
          // css module
          {
            test: /\.module\.css$/,
            use: [
              'style-loader',
              'css-loader'
            ]
          },
          // scss
          {
            test: /\.(scss|sass)$/,
            use: [
              'style-loader',
              'css-loader',
              'sass-loader'
            ]
          },
          // sass module
          {
            test: /\.module\.(scss|sass)$/,
            use: [
              'style-loader',
              'css-loader',
              'sass-loader'
            ]
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      inject: true,
      filename: "index.html",
      template: "public/index.html"
    }),
    new ProgressBarPlugin(),
    new MiniCssExtractPlugin({
      filename: "static/css/[name].[contenthash:8].css",
      chunkFilename: "static/css/[name].[contenthash:8].chunk.css"
    }),
    new ManifestPlugin({
      fileName: "asset-manifest.json"
    }),
  ],
  node: {
    dgram: "empty",
    fs: "empty",
    net: "empty",
    tls: "empty",
    child_process: "empty"
  },
  performance: false
}
 Copy code 

Verify the port usage of the development environment

const chalk = require('chalk');
const detect = require('detect-port-alt');

function choosePort(host, defaultPort) {
  return detect(defaultPort, host).then(
    port => {
      return new Promise(resolve => {
        if (port === defaultPort) {
          return resolve(port);
        }else {
          console.log(chalk.red(`Something is already running on port ${defaultPort}`))
          return resolve(null);
        }
      })
    },
    err => {
      throw new Error(
        chalk.red(`error at ${chalk.bold(host)}.`) +
          '\n' +
          ('Network error message: ' + err.message || err) +
          '\n'
      );
    }
  )
}
 Copy code 

merge config

Customize webpack To configure , You can put the root directory , Such as new construction gw.config.js, Use webpack-merge Merge configuration

establish dev compiler object

function createCompiler(webpack, config) {
  let compiler;
  try {
    compiler = webpack(config);
  } catch (err) {
    console.log(chalk.red('Failed to compile.'));
    console.log();
    console.log(err.message || err);
    console.log();
    process.exit(1);
  }

  compiler.plugin('invalid', () => {
    console.log('Compiling...');
  });

  compiler.plugin('done', stats => {
    const messages = stats.toJson({}, true)
    const isSuccessful = !messages.errors.length && !messages.warnings.length;
    //  success 
    if (isSuccessful) {
      console.log(chalk.green('Compiled successfully!'));
    }

    // error
    if (messages.errors.length) {
      console.log(chalk.red('Failed to compile.\n'));
      console.log(messages.errors.join('\n\n'));
      return;
    }

    //  Warning 
    if (messages.warnings.length) {
      console.log(chalk.yellow('Compiled with warnings.\n'));
      console.log(messages.warnings.join('\n\n'));
    }
  });

  return compiler;
}
 Copy code 

To configure dev server

module.exports = function (proxy, allowedHost) {
  return {
    disableHostCheck: true,
    compress: true,
    clientLogLevel: 'none',
    contentBase: paths.appPublic,
    watchContentBase: true,
    hot: true,
    publicPath: config.output.publicPath,
    quiet: true,
    watchOptions: {
      ignored: '/node_modules/',
      poll: true
    },
    host: host,
    overlay: true, //  When there is a compilation error , Display errors directly on the page 
    historyApiFallback: {
      disableDotRule: true,
    },
    stats: "normal",
    public: allowedHost,
    proxy,
    before(app) {
      // console.log('before',app)
    },
    after(app) {
      // console.log('after')
    },
  };
};
 Copy code 

establish dev server example

const devServer = new WebpackDevServer(compiler, serverConfig);

  devServer.listen(port, HOST, err => {
    if(err) {
      console.log(err);
      return;
    }
    ["SIGINT", "SIGTERM"].forEach(function(sig) {
      process.on(sig, function() {
        devServer.close();
        process.exit();
      });
    });
  });
 Copy code 

3、 ... and . eslint-config-gw

About eslint Importance , No more details here . eslint All the rules of Not necessarily , But use Suitable for the team Common specifications are sufficient .

eslint The rules , Can be packaged into separate modules , It can be used in web End , react native etc. , utilize eslint extend , We can easily use Common basic rules .

Basics rules

{
    //  Use humps 
    "camelcase": 1,
    //  Disable variables declared as 
    "no-undef": 2,
    //  Do not extend native types , For example, add Object The prototype of the 
    "no-extend-native": 2,
    //  Do not use return In the sentence , Use assignment 
    "no-return-assign": 2,
    //  Automatic adjustment import The order 
    "import/order": 0,
    //  Prohibit import not in package.json The dependency declared in , There may be a declaration in the parent 
    "import/no-extraneous-dependencies": 0,
    // commonjs Of require And esm Different , It can provide dynamic parsing at run time , Although some scenarios need to use , But the suggestion is still static 
    "import/no-dynamic-require": 0,
    //  Some file parsing algorithms run omitting the reference file extension , There is no need for strong intervention here 
    "import/extensions": 0,
    //  The import module is a module of the text file system , Close here , avoid eslint incognizance webpack alias
    "import/no-unresolved": 0,
    //  When the module has only 1 When exporting , Prefer to use export default, Instead of naming the export 
    "import/prefer-default-export": 0,
    //  Unused variables are prohibited 
    "no-unused-vars": [2, { "vars": "all", "args": "none" }],
    //  mandatory generate Function  *  Use consistent spaces around 
    "generator-star-spacing": 0,
    //  Disallow the unary operator , There is no need to prohibit 
    "no-plusplus": 0,
    //  Named... Is required function expression , There is no need for 
    "func-names": 0,
    //  No use console.log, Some scenarios may require console Print log 
    "no-console": 0,
    //  It's forced on parseInt Cardinality parameter used in , You don't need to 
    "radix": 0,
    //  Prohibit the use of control statements in regular expressions , such as :/\x1f/
    "no-control-regex": 2,
    //  No use continue, Some situations may require 
    "no-continue": 0,
    //  No use debugger, The production environment is not allowed , The development environment can support debugger
    "no-debugger": process.env.NODE_ENV === 'production' ? 2 : 0,
    //  No right function Parameter assignment , It's not mandatory here , Warning 
    "no-param-reassign": 1,
    //  The identifier is prohibited  '_',  occasionally lodash There will be , It's not mandatory here , Warning 
    "no-underscore-dangle": 1,
    //  requirement require Write at the top of the code , You may need to be dynamic first require The situation of , A warning is given here 
    "global-require": 1,
    //  Defining variables , Required let, const, instead of  var
    "no-var": 2,
    //  All required var, Must be at the top of the scope 
    "vars-on-top": 2,
    //  Object and array deconstruction are preferred 
    "prefer-destructuring": 1,
    //  Prohibit unnecessary string literal or template literal Links 
    "no-useless-concat": 1,
    //  It is forbidden to declare a variable with the same name as the outer scope 
    "no-shadow": 2,
    //  requirement for in in , There must be a if sentence , Without filtering the results , May appear bug, There is no need for 
    "guard-for-in": 0,
    //  The use of specific syntax is prohibited , You can refer to :https://cn.eslint.org/docs/rules/no-restricted-syntax
    "no-restricted-syntax": 1,
    //  Mandatory methods must have a return value , Actually, it's not necessarily 
    "consistent-return": 0,
    //  Judge equal , You have to use  === , instead of  ==
    "eqeqeq": 2,
    //  Never use expressions 
    "no-unused-expressions": 1,
    //  Access variables within a block outside the block level scope , Prompt 
    "block-scoped-var": 1,
    //  Multiple declarations are prohibited , The same variable 
    "no-redeclare": 2,
    //  Force the callback function to use the arrow function 
    "prefer-arrow-callback": 1,
    //  Force the callback method of the array , need return
    "array-callback-return": 1,
    //  requirement switch sentence , Need to have default Branch 
    "default-case": 2,
    //  It is forbidden to , appear function Declarations and expressions 
    "no-loop-func": 2,
    //  prohibit case The sentence failed 
    "no-fallthrough": 2,
    //  Connection assignment is prohibited ,let a = b = c = 1;
    "no-multi-assign": 2,
    //  prohibit  if Appear alone in  else In the sentence ,else if It will be clearer 
    "no-lonely-if": 2,
    //  Do not allow nonstandard spaces in strings , Except for notes 
    "no-irregular-whitespace": 2,
    //  Required const Declare variables that are no longer modified 
    "prefer-const": 2,
    //  It is forbidden to use variables before defining them 
    "no-use-before-define": 2,
    //  Unnecessary escape characters are prohibited 
    "no-useless-escape": 2,
    //  Ban array Constructors , You can refer to :https://eslint.org/docs/rules/no-array-constructor#disallow-array-constructors-no-array-constructor
    "no-array-constructor": 0,
    //  Requires or disables method attribute abbreviations in literals 
    "object-shorthand": 0,
    //  Do not call directly Object.prototype Built in properties 
    "no-prototype-builtins": 2,
    //  Multiple nested ternary expressions are prohibited 
    "no-nested-ternary": 2,
    //  No right String, Number, Boolean Use new The operator , There is no reason to wrap these primitive types into constructors 
    "no-new-wrappers": 2,
    // promise reject Why , Need to pass through Error Object packaging 
    "prefer-promise-reject-errors": 1,
    //  No use label sentence 
    "no-labels": 2,
    //  format 
    "prettier/prettier": 2
}
 Copy code 

vscode Integrate

  • First installation vscode eslint plug-in unit , install perttier plug-in unit .
  • To configure vscode -> file -> Preferences -> setting.json, Set the following rules :
{
  "eslint.validate": [
    {
      "language": "vue",
      "autoFix": true
    },
    {
      "language": "html",
      "autoFix": true
    },
    {
      "language": "javascript",
      "autoFix": true
    },
    {
      "language": "javascriptreact",
      "autoFix": true
    }
  ],
  "eslint.autoFixOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "window.zoomLevel": 0,
  "editor.fontSize": 16
}
 Copy code 

Four . gw-web-template

react web project , Need a template project . Control the of each project react, react-router, react-dom, antd, And the version number of the library is very important . Construction of basic database , Construction of component library , And more friendly access to the micro front end in the later stage , It's all very important .

Formwork works

A template project , These should be built in ( Not limited to ):

  • react, react-dom, react-router, antd, reset.css Wait for the base database , and CDN introduce
  • Construction of static resources path, And the upper ingress Strong cache control
  • reasonable code split, as well as hash chunk control
  • Unified css Modular solution , Here the css module
  • Front end container deployment , Build a private image library , Built in business compliant dockerfile. And reasonable image resource optimization ( Such as : Image layering )
  • built-in eslint
  • built-in prettier
  • Integration unit testing
  • common gitigore
  • built-in ts, And reasonable tsconfig Set up
  • Unified ajax Library Introduction , Need to be uniformly encapsulated . It can be a direct call to , It can also be hooks Way to call
  • Integrate redux,react-router, Route authority control
  • Customize hooks library
  • Custom component library
  • Build script , Integrated operation and maintenance CI,CD
  • Access the service dial test program of the upper layer , or k8s The heart of
  • Access custom exception monitoring service
  • Constraints develop uniform specification components , Friendly access front-end automated testing
  • Reasonable Modular Scheme , Unified assert Path Prefix , So that at the front of the upper layer ingress Forward and collect information
  • ...

All the above must be done for Formwork Engineering , At the same time, for scaffold users Shielded . That is, scaffold users do not pay attention to these , Developers just need to invest in business development .

Engineering structure

The engineering structure is not invariable , This is just the team's constraint specification , We all abide by .

The following is for reference only

|--build             Packaged results folder 
|--public            Public folders 
|--src               Source folder 
    |-- api            Interface definition 
    |-- assets        iconfont etc. 
    |-- config         Configure class folder , Such as axios To configure 
    |-- model          data storage , Such as  redux
    |-- page           View layer folder 
    |-- router         Route definition folder 
    |-- util           Tools folder 
    |-- index.tsx      The main entrance 
|--types            typescript  Type definition folder 
|--.editorconfig
|--.eslintignore
|--.eslintrc.js
|--.gitattributes
|--.gitignore
|--.npmignore
|--.prettierignore
|--build.sh          Build script 
|--Dockerfile         
|--package.json
|--README.md
|--run.sh            What needs to be done when building a container image shell
|--tsconfig.json    ts Rule configuration 

 Copy code 

5、 ... and . cli

After the preparation of the above modules , We need to make a npm modular , Let users install . Custom interactive commands . This is it. cli Main role of .

Theoretically , The command line interaction module only does Interactive events , scripts And formwork works and other modules , Should be split separately , Easy to reuse .

Command line interaction design , It also depends on the actual situation of each company .

It can be roughly divided into two categories :

  • react web
  • react native
  • The document library
  • Component library
  • Micro front end base
  • Micro front terminal service
  • hooks library
  • ...

Be careful : Secondary interaction , You don't have to set it eslint,ts And so on , For the whole team , It's best to standardize , Unified use . There is no need to learn from vue-cli, cra To do . Because everyone is facing Users Dissimilarity , The ultimate goal is also different .

Set global command

package.json Add :

"bin": {
  "gw": "./index.js"
},
 Copy code 

check node edition

const currentNodeVersion = process.versions.node;
const semver = currentNodeVersion.split('.');
const major = semver[0];

if (major < 10) {
  console.error(
    'You are running Node ' +
      currentNodeVersion +
      '.\n' +
      'gw cli requires Node 10 or higher. \n' +
      'we recommand 10.18.0'
  );
  process.exit(1);
}
 Copy code 

commander

Use commander Get user input command .

such as , We hope

gw create <project-name>
 Copy code 

Represents the creation of a project .

We can also integrate the runtime , reference Ruby on Rails Interaction , You can automatically create files .

We hope , By the following order , To automatically create pages index.tsx, index.module.css, dto.ts, index.test.js wait .

gw page <page-path>
 Copy code 

wait ...

We are free to play , But there is only one purpose : Let the repetitive work be handed over to the machine

Set up interaction

The interaction module uses inquirer, You can do the following packaging :

const inquirer = require("inquirer");

module.exports = function(callback) {
  const prompt = inquirer.createPromptModule();
  prompt([
    {
      name: "type",
      type: 'list',
      message: "'Which basic framework do you want to choose!",
      choices: [
        { name: "react web", value: "web" },
        { name: " The document library ", value: "doc" },
        { name: " Component library ", value: 'component' },
        { name: "react native app", value: "react-native" },
        { name: "hooks library ", value: "hooks" },
        { name: " Micro front end base ", value: "micro-pedestal" },
        { name: " Micro front terminal service ", value: "micro-service" }
      ]
    }
  ]).then(answer => {
    callback(answer.type);
  });
}
 Copy code 

create project

When we choose to create react-web when , How do we get the template project ?

There are roughly three ways :

  • long-range clone Formwork works

  • npm Installation of formwork works , Again cp -r *

  • Formwork works and cli Put together , direct cp -r *

here , We can choose npm install , I personally suggest this .

Installation code , May refer to :

/* * @param {String} root  The installation directory  * @param {Array<string>} dependencies  Dependent arrays  * @param {Boolean} verbose */
function install(root, dependencies, verbose) => {
    return new Promise((resolve, reject) => {
      let command = 'yarnpkg';
  
      let args = ['add', '--exact'];
  
      [].push.apply(args, dependencies);
  
      args.push('--cwd');
      args.push(root);
  
      if (verbose) {
        args.push('--verbose');
      }
  
      const child = spawn(command, args, { stdio: 'inherit' });
      child.on('close', code => {
        if (code !== 0) {
          reject({
            command: `${command} ${args.join(' ')}`,
          });
          return;
        }
        resolve();
      });
    });
  }
 Copy code 

Effect display

image.png

image.png

6、 ... and . summary

The engineering of the front end is the direction that every senior engineer must specialize in , engineering !== webpack, And the whole big front-end engineering is be based on nodejs. nodejs It can be said that it is a required course for front-end Engineers ~

It's not easy to code words , Praise more and pay attention to ~

copyright notice
author[Lao Liu talks big],Please bring the original link to reprint, thank you.
https://en.qdmana.com/2021/08/20210827140256244k.html

guess what you like

Random recommended