preface

1. Why not just use create-react-app?

First of all, create-React-App (CRA for short) is considered the best scaffolding in the React world in terms of the richness of functions and general design. However, CRA also has some disadvantages. For example, due to its deep encapsulation of the basic configuration of WebPack, This results in developers having to configure the CRA documentation API, which is completely different from WebPack. Once the configuration in the document does not meet the requirements, you need to eject to expose the original WebPack configuration, but this step is irreversible, making it very difficult to upgrade the version later, and sometimes it is not very convenient to change the original WebPack configuration. .

To address these issues, there are even solutions like CRACO that are designed to reconfigure CRA. This makes the problem even more complicated. Most of the time, developers just want a scaffold that is flexible and simple enough to build popular features like ESLint, HMR, typescript, px2REM, etc., and to customize the configuration, they can just use their webPack knowledge to modify the source code directly. Therefore, this is the main point of this article, to create a scaffold configuration that is flexible and simple enough.

2. What are the important changes from Webpack V5 to V4?

  1. More convenient configuration experience. Loaders or plugins that were used a lot before: for exampleUrl-loader, file-loader, clean-webpack-pluginEtc are no longer required, with built-in support.
  2. Many third-party loaders have fault updates. For example, if the latest less-loader is installed in Webpack4 by default, an error will be reported when using optionsthis.getOptions is not function.
  3. More powerful persistent cache, cache option
  4. Federated module.

The body of the

1. Create a project

mkdir build-react-app
cd build-react-app
npm init -y
Copy the code

2. Install webpack

NPM I webpack webpack- CLI -d // Recommended local installation, version more controllableCopy the code

3. Early experience

Create a new SRC /index.js file and write something to it

const text = `hello webpack`;
document.write(text)
Copy the code

You can’t use the global command webpack XXX directly because of the local installation, but you can run it this way

/node_modules/.bin/ webpack. / SRC /index.js // the path can also be omitted, as this is the defaultCopy the code

It works if NPM v6+npx webpack.

Packed successfully. If you don’t trust, you can check againdist/main.js(default output path) to see if the packaged content is normal.

/node_modules/.bin/webpack to make this a little bit more difficult, you can use NPM script’s concise way: open package.json and create the following command

  "scripts": {
    "build":"webpack ./src/index.js"
  },
Copy the code

Run NPM run build directly.

4. Parse CSS, images, and font resources

New webpack.config.js (webpack reads the configuration in this file by default)

4.1 analytical CSS

npm i style-loader css-loader -D
Copy the code
module.exports = {
    entry:'./src/index.js',
    module:{
        rules:[
            {
                test:/\.css$/,
                use:[
                    'style-loader',
                    'css-loader'
                ]
            }
        ]
    }
}
Copy the code

In modern development, less and SASS preprocessors are preferred

NPM install sass-loader sass --save-dev // The latest version of Sass-loader is finally no longer dependent on node-sassCopy the code
npm install less-loader less  --save-dev
Copy the code
    {
        test: /\.scss$/,
        use: ["style-loader", "css-loader", "sass-loader"],
      },
      {
        test: /\.less$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
Copy the code

4.2 the CSS

npm i mini-css-extract-plugin -D
Copy the code

The mini-CSS-extract-plugin has a number of advantages over the extract-text-webpack-plugin commonly used in the past, as detailed here.

. In addition, style – loader and MiniCssExtractPlugin loader is used at the same time of conflict, it is easy to understand, a took the form of style tags inserted at the head page, a reference in the link way out as a separate style file, this is a single topic selection. Generally, the former is selected in development time and the latter is selected in production environment.

Process. env and NODE_ENV are recommended for webpack5. You can use the node-env Settings in webpack-CLI.

package.json

"scripts": {
    "dev": "webpack serve --node-env development",
    "build": "webpack --node-env production"
  },
Copy the code

webpack.config.js

. const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const isProd = process.env.NODE_ENV === 'production'; . module.exports = { ... module:{ mode: isProd ? "production" : "development", rules:[ ... { test: /\.css$/, use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader"], }, { test: /\.scss$/, use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader", "sass-loader"], }, { test: /\.less$/, use: [isProd ? MiniCssExtractPlugin.loader : "style-loader", "css-loader", "less-loader"], }, ] ... }, plugins: [// CSS file separate extract isProd && new MiniCssExtractPlugin({filename: 'css/[name]_[contenthash:8].css', }), ].filter(Boolean), ... . }Copy the code

4.3 Analyzing Pictures

/ / {resolution image resources test: / \. (PNG | SVG | JPG | jpeg | GIF) $/ I type: 'asset' parser: {dataUrlCondition: {maxSize: 10 * 1024, // 10kb }, }, generator: { filename: 'img/[name][hash][ext][query]', }, },Copy the code

The asset Module is used to handle the file type module in WebPack5. If you specify a resource type, webPack will automatically handle it for you. Asset is used here because it automatically helps you choose between asset/inline resource inlining or asset/resource moving the file as it is, depending on the size limit of the incoming resource.

4.4 Parsing font Files

{/ / parsing font resources test: / \. (woff | woff2 | eot | the vera.ttf | otf) $/ I type: 'asset/resource,},Copy the code

5. Use the Babel degrade JS syntax

5.1 installation Babel

npm install @babel/core @babel/cli -D
Copy the code

5.2 the Babel – loader

Babel is used in Webpack by way of babel-loader

npm install babel-loader -D
Copy the code
module.exports = { entry: "./src/index.js", module: { rules: [ { test: /\.js$/, use: ["babel-loader"], exclude: /node_modules/, }, ... ] ,}};Copy the code

5.3 the Babel configuration

@babel/preset-env is an intelligent preset that can be used for which syntactic conversions and polyfills are required for the intelligent preset of the target environment. This makes life easier for developers and also makes JavaScript packages smaller!

npm i @babel/preset-env -D
Copy the code

Next, a target environment configuration is needed, because Browserslist can also be read by many other tools besides Babel (such as -postcss-preset -env, Autoprefixer), so it is recommended to refer to a public place. I chose to do the configuration in package.json or a separate.browserslistrc file (I don’t want a bunch of separate configuration files in the project root directory). package.json

. "Browserslist" : [" > 1% ", "the last 2 versions", "not ie < = 8," "Android > = 4.0"]...Copy the code

5.5 polyfill as needed

Implement polyfill, usually using core-js, and set useBuiltIns in @babel/preset-env to Usage, and don’t forget to specify the version of Corejs you install (important).

npm i core-js -D
Copy the code

5.6 Configuration file of Babel

There are many ways to choose, the author used to choose THE JS way, because the format or annotation is more flexible than JSON, at the same time compared to. Babelrc way, can add some JS logic judgment, more convenient.

To build Babel. Config. Js

Module. exports = {presets: [['@babel/preset-env', {useBuiltIns: 'usage', corejs: '3.18.2',},],]};Copy the code

6. Clean up the last build

module.exports = { ... output: { filename: 'js/[name]_[chunkhash:8].js', path: path.resolve(__dirname, '.. Dist '), clean: true, // equivalent to the CleanWebpackPlugin}... };Copy the code

7. The react

npm i react react-dom 
Copy the code

Parsing JSX

webpack.config.js

{ test: /\.jsx? $/, use: ["babel-loader"], exclude: /node_modules/, },Copy the code
npm i @babel/preset-react -D
Copy the code

babel.config.js

module.exports = {
  presets: [
    ...
    "@babel/preset-react",
  ],
};

Copy the code

8. Static resources automatically insert HTML templates

npm i html-webpack-plugin -D
Copy the code
  plugins:[
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, `./src/index.html`),
      filename: `index.html`,
      inject: 'body',
      chunks: 'all',
      minify: {
          // html5:true,
          minifyJS: true,
      },
    })
  ]
Copy the code

9. Development server

npm i webpack-dev-server -D
Copy the code

Distinguish between development and production commands

  "scripts": {
    "dev": "webpack serve",
    "build": "webpack"
  },
Copy the code

webpack.config.js

. devServer: { static: './dist', open: true, compress: true, port: 3000, https: true, hot: true, }, ...Copy the code

10. HMR hot update

HMR stands for Module thermal replacement. First, some people have long misunderstood HMR as a hot update module that simply listens for code changes and automatically refreshes the browser. Instead, HMR replaces the modified module without refreshing the page as much as possible. A simple example, you are in a long form the debugger, entry from the top to the bottom of a pile of test data, is one of a certain field component to locate problems after you modify one line of code, and stored, at this time, the tragedy happened, the page refresh, all state is missing, and to fill in again from beginning to end, look have changed! Js HMR, webpack default support, but in the front-end scenario is the most used in fact. Vue. JSX and other component files hot update. There are more steps to be taken, such as triggering the re-rendering of the corresponding component when the module replacement is complete, and keeping the state of the other components as unchanged as possible. So the framework level of HMR implementation, also need to framework developers or corresponding framework community with some webpack module. Hot hook to jointly implement. In the React world, there are currently two main implementations:

  • The first loader-based react-hot-loader uses hoc to trigger the rerendering of local components, which incurs some code.
  • The second option is officially supported React-Refresh, which allows the authorities to do the HMR directly at the framework level, making it easy to achieve zero intrusion into the development code. This is the most recommended method now. The following is the community’s implementation based on the official API.
npm install @pmmmwh/react-refresh-webpack-plugin react-refresh -D
Copy the code

webpack.config.js

pulgins:[ ... !isProd && new ReactRefreshPlugin(), ... ] .filter(Boolean)Copy the code

babel.config.js

const isDev =  process.env.NODE_ENV === "development";
module.exports = {
  ...  
  plugins:[
    isDev && 'react-refresh/babel'
  ].filter(Boolean)
};
Copy the code

11. typescript

One thing to note here is that an important capability of Typecript is the ability to compile TS code (TSC) into a production environment that recognizes running JS code, which is actually a very realistic step. There is no way to run TS code directly in a browser or Node environment, but this feature overlaps with Babel’s. Earlier we passed TS code to TypeScript for JS and then passed the JS code to Babel for lower JS, so we had to configure two compilers and compile twice every time we made a change, which was pretty inefficient. The TS team was aware of this problem for a long time. TypeScript and Babel: A beautiful marriage, @babel/preset- TypeScript is the result of the TS team seeking out the Babel team for a year. So the current trend is to make TS more of a “tool” for improving the development experience, allowing developers to take full advantage of the static benefits it brings, leaving compilation to Babel, which is more powerful. Babel-loader + @babel/preset-typescript is encouraged to replace awesome-typescript-loader and TS-loader entirely. For details about this hybrid technology, see the TS official website. TSC is used for types

To install them

npm i typescript @babel/preset-typescript -D
Copy the code

Don’t forget that we are a React project, so install the package type in advance

npm install @types/react @types/react-dom -D
Copy the code

/node_modules/.bin/ TSC –init /node_modules/.bin/ TSC –init

{" compilerOptions ": {" allowSyntheticDefaultImports" : true, / / allow never set default default import export modules "experimentalDecorators" : true, "emitDecoratorMetadata": true, "outDir": "./dist", "sourceMap": true, "noImplicitAny": true, "module": "commonjs", "target": "es5", "jsx": "react", "lib": ["es6","dom"], "paths": { "@/*": ["./src/*"] }, }, "include": [ "./src/**/*", ], "exclude": [ "node_modules", ] }Copy the code

Next, change the webpack configuration webpack.config.js

module.exports = { ... entry: "/src/index.tsx", ... Resolve: {'@': path.resolve(__dirname, '.. /src'), }, extensions: [/ / file extensions. By default only supports js and json, add ts file support 'ts',' benchmark ', 'js',' JSX ', 'json', 'CSS', 'SCSS', 'less',],}, the module: {rules: [ { test: /\.(jsx?|tsx?)$/, use: ["babel-loader"], exclude: /node_modules/, }, ... ] ,}}Copy the code

babel.config.js

module.exports = {
  presets: [
    [
    ...
    "@babel/preset-typescript"
  ],
  ...  
};
Copy the code

12. eslint

The installation

npm install eslint --save-dev
Copy the code

The configuration document creates a new ESLint configuration file, which is created automatically by NPX esLint –init.

The following files and contents are automatically generated

module.exports = {
    "env": {
        "browser": true,
        "es2021": true
    },
    "extends": [
        "eslint:recommended",
        "plugin:react/recommended",
        "plugin:@typescript-eslint/recommended"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "ecmaFeatures": {
            "jsx": true
        },
        "ecmaVersion": 13,
        "sourceType": "module"
    },
    "plugins": [
        "react",
        "@typescript-eslint"
    ],
    "rules": {
    }
};

Copy the code

The next step is to customize some rules. The industry is generally recognized that Airbnb’s norms are better, so they are directly used here.

npx install-peerdeps --dev eslint-config-airbnb
Copy the code

.eslintrc

{... "extends": [ "airbnb", "airbnb/hooks" ], ... }Copy the code

The complete.eslintrc configuration is as follows:

{ "env": { "browser": true, "es2021": true }, "extends": [ "airbnb", "airbnb/hooks" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaFeatures": { "jsx": true }, "ecmaVersion": "latest", "sourceType": } "module", "plugins" : [" @ typescript - eslint ", "react"], "Settings" : {" react ": {" version" : "17.0.2"}}, "rules" : {}}Copy the code

The eslint-webpack-plugin is now the most recommended version of eslint-webpack-plugin.

npm i eslint-webpack-plugin -D
Copy the code

webpack.config.js

const ESLintPlugin = require('eslint-webpack-plugin');

module.exports = {
  // ...
  plugins: [new ESLintPlugin({
      extensions: ['ts', 'tsx', 'js'],
      failOnError:false,
  })],
  // ...
};
Copy the code

Start the project, write some error code, the following message is displayed, the configuration is successful.

13. Multi-page Build (Contract form)

Brief introduction:

  • MPA: As opposed to SPA, a separate page hosting a separate business module is a common pattern in development.
  • Contract form: it is the use of the convention on the file directory, (such as pages are placed under SRC/Pages), more convenient configuration of multi-entry, multiple HtmlWebpackPlugin, realize the multi-page application of zero configuration, out of the box.

webpack.config.js

/ / const getMPA = () => {const entryFiles = glob.sync(path.resolve(__dirname,)) "./src/pages/*/index.{ts,tsx}") ); const entry = {}; const htmlWebpackPlugins = []; Const getPageName = (filePath) => {const match = filePath. Match (/ SRC \/pages\/([^/]*)/); return match ? match[1] : null; }; entryFiles.forEach((filePath) => { const pageName = getPageName(filePath); if (! PageName) {throw new Error(" multipage file organization by "/ SRC /pages/{pageName}/index.ts" convention "); } entry[pageName] = filePath; htmlWebpackPlugins.push( new HtmlWebpackPlugin({ template: path.resolve( __dirname, `./src/pages/${pageName}/index.html` ), filename: `${pageName}.html`, inject: "body", chunks: [pageName], minify: { // html5:true, minifyJS: true, }, }) ); }); return { entry, htmlWebpackPlugins, }; }; // Configure multiple pages const {entry, htmlWebpackPlugins} = getMPA(); module.exports = { ... entry, output: { filename: "js/[name]_[chunkhash:8].js", path: path.resolve(__dirname, "./dist"), clean: true, }, ... plugins: [ ... ...htmlWebpackPlugins, ... };Copy the code

14. Mobile adaptation

14.1 Adopt taobao Lib-flexible solution

Automatic template filling script, manual paste copy is rejected

NPM I [email protected] -d // Make sure this version is installed, otherwise an error will be reportedCopy the code

Modify the template SRC/pages/index/index. HTML

<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta  name="viewport" content="width=device-width"> <title>index</title> <script> <%= require('raw-loader! . /.. /.. /node_modules/lib-flexible/flexible.js') %> </script> </head> <body> <div id='root'></div> </body> </html>Copy the code

14.2PX conversion REM

There are two options: postCSS plugin and Px2REM-loader. Here I choose the first option because postCSS is more powerful:

npm i postcss postcss-loader postcss-pxtorem -D
Copy the code

Because the project uses SASS and less to process style files, their Loader configuration is actually very reusable, so extract a general processing function.

/** * loaders * @param {*} preprocessor */ const getStyleFileLoaders = (preprocessor) => {const getStyleFileLoaders = (preprocessor) => {const StyleFileLoaders = [isProd? MiniCssExtractPlugin. Loader: 'style - loader', / / CSS production out of 'CSS - loader, {loader: 'postcss-loader',// postCSs-loader parsing time must be after preprocessing options: {postcssOptions: {plugins: ['postcss-preset-env',['postcss-pxtorem', { rootValue: 75, propList: ['*'] }]], }, }, }, ]; preprocessor && styleFileLoaders.push(preprocessor); // return styleFileLoaders; };Copy the code

14.3 Auto-complete the browser style prefix

The autoprefixer plugin is used for this, but postCSS provides a smarter option, postCSS-preset -env, which will automatically complete browser-compatible prefixes based on browserslist configured in the project.

npm i postcss-preset-env -D
Copy the code

Add webpack.config.js to postcssOptions above.

  plugins: [
  'postcss-preset-env',
  ['postcss-pxtorem', { rootValue: 75, propList: ['*'] }]
  ],
Copy the code

15. Build speed optimization

15.1 Packaging Multiple Processes

We may have heard of happy-Pack, but this package will not be maintained in the future. The official solution webPack currently provides is Thread-loader. The advantage is to improve the packaging speed, the disadvantage is that each process will have a cost of starting and communicating, so it is not recommended for small projects, may be negative optimization. (Babel-loader takes the longest, so thread-loader is used to optimize it)

{ test: /\.(jsx? |tsx?) $/, use: ["thread-loader","babel-loader"], exclude: /node_modules/, },Copy the code

15.2 Parallel Compression

By default, webpack enables terser-webpack-plugin to compress JS code when mode is production. Set parallel to true to enable multiple parallel compression.

webpack.config.js

const TerserWebpackPlugin = require('terser-webpack-plugin'); module.exports = { ... Optimization: {minimizer: [// '...', // Inherit the default compression, such as js compression terser-webpack-plugin new TerserWebpackPlugin({parallel: // do not extractComments into separate files, similar to xxx.js.license.txt}),...] ,},... }Copy the code

15.3 Cache Build improves the secondary Build speed

In Webpackage 4, this function is usually done at the loader level, such as enabling the cache of Babel-loader

{loader:"babel-loader", options:{cacheDirectory: true, // enable caching}}Copy the code

As you can see, the cache succeeded. In webpack5, global support is provided.

webpack.config.js

Cache: {// Enable the build result cache type: isProd? 'filesystem' : 'memory', },Copy the code

As you can see, it took 10s to build the first time and only 600ms to build the second time, and the comparison is still brutal.

Optimize the production environment

When Mode is set to Production, webpack does a lot of optimization by default, but it’s also explicitly listed here to give you an impression.

16.1 treeShaking

Remove useless JS imports and only support static ES6 import/export.

16.2 cope hoisting

The built code has a large number of closure codes, which will try to make each module code in the same scope, variable conflicts through renaming and other ways to solve.

16.3 compressed CSS

npm i css-minimizer-webpack-plugin -D
Copy the code

webpack.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); . module.exports = { ... Optimization: {minimizer: [... // new CssMinimizerPlugin(),],},... }Copy the code

16.4 Erasing Useless CSS Code

npm i purgecss-webpack-plugin glob -D
Copy the code

webpack.config.js

const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); const path = require('path'); const glob = require('glob'); . module.exports = { ... New PurgeCSSPlugin({paths: glob.sync(' ${path.join(__dirname, './ SRC ')}/**/* ', {nodir: true }), }), ], ... }Copy the code

16.5 Online Cache Optimization

The React stack, or other packages that do not change often, can be unpacked individually to improve the speed of reloading pages. External can also be used for CDN extraction of NPM packages. This is meaningful for cross-project cache optimization, but it also limits the package versions that need to be installed for each project. Otherwise, there may be bugs caused by inconsistency between online packages and local packages. Since most of the current online static resources are hosted by CDN and are fast enough to be accessed, the common packages of this project can be extracted separately with splitChunks to avoid inconsistent development and production versions caused by external.

Optimization: {splitChunks: {minSize: 5000, cacheGroups: {// React technology stack related reactVendor: {test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/, name: 'reactVendor', chunks: 'all', priority: 1, }, // node_modules defaultVendor: { test: /[\\/]node_modules[\\/]/, name: 'defaultVendor', chunks: 'all', // minChunks: 1, priority: 0, }, }, }, },Copy the code

17. Split the WebPack configuration

With the increasing number of configuration items, expressions such as isDev and isProd are used everywhere in the configuration to distinguish the configuration required in different modes, so the whole webpack.config.js needs to be split more clearly.

The webpack-cli command distinguishes the NodeEnv environment and distinguishes different configuration files according to the environment

  "scripts": {
    "dev": "webpack serve --node-env development",
    "build": "webpack --node-env production"
  },
Copy the code

Create the build directory to save the configuration file

  • Build /webpack.dev.js development configuration
  • Build /webpack.prod.js production configuration
  • Build /webpack.base.js common configuration
  • Reusable methods extracted from build/webpack.utils.js
  • Webpack.config.js configuration entry (the file specified by webpack by default)

Use the webpack-Merge package to solve the merging of base configurations with environment-specific configurations.

onst { merge } = require('webpack-merge');
const baseConfig = require('./build/webpack.base');
const developmentConfig = require('./build/webpack.dev');
const productionConfig = require('./build/webpack.prod');

module.exports = (env, argv) => {
    switch (argv.nodeEnv) {
        case 'development':
            return merge(baseConfig, developmentConfig);
        case 'production':
            return merge(baseConfig, productionConfig);
        default:
            throw new Error('No matching configuration was found!');
    }
};
Copy the code

The tail

The React scaffolding with mainstream functionality is almost complete. Webpack configuration is generally without any technical content, the exercise of their own mainly lies in:

  • Craters, various versions of Babel, Loader, ESLint, typescript, etc., and apis
  • The contrast between different solutions
  • Document information collection capability

Overall, it is a time-consuming, boring and forgetful job. The main purpose of the author of this article, but also want to make a record of this complete configuration process, useful to the time later, you can quickly refer to. Github address of the project. The complete configuration of this article has been uploaded. Please feel free to issue any questions or exchanges.